CCM NG: Revised Import/Export system

git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@5698 8810af33-2d31-482b-a856-94f89814c4df
ccm-docs
jensp 2018-09-27 17:28:01 +00:00
parent 2367bca059
commit ccad3258b7
3 changed files with 274 additions and 95 deletions

View File

@ -18,21 +18,57 @@
*/ */
package org.libreccm.imexport; package org.libreccm.imexport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; 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 <strong>not</strong> not part of the public API.
*
** More information about topological sorting:
* <a href="https://en.wikipedia.org/wiki/Topological_sorting">https://en.wikipedia.org/wiki/Topological_sorting</a>
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
final class EntityImExporterTreeManager { 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<EntityImExporterTreeNode> generateTree( public List<EntityImExporterTreeNode> generateTree(
final List<EntityImExporter<?>> imExporters) final List<EntityImExporter<?>> imExporters)
throws DependencyException { 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<String, EntityImExporterTreeNode> nodes = imExporters final Map<String, EntityImExporterTreeNode> nodes = imExporters
.stream() .stream()
.map(EntityImExporterTreeNode::new) .map(EntityImExporterTreeNode::new)
@ -46,32 +82,139 @@ final class EntityImExporterTreeManager {
.getName(), .getName(),
node -> node)); node -> node));
//Add the dependency relations to the nodes
for (final EntityImExporter<?> imExporter : imExporters) { for (final EntityImExporter<?> imExporter : imExporters) {
addDependencyRelations(imExporter, nodes); addDependencyRelations(imExporter, nodes);
} }
//Generate the node list
final List<EntityImExporterTreeNode> nodeList = new ArrayList<>(); final List<EntityImExporterTreeNode> nodeList = new ArrayList<>();
for (final Map.Entry<String, EntityImExporterTreeNode> entry for (final Map.Entry<String, EntityImExporterTreeNode> entry
: nodes.entrySet()) { : nodes.entrySet()) {
nodeList.add(entry.getValue()); nodeList.add(entry.getValue());
} }
LOGGER.info("Dependency tree generated.");
return nodeList; 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<EntityImExporterTreeNode> orderImExporters(
final List<EntityImExporterTreeNode> nodes)
throws DependencyException {
LOGGER.info("Creating an ordered list from the dependency tree...");
//List for the ordered and resolved nodes.
final List<EntityImExporterTreeNode> orderedNodes = new ArrayList<>();
final List<EntityImExporterTreeNode> 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( private void addDependencyRelations(
final EntityImExporter<?> imExporter, final EntityImExporter<?> imExporter,
final Map<String, EntityImExporterTreeNode> nodes) final Map<String, EntityImExporterTreeNode> nodes)
throws DependencyException { throws DependencyException {
//Get the dependencies of the current EntityImExporter
final Processes processes = imExporter final Processes processes = imExporter
.getClass() .getClass()
.getAnnotation(Processes.class); .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)) { if (!nodes.containsKey(className)) {
LOGGER.fatal("EntityImExporter nodes map does contain an entry for "
+ "\"{}\". That should not happen.",
className);
throw new IllegalArgumentException(String.format( throw new IllegalArgumentException(String.format(
"The nodes map does not contain a node for " "The nodes map does not contain a node for "
+ "EntityImExporter \"%s\"." + "EntityImExporter \"%s\"."
@ -79,34 +222,59 @@ final class EntityImExporterTreeManager {
className)); className));
} }
//Get the node from the map
final EntityImExporterTreeNode node = nodes.get(className); 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<? extends Exportable> clazz : processes.dependsOn()) { for (final Class<? extends Exportable> clazz : processes.dependsOn()) {
addDependencyRelation(nodes, node, clazz); 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( private void addDependencyRelation(
final Map<String, EntityImExporterTreeNode> nodes, final Map<String, EntityImExporterTreeNode> nodes,
EntityImExporterTreeNode node, EntityImExporterTreeNode node,
Class<? extends Exportable> clazz) Class<? extends Exportable> requiredClass)
throws DependencyException { 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( throw new DependencyException(String.format(
"EntityImExporter for type \"%s\" depends on type \"%s\" " "EntityImExporter for type \"%s\" depends on type \"%s\" "
+ "but no EntityImExporter for type \"%s\" is available.", + "but no EntityImExporter for type \"%s\" is available.",
node.getEntityImExporter().getClass().getAnnotation( node.getEntityImExporter().getClass().getAnnotation(
Processes.class).type().getName(), Processes.class).type().getName(),
clazz.getName(), requiredClass.getName(),
clazz.getName())); requiredClass.getName()));
} }
final EntityImExporterTreeNode dependencyNode = nodes final EntityImExporterTreeNode dependencyNode = nodes
.get(clazz.getName()); .get(requiredClass.getName());
//Create the dependencies relations.
node.addDependsOn(dependencyNode); node.addDependsOn(dependencyNode);
dependencyNode.addDependentImExporter(node); dependencyNode.addDependentImExporter(node);
} }

