From 1e47bc97d85a13e57cc02b9521f5cca15f045097 Mon Sep 17 00:00:00 2001 From: jensp Date: Thu, 27 Sep 2018 17:28:01 +0000 Subject: [PATCH] CCM NG: Revised Import/Export system git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@5698 8810af33-2d31-482b-a856-94f89814c4df Former-commit-id: ab15c9b9805fd667aec56e4ee4e767c4a2a55012 --- .../imexport/EntityImExporterTreeManager.java | 192 ++++++++++++++++-- .../org/libreccm/imexport/ImportExport.java | 119 +++++------ .../modules/DependencyTreeManager.java | 58 +++--- 3 files changed, 274 insertions(+), 95 deletions(-) diff --git a/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java index 23445348c..26f6529bc 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java @@ -18,21 +18,57 @@ */ package org.libreccm.imexport; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** + * This class implements topological sorting to determine the order in which + * entities are imported. + * + * The class is used by creating an instance with the parameterless constructor. + * To create the tree/graph call the null {@link #generateTree(java.util.List)} + * method. With the returned list of nodes call the + * {@link #orderImExporters(java.util.List)} method. The list returned by + * {@link #orderImExporters(java.util.List)} contains all + * {@link EntityImExporter} in the order. + * + * This class is not not part of the public API. + * + ** More information about topological sorting: + * https://en.wikipedia.org/wiki/Topological_sorting * * @author Jens Pelzetter */ final class EntityImExporterTreeManager { + private static final Logger LOGGER = LogManager + .getLogger(EntityImExporterTreeManager.class); + + /** + * Initialises the tree with the provided list of {@link EntityImExporter}s. + * + * @param imExporters The available {@link EntityImExporter}s. + * + * @return An ordered list of the tree nodes. + * + * @throws DependencyException If something is wrong with the dependency + * tree. For example if a module on which + * another module depends is missing or if a + * cycle is detected in the dependency tree. + */ public List generateTree( final List> imExporters) throws DependencyException { + LOGGER.info("Starting to generate dependency tree..."); + + //Create the tree nodes. A HashMap is used to avoid duplicates and + //the lookup the nodes based on their name. final Map nodes = imExporters .stream() .map(EntityImExporterTreeNode::new) @@ -46,32 +82,139 @@ final class EntityImExporterTreeManager { .getName(), node -> node)); + //Add the dependency relations to the nodes for (final EntityImExporter imExporter : imExporters) { addDependencyRelations(imExporter, nodes); } - + + //Generate the node list final List nodeList = new ArrayList<>(); - for (final Map.Entry entry - : nodes.entrySet()) { - + for (final Map.Entry entry + : nodes.entrySet()) { + nodeList.add(entry.getValue()); } - + + LOGGER.info("Dependency tree generated."); + return nodeList; } + /** + * Generates an ordered list of the tree nodes which can be used to process + * the imports in the correct order. + * + * In this method the topological sorting happens. + * + * @param nodes The nodes of the dependency tree. + * + * @return A ordered list of the tree nodes. + * + * @throws DependencyException If something is wrong with dependency graph. + */ + public List orderImExporters( + final List nodes) + throws DependencyException { + + LOGGER.info("Creating an ordered list from the dependency tree..."); + + //List for the ordered and resolved nodes. + final List orderedNodes = new ArrayList<>(); + final List resolvedNodes = new ArrayList<>(); + + LOGGER.info("Looking for EntityImExporters which do not depend on any " + + "other EntityImExporters."); + //Find all nodes which do not depend on any other nodes. These + //nodes are used as starting point for the sorting. + for (final EntityImExporterTreeNode node : nodes) { + + if (node.getDependsOn().isEmpty()) { + LOGGER.info( + "\tNode \"{}\" does not depend on any other module", + node.getEntityImExporter().getClass().getName()); + resolvedNodes.add(node); + } + } + + LOGGER.info("Ordering remaining nodes..."); + while (!resolvedNodes.isEmpty()) { + + //Remove the first node from the resolved nodes list + final EntityImExporterTreeNode current = resolvedNodes.remove(0); + LOGGER.info("\tProcessing node for EntityImExporter \"{}\"...", + current.getEntityImExporter().getClass().getName()); + + //Add the node to the ordered modules list. + orderedNodes.add(current); + + //Remove the edges to the current node. + for (final EntityImExporterTreeNode dependent + : current.getDependentImExporters()) { + + dependent.removeDependsOn(current); + + //If the dependent node has no more dependsOn relations put + //the node into the resolved modules list. + if (dependent.getDependsOn().isEmpty()) { + resolvedNodes.add(dependent); + } + } + } + + //Check if all nodes have been ordered. If not the tree has at least on + //on cycle and can't be processed. + if (orderedNodes.size() == nodes.size()) { + + LOGGER.info("EntityImExporter dependency graph processed " + + "successfully. EntityImExporters in order:"); + for (final EntityImExporterTreeNode node : orderedNodes) { + LOGGER.info("\t{}", + node.getEntityImExporter().getClass().getName()); + } + + return orderedNodes; + + } else { + + LOGGER.fatal("The EntityImExporter dependency graph has at least " + + "one cycle."); + throw new DependencyException("The EntityImExporter dependency " + + "graph has at least one cycle."); + } + + } + + /** + * Helper method for adding the dependency relations for an + * {@link EntityImExporter} to the nodes. + * + * @param imExporter The current {@link EntityImExporter}. + * @param nodes The map of nodes. + * + * @throws DependencyException If something goes wrong. + */ private void addDependencyRelations( final EntityImExporter imExporter, final Map nodes) throws DependencyException { + //Get the dependencies of the current EntityImExporter final Processes processes = imExporter .getClass() .getAnnotation(Processes.class); - final String className = imExporter.getClass().getName(); + //Get the name of the module from the module info. + final String className = imExporter.getClass().getName(); + LOGGER + .info("Adding dependency relations for EntityImExporter \"{}\"...", + className); + + //Check if the nodes map has an entry for the EntityImExporter. if (!nodes.containsKey(className)) { + LOGGER.fatal("EntityImExporter nodes map does contain an entry for " + + "\"{}\". That should not happen.", + className); throw new IllegalArgumentException(String.format( "The nodes map does not contain a node for " + "EntityImExporter \"%s\"." @@ -79,34 +222,59 @@ final class EntityImExporterTreeManager { className)); } + //Get the node from the map final EntityImExporterTreeNode node = nodes.get(className); - + LOGGER + .info("Processing required modules for EntityImExporter \"{}\"...", + className); + //Process the EntityImExporter required by the current module and add + //the dependency relations. for (final Class clazz : processes.dependsOn()) { addDependencyRelation(nodes, node, clazz); } } + /** + * Helper method for adding a single dependency relation. + * + * @param nodes The map of tree nodes. + * @param node The node to which the dependency relations are + * added. + * @param requiredClass The type which is required by the current + * module/node. + * + * @throws DependencyException + */ private void addDependencyRelation( final Map nodes, EntityImExporterTreeNode node, - Class clazz) + Class requiredClass) throws DependencyException { - if (!nodes.containsKey(clazz.getName())) { + LOGGER.info("\tEntityImExporter for \"{}\" requires " + + "EntityImExporter for \"{}\".", + node.getEntityImExporter().getClass().getName(), + requiredClass.getName()); + //Check if the nodes list has an entry for the required module. + if (!nodes.containsKey(requiredClass.getName())) { + + LOGGER.fatal("Required EntityImExporter for \"{}\" no found.", + requiredClass.getName()); throw new DependencyException(String.format( "EntityImExporter for type \"%s\" depends on type \"%s\" " + "but no EntityImExporter for type \"%s\" is available.", node.getEntityImExporter().getClass().getAnnotation( Processes.class).type().getName(), - clazz.getName(), - clazz.getName())); + requiredClass.getName(), + requiredClass.getName())); } final EntityImExporterTreeNode dependencyNode = nodes - .get(clazz.getName()); + .get(requiredClass.getName()); + //Create the dependencies relations. node.addDependsOn(dependencyNode); dependencyNode.addDependentImExporter(node); } diff --git a/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java b/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java index 816cd5f03..3c81f202e 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java @@ -102,26 +102,30 @@ public class ImportExport { importName)); } } catch (FileAccessException - | FileDoesNotExistException - | InsufficientPermissionsException ex) { + | FileDoesNotExistException + | InsufficientPermissionsException ex) { throw new UnexpectedErrorException(ex); } - final String manifestPath = String.format("%s/ccm-export.json", - importsPath); - try (final InputStream manifestInputStream = ccmFiles - .createInputStream(importsPath)) { + final List> imExportersList = new ArrayList<>(); + imExporters.forEach(imExporter -> imExportersList.add(imExporter)); - final JsonReader manifestReader = Json - .createReader(manifestInputStream); - final JsonObject manifest = manifestReader.readObject(); + try { + final EntityImExporterTreeManager treeManager + = new EntityImExporterTreeManager(); + final List tree = treeManager + .generateTree( + imExportersList); + final List orderedNodes = treeManager + .orderImExporters(tree); - } catch (IOException - | FileDoesNotExistException - | FileAccessException - | InsufficientPermissionsException ex) { + final ImportManifest manifest = createImportManifest(importName); + //ToDo + + } catch (DependencyException ex) { + throw new UnexpectedErrorException(ex); } throw new UnsupportedOperationException(); @@ -134,8 +138,8 @@ public class ImportExport { try { importArchivePaths = ccmFiles.listFiles("imports"); } catch (FileAccessException - | FileDoesNotExistException - | InsufficientPermissionsException ex) { + | FileDoesNotExistException + | InsufficientPermissionsException ex) { throw new UnexpectedErrorException(ex); } @@ -168,52 +172,53 @@ public class ImportExport { final String manifestPath = String.format("imports/%s/ccm-export.json", path); - final InputStream inputStream; - try { - inputStream = ccmFiles.createInputStream(manifestPath); - } catch (FileAccessException - | FileDoesNotExistException - | InsufficientPermissionsException ex) { + try (final InputStream inputStream = ccmFiles + .createInputStream(manifestPath)) { + + final JsonReader reader = Json.createReader(inputStream); + final JsonObject manifestJson = reader.readObject(); + + if (!manifestJson.containsKey("created")) { + throw new IllegalArgumentException(String.format( + "The manifest file \"%s\" is malformed. " + + "Key \"created\" is missing.", + manifestPath)); + } + + if (!manifestJson.containsKey("onServer")) { + throw new IllegalArgumentException(String.format( + "The manifest file \"%s\" is malformed. " + + "Key \"onServer\" is missing.", + manifestPath)); + } + + if (!manifestJson.containsKey("types")) { + throw new IllegalArgumentException(String.format( + "The manifest file \"%s\" is malformed. " + + "Key \"types\" is missing.", + manifestPath)); + } + + final LocalDateTime created = LocalDateTime + .parse(manifestJson.getString("created")); + final String onServer = manifestJson.getString("onServer"); + final List types = manifestJson.getJsonArray("types") + .stream() + .map(value -> value.toString()) + .collect(Collectors.toList()); + + return new ImportManifest( + Date.from(created.atZone(ZoneId.of("UTC")).toInstant()), + onServer, + types); + } catch (IOException + | FileAccessException + | FileDoesNotExistException + | InsufficientPermissionsException ex) { throw new UnexpectedErrorException(ex); } - final JsonReader reader = Json.createReader(inputStream); - final JsonObject manifestJson = reader.readObject(); - - if (!manifestJson.containsKey("created")) { - throw new IllegalArgumentException(String.format( - "The manifest file \"%s\" is malformed. " - + "Key \"created\" is missing.", - manifestPath)); - } - - if (!manifestJson.containsKey("onServer")) { - throw new IllegalArgumentException(String.format( - "The manifest file \"%s\" is malformed. " - + "Key \"onServer\" is missing.", - manifestPath)); - } - - if (!manifestJson.containsKey("types")) { - throw new IllegalArgumentException(String.format( - "The manifest file \"%s\" is malformed. " - + "Key \"types\" is missing.", - manifestPath)); - } - - final LocalDateTime created = LocalDateTime - .parse(manifestJson.getString("created")); - final String onServer = manifestJson.getString("onServer"); - final List types = manifestJson.getJsonArray("types") - .stream() - .map(value -> value.toString()) - .collect(Collectors.toList()); - - return new ImportManifest( - Date.from(created.atZone(ZoneId.of("UTC")).toInstant()), - onServer, - types); } } diff --git a/ccm-core/src/main/java/org/libreccm/modules/DependencyTreeManager.java b/ccm-core/src/main/java/org/libreccm/modules/DependencyTreeManager.java index 3da274121..3dce0d056 100644 --- a/ccm-core/src/main/java/org/libreccm/modules/DependencyTreeManager.java +++ b/ccm-core/src/main/java/org/libreccm/modules/DependencyTreeManager.java @@ -34,10 +34,10 @@ import java.util.Map; * The class is used by creating an instance with the parameterless constructor. * To create the tree/graph call the * {@link #generateTree(javax.enterprise.inject.Instance)} method. With the - * returned list of nodes call the the {@link #orderModules(java.util.List)} + * returned list of nodes call the {@link #orderModules(java.util.List)} * method. The list returned by {@link #orderModules(java.util.List)} contains * all modules in order. - * + * * This class is not part of the public API. * * More information about topological sorting: @@ -52,13 +52,15 @@ final class DependencyTreeManager { /** * Initialises the tree with the provided list of modules. - * + * * @param modules The module for which a dependency tree is generated. + * * @return An ordered list of tree nodes. - * - * @throws DependencyException If something is wrong with the dependency - * tree. For example if a module on which another module depends is missing - * or if a cycle is detected in the dependency tree. + * + * @throws DependencyException If something is wrong with the dependency + * tree. For example if a module on which + * another module depends is missing or if a + * cycle is detected in the dependency tree. */ public List generateTree(final Iterable modules) throws DependencyException { @@ -89,15 +91,17 @@ final class DependencyTreeManager { } /** - * Generates an ordered list of the tree nodes which can be used to + * Generates an ordered list of the tree nodes which can be used to * initialise the modules in correct order. - * + * * In this method the topological sorting happens. - * + * * @param dependencyTree The list of tree nodes of the dependency tree. + * * @return A ordered list of the tree nodes. - * @throws DependencyException If something is wrong with the dependency - * tree. + * + * @throws DependencyException If something is wrong with the dependency + * tree. */ public List orderModules(final List dependencyTree) throws DependencyException { @@ -127,7 +131,7 @@ final class DependencyTreeManager { final TreeNode current = resolvedModules.remove(0); LOGGER.info("\tProcessing node for module \"{}\"...", current.getModuleInfo().getModuleName()); - + //Add the node to the ordered modules list. orderedModules.add(current); @@ -135,7 +139,7 @@ final class DependencyTreeManager { for (final TreeNode dependent : current.getDependentModules()) { dependent.removeDependsOn(current); - //If the dependent node has node more dependsOn relations put + //If the dependent node has no more dependsOn relations put //the node into the resolved modules list. if (dependent.getDependsOn().isEmpty()) { resolvedModules.add(dependent); @@ -172,13 +176,13 @@ final class DependencyTreeManager { } /** - * Helper method for adding the dependency relations for a module to the + * Helper method for adding the dependency relations for a module to the * nodes. - * + * * @param module The module. - * @param nodes The map of nodes. - * - * @throws DependencyException If something goes wrong. + * @param nodes The map of nodes. + * + * @throws DependencyException If something goes wrong. */ private void addDependencyRelations(final CcmModule module, final Map nodes) @@ -220,11 +224,13 @@ final class DependencyTreeManager { /** * Helper method for adding a single dependency relation. - * - * @param nodes The map of tree nodes. - * @param node The node to which the dependency relations are added. + * + * @param nodes The map of tree nodes. + * @param node The node to which the dependency relations are + * added. * @param requiredModule The module required by the current module/node. - * @throws DependencyException + * + * @throws DependencyException */ private void addDependencyRelation(final Map nodes, final TreeNode node, @@ -301,11 +307,11 @@ final class DependencyTreeManager { if ((minRequiredVersion == null || minRequiredVersion.isEmpty()) && (maxRequiredVersion == null || maxRequiredVersion. - isEmpty())) { + isEmpty())) { return true; } else if ((minRequiredVersion != null && !minRequiredVersion.isEmpty()) && (maxRequiredVersion == null || maxRequiredVersion - .isEmpty())) { + .isEmpty())) { final ComparableVersion minVersion = new ComparableVersion( minRequiredVersion); final ComparableVersion version = new ComparableVersion( @@ -314,7 +320,7 @@ final class DependencyTreeManager { return minVersion.compareTo(version) <= 0; } else if ((minRequiredVersion == null || minRequiredVersion.isEmpty()) && (maxRequiredVersion != null && !maxRequiredVersion - .isEmpty())) { + .isEmpty())) { final ComparableVersion maxVersion = new ComparableVersion( maxRequiredVersion); final ComparableVersion version = new ComparableVersion(