diff --git a/ccm-core/src/main/java/org/libreccm/imexport/DependencyException.java b/ccm-core/src/main/java/org/libreccm/imexport/DependencyException.java new file mode 100644 index 000000000..d2ef26784 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/imexport/DependencyException.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.imexport; + +/** + * + * @author Jens Pelzetter + */ +public class DependencyException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new instance of DependencyException without detail + * message. + */ + public DependencyException() { + super(); + //Nothing more + } + + /** + * Constructs an instance of DependencyException with the + * specified detail message. + * + * @param msg the detail message. + */ + public DependencyException(final String msg) { + super(msg); + } + + /** + * Constructs an instance of DependencyException which wraps + * the specified exception. + * + * @param exception The exception to wrap. + */ + public DependencyException(final Exception exception) { + super(exception); + } + + /** + * Constructs an instance of DependencyException with the + * specified message which also wraps the specified exception. + * + * @param msg The detail message. + * @param exception The exception to wrap. + */ + public DependencyException(final String msg, final Exception exception) { + super(msg, exception); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/imexport/EntityImporter.java b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporter.java similarity index 65% rename from ccm-core/src/main/java/org/libreccm/imexport/EntityImporter.java rename to ccm-core/src/main/java/org/libreccm/imexport/EntityImExporter.java index d1099fa35..7653cf748 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/EntityImporter.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporter.java @@ -18,19 +18,23 @@ */ package org.libreccm.imexport; +import javax.enterprise.context.RequestScoped; import javax.json.JsonObject; /** - * Imports an entity. Implementations must be annotated with {@link Imports} to - * register the implementation in the Import/Export system. - * - * Implementations must be CDI beans with annotated with {@link RequestScoped}. - * + * Interface for importers/exporters. Implementations must be annotated with + * {@link Procsses} to register the implementation in the Import/Export service. + * + * Implementations must also be CDI beans. It is recommended that the beans + * are {@link RequestScoped}. + * * @author Jens Pelzetter - * @param The entity type which can processed by the implementation. + * @param The type of the entity which is processed by the implementation. */ -public interface EntityImporter { - +public interface EntityImExporter { + T importEntity(JsonObject data); - + + JsonObject exportEntity(T entity); + } diff --git a/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java new file mode 100644 index 000000000..23445348c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.imexport; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * + * @author Jens Pelzetter + */ +final class EntityImExporterTreeManager { + + public List generateTree( + final List> imExporters) + throws DependencyException { + + final Map nodes = imExporters + .stream() + .map(EntityImExporterTreeNode::new) + .collect(Collectors + .toMap( + node -> node + .getEntityImExporter() + .getClass() + .getAnnotation(Processes.class) + .type() + .getName(), + node -> node)); + + for (final EntityImExporter imExporter : imExporters) { + addDependencyRelations(imExporter, nodes); + } + + final List nodeList = new ArrayList<>(); + for (final Map.Entry entry + : nodes.entrySet()) { + + nodeList.add(entry.getValue()); + } + + return nodeList; + } + + private void addDependencyRelations( + final EntityImExporter imExporter, + final Map nodes) + throws DependencyException { + + final Processes processes = imExporter + .getClass() + .getAnnotation(Processes.class); + final String className = imExporter.getClass().getName(); + + if (!nodes.containsKey(className)) { + + throw new IllegalArgumentException(String.format( + "The nodes map does not contain a node for " + + "EntityImExporter \"%s\"." + + "This should not happen.", + className)); + } + + final EntityImExporterTreeNode node = nodes.get(className); + + for (final Class clazz : processes.dependsOn()) { + + addDependencyRelation(nodes, node, clazz); + } + } + + private void addDependencyRelation( + final Map nodes, + EntityImExporterTreeNode node, + Class clazz) + throws DependencyException { + + if (!nodes.containsKey(clazz.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())); + } + + final EntityImExporterTreeNode dependencyNode = nodes + .get(clazz.getName()); + + node.addDependsOn(dependencyNode); + dependencyNode.addDependentImExporter(node); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeNode.java b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeNode.java new file mode 100644 index 000000000..cb921689b --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeNode.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.imexport; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * + * @author Jens Pelzetter + */ +final class EntityImExporterTreeNode { + + private EntityImExporter entityImExporter; + + private List dependentImExporters; + + private List dependsOn; + + public EntityImExporterTreeNode() { + + super(); + + dependentImExporters = new ArrayList<>(); + dependsOn = new ArrayList<>(); + } + + public EntityImExporterTreeNode( + final EntityImExporter entityImExporter) { + + this(); + this.entityImExporter = entityImExporter; + } + + public EntityImExporter getEntityImExporter() { + + return entityImExporter; + } + + void setEntityImExporter(final EntityImExporter entityImExporter) { + + this.entityImExporter = entityImExporter; + } + + public List getDependentImExporters() { + return Collections.unmodifiableList(dependentImExporters); + } + + void setDependentImExporters( + final List dependentImExporters) { + + this.dependentImExporters = new ArrayList<>(dependentImExporters); + } + + void addDependentImExporter(final EntityImExporterTreeNode node) { + + dependentImExporters.add(node); + } + + void removeDependentImExporter(final EntityImExporterTreeNode node) { + + dependentImExporters.remove(node); + } + + public List getDependsOn() { + + return Collections.unmodifiableList(dependsOn); + } + + void setDependsOn(final List dependsOn) { + + this.dependsOn = new ArrayList<>(dependsOn); + } + + void addDependsOn(final EntityImExporterTreeNode node) { + + dependsOn.add(node); + } + + void removeDependsOn(final EntityImExporterTreeNode node) { + + dependsOn.remove(node); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 47 + * hash + + Objects.hashCode(this.entityImExporter.getClass().getName()); + return hash; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof EntityImExporterTreeNode)) { + return false; + } + final EntityImExporterTreeNode other = (EntityImExporterTreeNode) obj; + return Objects.equals( + this.entityImExporter.getClass().getName(), + other.getEntityImExporter().getClass().getName()); + } + + + +} diff --git a/ccm-core/src/main/java/org/libreccm/imexport/ExportedEntity.java b/ccm-core/src/main/java/org/libreccm/imexport/ExportedEntity.java deleted file mode 100644 index 71e55b6f0..000000000 --- a/ccm-core/src/main/java/org/libreccm/imexport/ExportedEntity.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2018 LibreCCM Foundation. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301 USA - */ -package org.libreccm.imexport; - -import java.util.Objects; -import java.util.Optional; - -import javax.json.JsonArray; -import javax.json.JsonObject; - -/** - * A transfer object used by {@link Exporter} to wrap the exported object and - * optionally the associations extracted from the object. - * - * @author Jens Pelzetter - */ -public class ExportedEntity { - - private final JsonObject entity; - private final JsonArray associations; - - public ExportedEntity(final JsonObject entity) { - - this.entity = Objects.requireNonNull(entity); - this.associations = null; - } - - public ExportedEntity(final JsonObject entity, - final JsonArray associations) { - - this.entity = Objects.requireNonNull(entity); - this.associations = Objects.requireNonNull(associations); - } - - public JsonObject getEntity() { - return entity; - } - - public Optional getAssociations() { - return Optional.of(associations); - } - -} diff --git a/ccm-core/src/main/java/org/libreccm/imexport/Exports.java b/ccm-core/src/main/java/org/libreccm/imexport/Exports.java deleted file mode 100644 index f26eb27bb..000000000 --- a/ccm-core/src/main/java/org/libreccm/imexport/Exports.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2018 LibreCCM Foundation. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, - * MA 02110-1301 USA - */ -package org.libreccm.imexport; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import javax.inject.Qualifier; - -/** - * Declares which entity type a implementation of {@link Exporter} can process. - * - * @author Jens Pelzetter - */ -@Retention(RetentionPolicy.RUNTIME) -@Qualifier -@Target({ElementType.TYPE, - ElementType.PARAMETER, - ElementType.FIELD, - ElementType.METHOD}) -public @interface Exports { - - Class value(); - -} 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 3bb73655c..816cd5f03 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java @@ -18,15 +18,29 @@ */ package org.libreccm.imexport; - +import org.libreccm.core.UnexpectedErrorException; +import org.libreccm.files.CcmFiles; import org.libreccm.files.CcmFilesConfiguration; +import org.libreccm.files.FileAccessException; +import org.libreccm.files.FileDoesNotExistException; +import org.libreccm.files.InsufficientPermissionsException; +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Any; import javax.enterprise.inject.Instance; import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; /** * Central service for importing and exporting entities. @@ -37,12 +51,11 @@ import javax.inject.Inject; public class ImportExport { @Inject - @Any - private Instance> importers; + private CcmFiles ccmFiles; @Inject @Any - private Instance> exporters; + private Instance> imExporters; /** * Exports the provided entities. The export will be written to a to the @@ -53,36 +66,154 @@ public class ImportExport { * file the provided name will be used. * * - * @param entities The entities to export. + * @param entities The entities to export. * @param exportName The name file to which the export is written. - * @param split Split the entities by package? * * @see CcmFilesConfiguration#dataPath */ public void exportEntities(final List entities, - final String exportName, - final boolean split) { + final String exportName) { throw new UnsupportedOperationException(); } - + /** - * Imports all entities from the files in the {@link imports} directory inside - * the CCM files data directory. The data to import can either be a file with - * the provided name or a directory with the provided name. If it is a directory - * the entry file must also use the provided name. - * + * Imports all entities from the files in the {@link imports} directory + * inside the CCM files data directory. The data to import can either be a + * file with the provided name or a directory with the provided name. If it + * is a directory the entry file must also use the provided name. + * * If an entity which is part of the import already exists in the database * the values from the import are used to update the entity. - * + * * @param importName The name of the import. - * + * * @see CcmFilesConfiguration#dataPath */ public void importEntities(final String importName) { - + + final String importsPath = String.format("imports/%s", importName); + + try { + if (!ccmFiles.isDirectory(importsPath)) { + + throw new IllegalArgumentException(String.format( + "No imports with name \"%s\" available.", + importName)); + } + } catch (FileAccessException + | 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 JsonReader manifestReader = Json + .createReader(manifestInputStream); + final JsonObject manifest = manifestReader.readObject(); + + } catch (IOException + | FileDoesNotExistException + | FileAccessException + | InsufficientPermissionsException ex) { + + } + throw new UnsupportedOperationException(); - + + } + + public List listAvailableImportArchivies() { + + final List importArchivePaths; + try { + importArchivePaths = ccmFiles.listFiles("imports"); + } catch (FileAccessException + | FileDoesNotExistException + | InsufficientPermissionsException ex) { + + throw new UnexpectedErrorException(ex); + } + + return importArchivePaths. + stream() + .filter(this::isImportArchive) + .map(this::createImportManifest) + .collect(Collectors.toList()); + } + + private boolean isImportArchive(final String path) { + + final String manifestPath = String.format("imports/%s/ccm-export.json", + path); + + final boolean result; + try { + result = ccmFiles.existsFile(manifestPath); + } catch (FileAccessException | InsufficientPermissionsException ex) { + + throw new UnexpectedErrorException(ex); + } + + return result; + } + + private ImportManifest createImportManifest(final String path) { + + final String manifestPath = String.format("imports/%s/ccm-export.json", + path); + + final InputStream inputStream; + try { + inputStream = ccmFiles.createInputStream(manifestPath); + } catch (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/imexport/EntityExporter.java b/ccm-core/src/main/java/org/libreccm/imexport/ImportManifest.java similarity index 56% rename from ccm-core/src/main/java/org/libreccm/imexport/EntityExporter.java rename to ccm-core/src/main/java/org/libreccm/imexport/ImportManifest.java index a4e31e6cd..140e551f1 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/EntityExporter.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/ImportManifest.java @@ -18,27 +18,39 @@ */ package org.libreccm.imexport; -import javax.enterprise.context.RequestScoped; +import java.util.Collections; +import java.util.Date; +import java.util.List; /** - * Interface for exporters. Implementation must be annotated with - * {@link Exports} to register the implementation in the Import/Export system. - * - * Implementations must be CDI beans with annotated with {@link RequestScoped}. * * @author Jens Pelzetter - * @param The type of the entity which is processed by the implementation. */ -public interface EntityExporter { +public class ImportManifest { - /** - * Converts the provided entity to a JSON object and an optional array of - * associations. - * - * @param entity The entity to export. - * - * @return The JSON representation of the entity. - */ - ExportedEntity exportEntity(T entity); + private final Date created; + private final String onServer; + private final List types; + + public ImportManifest(final Date created, + final String onServer, + final List types) { + + this.created = created; + this.onServer = onServer; + this.types = types; + } + + public Date getCreated() { + return new Date(created.getTime()); + } + + public String getOnServer() { + return onServer; + } + + public List getTypes() { + return Collections.unmodifiableList(types); + } } diff --git a/ccm-core/src/main/java/org/libreccm/imexport/Imports.java b/ccm-core/src/main/java/org/libreccm/imexport/Processes.java similarity index 80% rename from ccm-core/src/main/java/org/libreccm/imexport/Imports.java rename to ccm-core/src/main/java/org/libreccm/imexport/Processes.java index 31a30778f..d0dcb5c79 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/Imports.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/Processes.java @@ -23,22 +23,26 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import javax.enterprise.util.Nonbinding; import javax.inject.Qualifier; /** - * Declares which entity types an implementation of {@link Importer} can - * process. + * Declares which entity types an implementation of {@link Importer} and/or + * {@link Exporter} can process. * * @author Jens Pelzetter */ @Retention(RetentionPolicy.RUNTIME) @Qualifier @Target({ElementType.TYPE, - ElementType.PARAMETER, - ElementType.FIELD, - ElementType.METHOD}) -public @interface Imports { + ElementType.PARAMETER, + ElementType.FIELD, + ElementType.METHOD}) +public @interface Processes { - Class value(); + Class type(); + + @Nonbinding + Class[] dependsOn(); } diff --git a/ccm-core/src/main/java/org/libreccm/modules/TreeNode.java b/ccm-core/src/main/java/org/libreccm/modules/TreeNode.java index 7b7bcc1db..e3feae1fc 100644 --- a/ccm-core/src/main/java/org/libreccm/modules/TreeNode.java +++ b/ccm-core/src/main/java/org/libreccm/modules/TreeNode.java @@ -86,7 +86,7 @@ final class TreeNode { } void setDependentModules(final List dependentModules) { - this.dependentModules = dependentModules; + this.dependentModules = new ArrayList<>(dependentModules); } void addDependentModule(final TreeNode node) { @@ -102,7 +102,7 @@ final class TreeNode { } void setDependsOn(final List dependsOn) { - this.dependsOn = dependsOn; + this.dependsOn = new ArrayList<>(dependsOn); } void addDependsOn(final TreeNode node) {