View File

@ -102,26 +102,30 @@ public class ImportExport {
importName)); importName));
} }
} catch (FileAccessException } catch (FileAccessException
| FileDoesNotExistException | FileDoesNotExistException
| InsufficientPermissionsException ex) { | InsufficientPermissionsException ex) {
throw new UnexpectedErrorException(ex); throw new UnexpectedErrorException(ex);
} }
final String manifestPath = String.format("%s/ccm-export.json", final List<EntityImExporter<?>> imExportersList = new ArrayList<>();
importsPath); imExporters.forEach(imExporter -> imExportersList.add(imExporter));
try (final InputStream manifestInputStream = ccmFiles
.createInputStream(importsPath)) {
final JsonReader manifestReader = Json try {
.createReader(manifestInputStream); final EntityImExporterTreeManager treeManager
final JsonObject manifest = manifestReader.readObject(); = new EntityImExporterTreeManager();
final List<EntityImExporterTreeNode> tree = treeManager
.generateTree(
imExportersList);
final List<EntityImExporterTreeNode> orderedNodes = treeManager
.orderImExporters(tree);
} catch (IOException final ImportManifest manifest = createImportManifest(importName);
| FileDoesNotExistException
| FileAccessException
| InsufficientPermissionsException ex) {
//ToDo
} catch (DependencyException ex) {
throw new UnexpectedErrorException(ex);
} }
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
@ -134,8 +138,8 @@ public class ImportExport {
try { try {
importArchivePaths = ccmFiles.listFiles("imports"); importArchivePaths = ccmFiles.listFiles("imports");
} catch (FileAccessException } catch (FileAccessException
| FileDoesNotExistException | FileDoesNotExistException
| InsufficientPermissionsException ex) { | InsufficientPermissionsException ex) {
throw new UnexpectedErrorException(ex); throw new UnexpectedErrorException(ex);
} }
@ -168,52 +172,53 @@ public class ImportExport {
final String manifestPath = String.format("imports/%s/ccm-export.json", final String manifestPath = String.format("imports/%s/ccm-export.json",
path); path);
final InputStream inputStream; try (final InputStream inputStream = ccmFiles
try { .createInputStream(manifestPath)) {
inputStream = ccmFiles.createInputStream(manifestPath);
} catch (FileAccessException final JsonReader reader = Json.createReader(inputStream);
| FileDoesNotExistException final JsonObject manifestJson = reader.readObject();
| InsufficientPermissionsException ex) {
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<String> 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); 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<String> 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);
} }
} }

View File

