UI for Import/Export of entities

Former-commit-id: 8bddae0711
pull/7/head
Jens Pelzetter 2020-12-09 20:19:39 +01:00
parent e1137ae2e9
commit e6e6919b92
13 changed files with 291 additions and 128 deletions

View File

@ -402,8 +402,9 @@ public class ImportExport {
private ImportManifest createImportManifest(final String path) { private ImportManifest createImportManifest(final String path) {
final String manifestPath = String.format("imports/%s/ccm-export.json", final String manifestPath = String.format(
path); "imports/%s/ccm-export.json", path
);
try (final InputStream inputStream = ccmFiles try (final InputStream inputStream = ccmFiles
.createInputStream(manifestPath)) { .createInputStream(manifestPath)) {
@ -412,24 +413,33 @@ public class ImportExport {
final JsonObject manifestJson = reader.readObject(); final JsonObject manifestJson = reader.readObject();
if (!manifestJson.containsKey("created")) { if (!manifestJson.containsKey("created")) {
throw new IllegalArgumentException(String.format( throw new IllegalArgumentException(
String.format(
"The manifest file \"%s\" is malformed. " "The manifest file \"%s\" is malformed. "
+ "Key \"created\" is missing.", + "Key \"created\" is missing.",
manifestPath)); manifestPath
)
);
} }
if (!manifestJson.containsKey("onServer")) { if (!manifestJson.containsKey("onServer")) {
throw new IllegalArgumentException(String.format( throw new IllegalArgumentException(
String.format(
"The manifest file \"%s\" is malformed. " "The manifest file \"%s\" is malformed. "
+ "Key \"onServer\" is missing.", + "Key \"onServer\" is missing.",
manifestPath)); manifestPath
)
);
} }
if (!manifestJson.containsKey("types")) { if (!manifestJson.containsKey("types")) {
throw new IllegalArgumentException(String.format( throw new IllegalArgumentException(
String.format(
"The manifest file \"%s\" is malformed. " "The manifest file \"%s\" is malformed. "
+ "Key \"types\" is missing.", + "Key \"types\" is missing.",
manifestPath)); manifestPath
)
);
} }
final LocalDateTime created = LocalDateTime final LocalDateTime created = LocalDateTime
@ -446,9 +456,11 @@ public class ImportExport {
} }
return new ImportManifest( return new ImportManifest(
path,
Date.from(created.atZone(ZoneId.of("UTC")).toInstant()), Date.from(created.atZone(ZoneId.of("UTC")).toInstant()),
onServer, onServer,
types); types
);
} catch (IOException } catch (IOException
| FileAccessException | FileAccessException
| FileDoesNotExistException | FileDoesNotExistException

View File

@ -29,19 +29,30 @@ import java.util.List;
*/ */
public class ImportManifest { public class ImportManifest {
private final String importName;
private final Date created; private final Date created;
private final String onServer; private final String onServer;
private final List<String> types; private final List<String> types;
public ImportManifest(final Date created, public ImportManifest(
final String importName,
final Date created,
final String onServer, final String onServer,
final List<String> types) { final List<String> types
) {
this.importName = importName;
this.created = created; this.created = created;
this.onServer = onServer; this.onServer = onServer;
this.types = types; this.types = types;
} }
public String getImportName() {
return importName;
}
public Date getCreated() { public Date getCreated() {
return new Date(created.getTime()); return new Date(created.getTime());
} }

View File

@ -23,9 +23,12 @@ import org.libreccm.imexport.AbstractEntityImExporter;
import org.libreccm.imexport.EntityImExporterTreeNode; import org.libreccm.imexport.EntityImExporterTreeNode;
import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Exportable;
import org.libreccm.imexport.ImportExport; import org.libreccm.imexport.ImportExport;
import org.libreccm.imexport.ImportManifest;
import org.libreccm.security.AuthorizationRequired; import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege; import org.libreccm.security.RequiresPrivilege;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -129,15 +132,6 @@ public class ImExportController {
taskManager.exportEntities(exportTypes, exportName); taskManager.exportEntities(exportTypes, exportName);
// models.put(
// "exportEntities",
// exportNodes
// .stream()
// .map(node -> node.getEntityImExporter().getEntityClass().getName())
// .sorted()
// .collect(Collectors.joining("\n"))
// );
// return "org/libreccm/ui/admin/imexport/exporting.xhtml";
return "redirect:imexport"; return "redirect:imexport";
} }
@ -146,7 +140,33 @@ public class ImExportController {
@AuthorizationRequired @AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String importEntities() { public String importEntities() {
throw new NotFoundException(); models.put(
"importArchives",
importExport
.listAvailableImportArchivies()
.stream()
.map(this::buildImportOption)
.sorted()
.collect(
Collectors.toMap(
ImportOption::getImportName,
ImportOption::getLabel
)
)
);
return "org/libreccm/ui/admin/imexport/import.xhtml";
}
@POST
@Path("/import")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String importEntities(
@FormParam("archive") final String importArchive
) {
taskManager.importEntities(importArchive);
return "redirect:imexport";
} }
private String noDuplicateKeys(final String str1, final String str2) { private String noDuplicateKeys(final String str1, final String str2) {
@ -176,4 +196,19 @@ public class ImExportController {
} }
} }
private ImportOption buildImportOption(final ImportManifest manifest) {
return new ImportOption(
manifest.getImportName(),
String.format(
"%s from server %s created on %s with types %s",
manifest.getImportName(),
manifest.getOnServer(),
DateTimeFormatter.ISO_DATE_TIME.withZone(
ZoneOffset.systemDefault()
).format(manifest.getCreated().toInstant()),
manifest.getTypes().stream().collect(Collectors.joining(", "))
)
);
}
} }

View File

@ -22,14 +22,7 @@ import org.libreccm.imexport.Exportable;
import org.libreccm.imexport.ImportExport; import org.libreccm.imexport.ImportExport;
import java.util.Collection; import java.util.Collection;
import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Singleton;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.ObservesAsync; import javax.enterprise.event.ObservesAsync;
import javax.inject.Inject; import javax.inject.Inject;
@ -45,22 +38,6 @@ public class ImExportTasks {
@Inject @Inject
private ImportExport importExport; private ImportExport importExport;
// @Asynchronous
// @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
// public Future<?> startExport(
// final Collection<Exportable> entities,
// final String exportName
// ) {
// importExport.exportEntities(entities, exportName);
// return new AsyncResult<>(null);
// }
//
// @Asynchronous
// @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
// public Future<?> startImport(final String importName) {
// importExport.importEntities(importName);
// return new AsyncResult<>(null);
// }
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public ExportTask exportEntities(@ObservesAsync final ExportTask task) { public ExportTask exportEntities(@ObservesAsync final ExportTask task) {
@ -73,7 +50,7 @@ public class ImExportTasks {
} }
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public void importEntitites(@ObservesAsync final ImExportTaskStatus task) { public void importEntitites(@ObservesAsync final ImportTask task) {
final String importName = task.getName(); final String importName = task.getName();
importExport.importEntities(importName); importExport.importEntities(importName);

View File

@ -63,12 +63,6 @@ public class ImportExportTaskManager {
@Inject @Inject
private Event<ImportTask> importTaskSender; private Event<ImportTask> importTaskSender;
// @Inject
// private ImExportTasks imExportTasks;
// private SortedSet<ImExportTaskStatus> exportTasks;
//
// private SortedSet<ImExportTaskStatus> importTasks;
private final SortedSet<ExportTaskStatus> exportTasks; private final SortedSet<ExportTaskStatus> exportTasks;
private final SortedSet<ImportTaskStatus> importTasks; private final SortedSet<ImportTaskStatus> importTasks;
@ -111,9 +105,6 @@ public class ImportExportTaskManager {
final ExportTaskStatus taskStatus = new ExportTaskStatus(); final ExportTaskStatus taskStatus = new ExportTaskStatus();
taskStatus.setName(exportName); taskStatus.setName(exportName);
taskStatus.setStarted(LocalDateTime.now()); taskStatus.setStarted(LocalDateTime.now());
// final Future<?> status = imExportTasks.startExport(
// entities, exportName
// );
exportTaskSender.fireAsync( exportTaskSender.fireAsync(
new ExportTask(exportName, LocalDate.now(), entities, taskStatus) new ExportTask(exportName, LocalDate.now(), entities, taskStatus)
).handle((task , ex) -> handleExportTaskResult(task, ex, taskStatus)); ).handle((task , ex) -> handleExportTaskResult(task, ex, taskStatus));
@ -122,24 +113,15 @@ public class ImportExportTaskManager {
exportTasks.add(taskStatus); exportTasks.add(taskStatus);
} }
// public void exportEntities(
// final Collection<Exportable> entities, final String exportName
// ) {
// final ImExportTaskStatus task = new ImExportTaskStatus();
// task.setName(exportName);
// task.setStarted(LocalDate.now());
// final Future<?> status = startExport(entities, exportName);
// task.setStatus(status);
// exportTasks.add(task);
// }
public void importEntities(final String importName) { public void importEntities(final String importName) {
// final ImExportTaskStatus task = new ImExportTaskStatus(); final ImportTaskStatus taskStatus = new ImportTaskStatus();
// task.setName(importName); taskStatus.setStarted(LocalDateTime.now());
// task.setStarted(LocalDateTime.now()); importTaskSender.fireAsync(
// final Future<?> status = imExportTasks.startImport(importName); new ImportTask(importName, LocalDate.now(), taskStatus)
// task.setStatus(status); ).handle((task, ex) -> handleImportTaskResult(task, ex, taskStatus));
// importTasks.add(task);
throw new UnsupportedOperationException(); taskStatus.setStatus(ImExportTaskStatusEnum.RUNNING);
importTasks.add(taskStatus);
} }
@Schedule(hour = "*", minute = "*/5", persistent = false) @Schedule(hour = "*", minute = "*/5", persistent = false)
@ -180,4 +162,17 @@ public class ImportExportTaskManager {
return task; return task;
} }
private Object handleImportTaskResult(
final ImportTask task, final Throwable ex, final ImportTaskStatus status
) {
if (ex == null) {
status.setStatus(ImExportTaskStatusEnum.FINISHED);
} else {
status.setStatus(ImExportTaskStatusEnum.ERROR);
status.setException(ex);
LOGGER.error("Import Task {} failed", task);
LOGGER.error("with exception: ", ex);
}
return task;
}
} }

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2020 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.ui.admin.imexport;
import java.util.Objects;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class ImportOption implements Comparable<ImportOption> {
private final String importName;
private final String label;
public ImportOption(
final String importName,
final String label
) {
this.importName = importName;
this.label = label;
}
public String getImportName() {
return importName;
}
public String getLabel() {
return label;
}
@Override
public int compareTo(final ImportOption other) {
return importName.compareTo(
Objects.requireNonNull(other).getImportName()
);
}
}

View File

@ -30,9 +30,16 @@ public class ImportTask {
private final LocalDate started; private final LocalDate started;
public ImportTask(final String name, final LocalDate started) { private final ImportTaskStatus status;
public ImportTask(
final String name,
final LocalDate started,
final ImportTaskStatus status
) {
this.name = name; this.name = name;
this.started = started; this.started = started;
this.status = status;
} }
public String getName() { public String getName() {
@ -43,4 +50,8 @@ public class ImportTask {
return started; return started;
} }
public ImportTaskStatus getStatus() {
return status;
}
} }

View File

@ -23,7 +23,6 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Comparator; import java.util.Comparator;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletionStage;
/** /**
* *
@ -35,12 +34,15 @@ public class ImportTaskStatus implements Comparable<ImportTaskStatus> {
private LocalDateTime started; private LocalDateTime started;
private CompletionStage<ImportTask> status; private ImExportTaskStatusEnum status;
private Throwable exception;
public String getName() { public String getName() {
return name; return name;
} }
protected void setName(final String name) { protected void setName(final String name) {
this.name = name; this.name = name;
} }
@ -53,14 +55,56 @@ public class ImportTaskStatus implements Comparable<ImportTaskStatus> {
this.started = started; this.started = started;
} }
public CompletionStage<ImportTask> getStatus() { public ImExportTaskStatusEnum getStatus() {
return status; return status;
} }
protected void setStatus(final CompletionStage<ImportTask> status) { protected void setStatus(final ImExportTaskStatusEnum status) {
this.status = status; this.status = status;
} }
public Throwable getException() {
return exception;
}
protected void setException(final Throwable exception) {
this.exception = exception;
}
@Override
public int hashCode() {
int hash = 7;
hash = 53 * hash + Objects.hashCode(name);
hash = 53 * hash + Objects.hashCode(started);
hash = 53 * hash + Objects.hashCode(status);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof ImExportTaskStatus)) {
return false;
}
final ImportTaskStatus other = (ImportTaskStatus) obj;
if (!other.canEqual(this)) {
return false;
}
if (!Objects.equals(name, other.getName())) {
return false;
}
if (!Objects.equals(started, other.getStarted())) {
return false;
}
return status == other.getStatus();
}
public boolean canEqual(final Object obj) { public boolean canEqual(final Object obj) {
return obj instanceof ImExportTaskStatus; return obj instanceof ImExportTaskStatus;
} }

View File

@ -3,11 +3,6 @@
xmlns:cc="http://xmlns.jcp.org/jsf/composite" xmlns:cc="http://xmlns.jcp.org/jsf/composite"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"> xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<cc:interface shortDescription="Component for a single checkbox"> <cc:interface shortDescription="Component for a single checkbox">
<cc:attribute default="false"
name="checked"
shortDescription="Is the checkbox selected?"
required="false"
type="boolean" />
<cc:attribute name="help" <cc:attribute name="help"
required="true" required="true"
shortDescription="A short description of the input field" shortDescription="A short description of the input field"

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:bootstrap="http://xmlns.jcp.org/jsf/composite/components/bootstrap"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:libreccm="http://xmlns.jcp.org/jsf/composite/components/libreccm"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<ui:composition template="/WEB-INF/views/org/libreccm/ui/admin/ccm-admin.xhtml">
<ui:param name="activePage" value="imexport" />
<ui:param name="title" value="#{AdminMessages['imexport.label']}" />
<ui:define name="breadcrumb">
<li class="breadcrumb-item">
<a href="#{mvc.uri('ImExportController#getImExportDashboard')}">
#{AdminMessages['imexport.label']}
</a>
</li>
<li class="breadcrumb-item active">
#{AdminMessages['imexport.export.label']}
</li>
</ui:define>
<ui:define name="main">
<div class="container">
<h1>#{AdminMessages['imexport.export.label']}</h1>
<pre>#{exportEntities}</pre>
</div>
</ui:define>
</ui:composition>
</html>

View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:bootstrap="http://xmlns.jcp.org/jsf/composite/components/bootstrap"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:libreccm="http://xmlns.jcp.org/jsf/composite/components/libreccm"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<ui:composition template="/WEB-INF/views/org/libreccm/ui/admin/ccm-admin.xhtml">
<ui:param name="activePage" value="imexport" />
<ui:param name="title" value="#{AdminMessages['imexport.label']}" />
<ui:define name="breadcrumb">
<li class="breadcrumb-item">
<a href="#{mvc.uri('ImExportController#getImExportDashboard')}">
#{AdminMessages['imexport.label']}
</a>
</li>
<li class="breadcrumb-item active">
#{AdminMessages['imexport.import.label']}
</li>
</ui:define>
<ui:define name="main">
<div class="container">
<h1>#{AdminMessages['imexport.import.label']}</h1>
<form action="#{mvc.uri('ImExportController#importEntities')}"
aria-described="import-help"
method="post">
<p id="import-help">#{AdminMessages['imexport.import.help']}</p>
<bootstrap:formGroupRadio help="#{AdminMessages['imexport.import.archives.help']}"
inputId="importarchives"
label="#{AdminMessages['imexport.import.archives.label']}"
name="archive"
options="#{importArchives}" />
<a class="btn btn-warning"
href="#{mvc.uri('ImExportController#getImExportDashboard')}">
#{AdminMessages['imexport.import.cancel']}
</a>
<button class="btn btn-success" type="submit">
#{AdminMessages['imexport.import.submit']}
</button>
</form>
</div>
</ui:define>
</ui:composition>
</html>

View File

@ -556,3 +556,9 @@ imexport.activeimports.table.columns.actions.heading=Actions
imexport.activeimports.table.columns.status.finished=Finished imexport.activeimports.table.columns.status.finished=Finished
imexport.activeimports.table.columns.status.running=In progress imexport.activeimports.table.columns.status.running=In progress
imexport.activeimports.table.columns.actions.button_label=Cancel imexport.activeimports.table.columns.actions.button_label=Cancel
imexport.import.label=Import
imexport.import.help=Select one of the available import archives
imexport.import.archives.help=Select the archive to import
imexport.import.archives.label=Available import archives
imexport.import.cancel=Cancel
imexport.import.submit=Start Import

View File

@ -556,3 +556,9 @@ imexport.activeimports.table.columns.actions.heading=Aktionen
imexport.activeimports.table.columns.status.finished=Abgeschlossen imexport.activeimports.table.columns.status.finished=Abgeschlossen
imexport.activeimports.table.columns.status.running=In Arbeit imexport.activeimports.table.columns.status.running=In Arbeit
imexport.activeimports.table.columns.actions.button_label=Abbrechen imexport.activeimports.table.columns.actions.button_label=Abbrechen
imexport.import.label=Import
imexport.import.help=W\u00e4hlen Sie das zu importierende Archiv
imexport.import.archives.help=W\u00e4hlen Sie das zu importierende Archiv
imexport.import.archives.label=Verf\u00fcgbare Import-Archive
imexport.import.cancel=Abbrechen
imexport.import.submit=Import starten