Aktueller Stand ImExport UI

Jens Pelzetter 2020-12-05 19:36:39 +01:00
parent beef038b62
commit 0acc7f17b4
14 changed files with 644 additions and 21 deletions

View File

@ -85,7 +85,6 @@ public abstract class AbstractEntityImExporter<T extends Exportable> {
@Transactional(Transactional.TxType.REQUIRED)
public String exportEntity(final Exportable entity) throws ExportException {
try {
return objectMapper.writeValueAsString(entity);
} catch (JsonProcessingException ex) {

View File

@ -105,10 +105,10 @@ public final class EntityImExporterTreeNode {
@Override
public int hashCode() {
int hash = 7;
hash = 47
* hash
+ Objects
.hashCode(this.entityImExporter.getClass().getName());
hash = 47 * hash
+ Objects.hashCode(
this.entityImExporter.getClass().getName()
);
return hash;
}

View File

@ -38,6 +38,7 @@ import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@ -59,6 +60,7 @@ import javax.json.JsonObjectBuilder;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonWriter;
import javax.transaction.Transactional;
/**
* Central service for importing and exporting entities.
@ -106,8 +108,9 @@ public class ImportExport {
*
* @see CcmFilesConfiguration#dataPath
*/
@Transactional(Transactional.TxType.REQUIRED)
public void exportEntities(
final List<Exportable> entities, final String exportName
final Collection<Exportable> entities, final String exportName
) {
final JsonObjectBuilder manifestBuilder = Json.createObjectBuilder();
manifestBuilder.add("created",
@ -162,7 +165,6 @@ public class ImportExport {
for (final Map.Entry<String, List<Exportable>> entry
: typeEntityMap.entrySet()) {
createExportedEntities(exportName,
entry.getKey(),
entry.getValue());
@ -182,8 +184,8 @@ public class ImportExport {
*
* @see CcmFilesConfiguration#dataPath
*/
@Transactional(Transactional.TxType.REQUIRED)
public void importEntities(final String importName) {
final String importsPath = String.format("imports/%s", importName);
try {

View File

@ -19,6 +19,7 @@
package org.libreccm.security;
import org.libreccm.imexport.AbstractEntityImExporter;
import org.libreccm.imexport.ExportException;
import org.libreccm.imexport.Exportable;
import org.libreccm.imexport.Processes;
@ -55,4 +56,6 @@ public class RoleImExporter extends AbstractEntityImExporter<Role> {
return Collections.emptySet();
}
}

View File

@ -18,23 +18,28 @@
*/
package org.libreccm.ui.admin.imexport;
import com.arsdigita.ui.admin.importexport.ImportExportMonitor;
import org.libreccm.core.CoreConstants;
import org.libreccm.imexport.AbstractEntityImExporter;
import org.libreccm.imexport.EntityImExporterTreeNode;
import org.libreccm.imexport.Exportable;
import org.libreccm.imexport.ImportExport;
import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
/**
@ -50,7 +55,7 @@ public class ImExportController {
private ImportExport importExport;
@Inject
private ImportExportMonitor importExportMonitor;
private ImportExportTaskManager taskManager;
@Inject
private Models models;
@ -77,12 +82,65 @@ public class ImExportController {
.map(AbstractEntityImExporter::getEntityClass)
.map(Class::getName)
.sorted()
.collect(Collectors.toList())
.collect(
Collectors.toMap(
clazz -> clazz,
clazz -> clazz,
this::noDuplicateKeys,
TreeMap::new
)
)
//.collect(Collectors.toList())
);
return "org/libreccm/ui/admin/imexport/export.xhtml";
}
@POST
@Path("/export")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
public String exportEntities(
@FormParam("selectedEntities") final String[] selectedEntitiesParam,
@FormParam("exportName") final String exportName
) {
final Set<String> selectedEntities = Arrays
.stream(selectedEntitiesParam)
.collect(Collectors.toSet());
final Set<EntityImExporterTreeNode> selectedNodes = importExport
.getExportableEntityTypes()
.stream()
.filter(
node -> selectedEntities.contains(
node.getEntityImExporter().getEntityClass().getName()
)
)
.collect(Collectors.toSet());
final Set<EntityImExporterTreeNode> exportNodes = addRequiredEntities(
new HashSet<>(selectedNodes)
);
final Set<Class<? extends Exportable>> exportTypes = exportNodes
.stream()
.map(node -> node.getEntityImExporter().getEntityClass())
.collect(Collectors.toSet());
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";
}
@GET
@Path("/import")
@AuthorizationRequired
@ -91,4 +149,31 @@ public class ImExportController {
throw new NotFoundException();
}
private String noDuplicateKeys(final String str1, final String str2) {
throw new RuntimeException("No duplicate keys allowed.");
}
private Set<EntityImExporterTreeNode> addRequiredEntities(
final Set<EntityImExporterTreeNode> selectedNodes
) {
boolean foundRequiredNodes = false;
final Set<EntityImExporterTreeNode> exportNodes = new HashSet<>(
selectedNodes
);
for (final EntityImExporterTreeNode node : selectedNodes) {
if (node.getDependsOn() != null
&& !node.getDependsOn().isEmpty()
&& !exportNodes.containsAll(node.getDependsOn())) {
exportNodes.addAll(node.getDependsOn());
foundRequiredNodes = true;
}
}
if (foundRequiredNodes) {
return addRequiredEntities(exportNodes);
} else {
return exportNodes;
}
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.Objects;
import java.util.concurrent.Future;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class ImExportTask implements Comparable<ImExportTask> {
private String name;
private LocalDateTime started;
private Future<?> status;
public String getName() {
return name;
}
protected void setName(final String name) {
this.name = name;
}
public LocalDateTime getStarted() {
return started;
}
public String getStartedAsIso() {
return DateTimeFormatter.ISO_DATE_TIME.withZone(
ZoneId.systemDefault()).format(started
);
}
protected void setStarted(final LocalDateTime started) {
this.started = started;
}
public Future<?> getStatus() {
return status;
}
protected void setStatus(final Future<?> status) {
this.status = status;
}
public boolean isDone() {
return status.isDone();
}
protected void cancel() {
status.cancel(true);
}
@Override
public int hashCode() {
int hash = 7;
hash = 23 * hash + Objects.hashCode(this.name);
hash = 23 * hash + Objects.hashCode(this.started);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof ImExportTask)) {
return false;
}
final ImExportTask other = (ImExportTask) obj;
if (!other.canEqual(this)) {
return false;
}
if (!Objects.equals(this.name, other.getName())) {
return false;
}
return Objects.equals(this.started, other.getStarted());
}
public boolean canEqual(final Object obj) {
return obj instanceof ImExportTask;
}
@Override
public int compareTo(final ImExportTask other) {
return Comparator
.nullsFirst(Comparator
.comparing(ImExportTask::getName)
.thenComparing(ImExportTask::getStarted)
)
.compare(this, other);
}
@Override
public String toString() {
return String.format(
"%s{ "
+ "name = %s, "
+ "started = %s, "
+ "status = %s"
+ " }",
super.toString(),
name,
DateTimeFormatter.ISO_DATE_TIME.withZone(
ZoneId.systemDefault()
).format(started),
Objects.toString(status)
);
}
}

View File

@ -0,0 +1,64 @@
/*
* 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 org.libreccm.imexport.Exportable;
import org.libreccm.imexport.ImportExport;
import java.util.Collection;
import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.inject.Inject;
import javax.transaction.Transactional;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@Stateless
public class ImExportTasks {
@Inject
private ImportExport importExport;
@Asynchronous
@Transactional(Transactional.TxType.REQUIRES_NEW)
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public Future<?> startExport(
final Collection<Exportable> entities,
final String exportName
) {
importExport.exportEntities(entities, exportName);
return new AsyncResult<>(null);
}
@Asynchronous
@Transactional(Transactional.TxType.REQUIRES_NEW)
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public Future<?> startImport(final String importName) {
importExport.importEntities(importName);
return new AsyncResult<>(null);
}
}

View File

@ -0,0 +1,152 @@
/*
* 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 org.libreccm.imexport.Exportable;
import org.libreccm.imexport.ImportExport;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Future;
import javax.ejb.AsyncResult;
import javax.ejb.Asynchronous;
import javax.ejb.Schedule;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@ApplicationScoped
@Named("ImportExportTaskManager")
public class ImportExportTaskManager {
@Inject
private EntityManager entityManager;
@Inject
private ImExportTasks imExportTasks;
private SortedSet<ImExportTask> exportTasks;
private SortedSet<ImExportTask> importTasks;
public ImportExportTaskManager() {
exportTasks = new TreeSet<>(
Comparator.comparing(
ImExportTask::getStarted)
.thenComparing(ImExportTask::getName)
);
importTasks = new TreeSet<>(
Comparator.comparing(
ImExportTask::getStarted)
.thenComparing(ImExportTask::getName)
);
}
public SortedSet<ImExportTask> getExportTasks() {
return Collections.unmodifiableSortedSet(exportTasks);
}
public SortedSet<ImExportTask> getImportTasks() {
return Collections.unmodifiableSortedSet(importTasks);
}
@Transactional(Transactional.TxType.REQUIRED)
public void exportEntities(
final Set<Class<? extends Exportable>> exportTypes,
final String exportName
) {
final Set<Exportable> entities = new HashSet<>();
for (final Class<? extends Exportable> type : exportTypes) {
@SuppressWarnings("unchecked")
final Set<? extends Exportable> entitiesOfType = collectEntities(
(Class<Exportable>) type
);
entities.addAll(entitiesOfType);
}
final ImExportTask task = new ImExportTask();
task.setName(exportName);
task.setStarted(LocalDateTime.now());
final Future<?> status = imExportTasks.startExport(
entities, exportName
);
task.setStatus(status);
exportTasks.add(task);
}
// public void exportEntities(
// final Collection<Exportable> entities, final String exportName
// ) {
// final ImExportTask task = new ImExportTask();
// 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) {
final ImExportTask task = new ImExportTask();
task.setName(importName);
task.setStarted(LocalDateTime.now());
final Future<?> status = imExportTasks.startImport(importName);
task.setStatus(status);
importTasks.add(task);
}
@Schedule(hour = "*", minute = "*/5", persistent = false)
protected void removeFinishedTasks() {
exportTasks.removeIf(ImExportTask::isDone);
importTasks.removeIf(ImExportTask::isDone);
}
public void cancelTask(final ImExportTask task) {
task.cancel();
}
private Set<? extends Exportable> collectEntities(
final Class<Exportable> ofType
) {
final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Exportable> query = builder.createQuery(ofType);
final Root<Exportable> from = query.from(ofType);
return new HashSet<>(
entityManager.createQuery(
query.select(from)
).getResultList()
);
}
}

View File

@ -36,7 +36,8 @@
class="form-check-input"
id="#{cc.attrs.inputId}"
name="#{cc.attrs.name}"
type="checkbox" />
type="checkbox"
value="#{value}" />
<label for="#{cc.attrs.inputId}">#{cc.attrs.label}</label>
</div>
</cc:implementation>

View File

@ -26,23 +26,31 @@
<div class="container">
<h1>#{AdminMessages['imexport.export.label']}</h1>
<form action="#"
aria-describedby="export-help">
<form action="#{mvc.uri('ImExportController#exportEntities')}"
aria-describedby="export-help"
method="post">
<p id="export-help">#{AdminMessages['imexport.export.help']}</p>
<c:forEach items="#{exportableEntities}" var="entity">
<!-- <c:forEach items="#{exportableEntities}" var="entity">
<bootstrap:formCheck
inputId="#{entity.replace('.', '-')}"
label="#{entity}"
name="#{entity.replace('.', '-')}"
name="selectedEntities"
value="#{entity}" />
</c:forEach>
</c:forEach>-->
<bootstrap:formGroupChecks options="#{exportableEntities}"
help="#{AdminMessages['imexport.export.exportentities.help']}"
inputId="exportentity"
label="#{AdminMessages['imexport.export.exportentities.label']}"
name="selectedEntities" />
<bootstrap:formGroupText
help="#{AdminMessages['imexport.export.export_name.help']}"
inputId="exportName"
label="#{AdminMessages['imexport.export.export_name.label']}"
name="exportName" />
name="exportName"
required="treu" />
<a class="btn btn-warning"
href="#{mvc.uri('ImExportController#getImExportDashboard')}">

View File

@ -0,0 +1,36 @@
<!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

@ -67,6 +67,100 @@
</div>
</div>
</div>
<div>
<h2>#{AdminMessages['imexport.activeexports.heading']}</h2>
<c:choose>
<c:when test="#{ImportExportTaskManager.exportTasks.isEmpty()}">
<p>
#{AdminMessages['imexport.activeexports.none']}
</p>
</c:when>
<c:otherwise>
<table class="table table-hover">
<thead>
<tr>
<th>#{AdminMessages['imexport.activeexports.table.columns.name.heading']}</th>
<th>#{AdminMessages['imexport.activeexports.table.columns.started.heading']}</th>
<th>#{AdminMessages['imexport.activeexports.table.columns.status.heading']}</th>
<th>#{AdminMessages['imexport.activeexports.table.columns.actions.heading']}</th>
</tr>
</thead>
<tbody>
<c:forEach items="#{ImportExportTaskManager.exportTasks}" var="task">
<tr>
<td>#{task.name}</td>
<td>#{task.started}</td>
<td>
<c:choose>
<c:when test="#{task.done}">
#{AdminMessages['imexport.activeexports.table.columns.status.finished']}
</c:when>
<c:otherwise>
#{AdminMessages['imexport.activeexports.table.columns.status.running']}
</c:otherwise>
</c:choose>
</td>
<td>
<a class="btn btn-warning" href="#">
#{AdminMessages['imexport.activeexports.table.columns.actions.button_label']}
</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</c:otherwise>
</c:choose>
</div>
<div>
<h2>#{AdminMessages['imexport.activeimports.heading']}</h2>
<c:choose>
<c:when test="#{ImportExportTaskManager.importTasks.isEmpty()}">
<p>
#{AdminMessages['imexport.activeimports.none']}
</p>
</c:when>
<c:otherwise>
<table class="table table-hover">
<thead>
<tr>
<th>#{AdminMessages['imexport.activeimports.table.columns.name.heading']}</th>
<th>#{AdminMessages['imexport.activeimports.table.columns.started.heading']}</th>
<th>#{AdminMessages['imexport.activeimports.table.columns.status.heading']}</th>
<th>#{AdminMessages['imexport.activeimports.table.columns.actions.heading']}</th>
</tr>
</thead>
<tbody>
<c:forEach items="#{ImportExportTaskManager.importTasks}" var="task">
<tr>
<td>#{task.name}</td>
<td>#{task.started}</td>
<td>
<c:choose>
<c:when test="#{task.done}">
#{AdminMessages['imexport.activeimports.table.columns.status.finished']}
</c:when>
<c:otherwise>
#{AdminMessages['imexport.activeimports.table.columns.status.running']}
</c:otherwise>
</c:choose>
</td>
<td>
<a class="btn btn-warning" href="#">
#{AdminMessages['imexport.activeimports.table.columns.actions.button_label']}
</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</c:otherwise>
</c:choose>
</div>
</div>
</ui:define>

View File

@ -536,3 +536,23 @@ imexport.export.export_name.help=Name of the export archive
imexport.export.export_name.label=to
imexport.export.submit=Export
imexport.export.cancel=Cancel
imexport.export.exportentities.help=Select the entity types to export. Additional required types will be selected automatically
imexport.export.exportentities.label=Export types
imexport.activeexports.heading=Active Exports
imexport.activeexports.table.columns.name.heading=Name
imexport.activeexports.table.columns.started.heading=Started
imexport.activeexports.table.columns.status.heading=Status
imexport.activeexports.table.columns.actions.heading=Actions
imexport.activeexports.table.columns.status.finished=Finished
imexport.activeexports.table.columns.status.running=In progress
imexport.activeexports.table.columns.actions.button_label=Cancel
imexport.activeexports.none=No active exports
imexport.activeimports.heading=Active Imports
imexport.activeimports.none=No active imports
imexport.activeimports.table.columns.name.heading=Name
imexport.activeimports.table.columns.started.heading=Started
imexport.activeimports.table.columns.status.heading=Status
imexport.activeimports.table.columns.actions.heading=Actions
imexport.activeimports.table.columns.status.finished=Finished
imexport.activeimports.table.columns.status.running=In progress
imexport.activeimports.table.columns.actions.button_label=Cancel

View File

@ -536,3 +536,23 @@ imexport.export.export_name.help=Name des Export Archives
imexport.export.export_name.label=nach
imexport.export.submit=Exportieren
imexport.export.cancel=Abbrechen
imexport.export.exportentities.help=W\u00e4hlen Sie die zu exportierenden Typen aus. Weitere ben\u00f6tigte Typen werden automatisch ausgew\u00e4hlt.
imexport.export.exportentities.label=Exportiere
imexport.activeexports.heading=Aktive Exporte
imexport.activeexports.table.columns.name.heading=Name
imexport.activeexports.table.columns.started.heading=Gestartet
imexport.activeexports.table.columns.status.heading=Status
imexport.activeexports.table.columns.actions.heading=Aktionen
imexport.activeexports.table.columns.status.finished=Abgeschlossen
imexport.activeexports.table.columns.status.running=In Arbeit
imexport.activeexports.table.columns.actions.button_label=Abbrechen
imexport.activeexports.none=Keine aktiven Exporte
imexport.activeimports.heading=Aktive Importe
imexport.activeimports.none=Keine aktiven Importe
imexport.activeimports.table.columns.name.heading=Name
imexport.activeimports.table.columns.started.heading=Gestartet
imexport.activeimports.table.columns.status.heading=Status
imexport.activeimports.table.columns.actions.heading=Aktionen
imexport.activeimports.table.columns.status.finished=Abgeschlossen
imexport.activeimports.table.columns.status.running=In Arbeit
imexport.activeimports.table.columns.actions.button_label=Abbrechen