@ -34,7 +34,7 @@ import java.util.Map;
* The class is used by creating an instance with the parameterless constructor. * The class is used by creating an instance with the parameterless constructor.
* To create the tree/graph call the * To create the tree/graph call the
* {@link #generateTree(javax.enterprise.inject.Instance)} method. With 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 * method. The list returned by {@link #orderModules(java.util.List)} contains
* all modules in order. * all modules in order.
* *
@ -54,11 +54,13 @@ final class DependencyTreeManager {
* Initialises the tree with the provided list of modules. * Initialises the tree with the provided list of modules.
* *
* @param modules The module for which a dependency tree is generated. * @param modules The module for which a dependency tree is generated.
*
* @return An ordered list of tree nodes. * @return An ordered list of tree nodes.
* *
* @throws DependencyException If something is wrong with the dependency * @throws DependencyException If something is wrong with the dependency
* tree. For example if a module on which another module depends is missing * tree. For example if a module on which
* or if a cycle is detected in the dependency tree. * another module depends is missing or if a
* cycle is detected in the dependency tree.
*/ */
public List<TreeNode> generateTree(final Iterable<CcmModule> modules) public List<TreeNode> generateTree(final Iterable<CcmModule> modules)
throws DependencyException { throws DependencyException {
@ -95,9 +97,11 @@ final class DependencyTreeManager {
* In this method the topological sorting happens. * In this method the topological sorting happens.
* *
* @param dependencyTree The list of tree nodes of the dependency tree. * @param dependencyTree The list of tree nodes of the dependency tree.
*
* @return A ordered list of the tree nodes. * @return A ordered list of the tree nodes.
*
* @throws DependencyException If something is wrong with the dependency * @throws DependencyException If something is wrong with the dependency
* tree. * tree.
*/ */
public List<TreeNode> orderModules(final List<TreeNode> dependencyTree) public List<TreeNode> orderModules(final List<TreeNode> dependencyTree)
throws DependencyException { throws DependencyException {
@ -135,7 +139,7 @@ final class DependencyTreeManager {
for (final TreeNode dependent : current.getDependentModules()) { for (final TreeNode dependent : current.getDependentModules()) {
dependent.removeDependsOn(current); 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. //the node into the resolved modules list.
if (dependent.getDependsOn().isEmpty()) { if (dependent.getDependsOn().isEmpty()) {
resolvedModules.add(dependent); resolvedModules.add(dependent);
@ -176,9 +180,9 @@ final class DependencyTreeManager {
* nodes. * nodes.
* *
* @param module The module. * @param module The module.
* @param nodes The map of nodes. * @param nodes The map of nodes.
* *
* @throws DependencyException If something goes wrong. * @throws DependencyException If something goes wrong.
*/ */
private void addDependencyRelations(final CcmModule module, private void addDependencyRelations(final CcmModule module,
final Map<String, TreeNode> nodes) final Map<String, TreeNode> nodes)
@ -221,9 +225,11 @@ final class DependencyTreeManager {
/** /**
* Helper method for adding a single dependency relation. * Helper method for adding a single dependency relation.
* *
* @param nodes The map of tree nodes. * @param nodes The map of tree nodes.
* @param node The node to which the dependency relations are added. * @param node The node to which the dependency relations are
* added.
* @param requiredModule The module required by the current module/node. * @param requiredModule The module required by the current module/node.
*
* @throws DependencyException * @throws DependencyException
*/ */
private void addDependencyRelation(final Map<String, TreeNode> nodes, private void addDependencyRelation(final Map<String, TreeNode> nodes,
@ -301,11 +307,11 @@ final class DependencyTreeManager {
if ((minRequiredVersion == null || minRequiredVersion.isEmpty()) if ((minRequiredVersion == null || minRequiredVersion.isEmpty())
&& (maxRequiredVersion == null || maxRequiredVersion. && (maxRequiredVersion == null || maxRequiredVersion.
isEmpty())) { isEmpty())) {
return true; return true;
} else if ((minRequiredVersion != null && !minRequiredVersion.isEmpty()) } else if ((minRequiredVersion != null && !minRequiredVersion.isEmpty())
&& (maxRequiredVersion == null || maxRequiredVersion && (maxRequiredVersion == null || maxRequiredVersion
.isEmpty())) { .isEmpty())) {
final ComparableVersion minVersion = new ComparableVersion( final ComparableVersion minVersion = new ComparableVersion(
minRequiredVersion); minRequiredVersion);
final ComparableVersion version = new ComparableVersion( final ComparableVersion version = new ComparableVersion(
@ -314,7 +320,7 @@ final class DependencyTreeManager {
return minVersion.compareTo(version) <= 0; return minVersion.compareTo(version) <= 0;
} else if ((minRequiredVersion == null || minRequiredVersion.isEmpty()) } else if ((minRequiredVersion == null || minRequiredVersion.isEmpty())
&& (maxRequiredVersion != null && !maxRequiredVersion && (maxRequiredVersion != null && !maxRequiredVersion
.isEmpty())) { .isEmpty())) {
final ComparableVersion maxVersion = new ComparableVersion( final ComparableVersion maxVersion = new ComparableVersion(
maxRequiredVersion); maxRequiredVersion);
final ComparableVersion version = new ComparableVersion( final ComparableVersion version = new ComparableVersion(