> getControllerClasses();
+
+ /**
+ * A identifier to use by {@link MvcContext#uri(java.lang.String)} to
+ * generate the URI of the page. The identifier has the same format as used
+ * in JavaDoc:
+ *
+ * ControllerSimpleClassName#methodName
+ *
+ *
+ * @return The identifier to use for generating the URL of the page
+ */
+ String getUriIdentifier();
+
+ /**
+ * Gets the resourcebundle which provides the label of the admin page.
+ *
+ * @return The bundle to use for retrieving the label of the page.
+ */
+ String getLabelBundle();
+
+ /**
+ * Gets the key for retrieving the label of the page from the label bundle.
+ *
+ * @return The key of the label.
+ */
+ String getLabelKey();
+
+ /**
+ * Gets the resourcebundle which provides the description of the admin page.
+ *
+ * @return The bundle to use for retrieving the label of the page.
+ */
+ String getDescriptionBundle();
+
+ /**
+ * Gets the key for retrieving the description of the page from the
+ * description bundle.
+ *
+ * @return The key of the label.
+ */
+ String getDescriptionKey();
+
+ /**
+ * Name of icon to use.
+ *
+ * @return The icon to use for the page.
+ */
+ String getIcon();
+
+ /**
+ * Gets the position of the page in the admin nav bar.
+ *
+ * @return The position of the page in the admin navigation.
+ */
+ int getPosition();
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPageModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPageModel.java
new file mode 100644
index 000000000..40eaf8b96
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPageModel.java
@@ -0,0 +1,92 @@
+/*
+ * 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;
+
+import java.util.Comparator;
+
+/**
+ * Model for the data of an admin page.
+ *
+ * @see AdminPage
+ *
+ * @author Jens Pelzetter
+ */
+public class AdminPageModel implements Comparable {
+
+ private String uriIdentifier;
+
+ private String label;
+
+ private String description;
+
+ private String icon;
+
+ private long position;
+
+ public String getUriIdentifier() {
+ return uriIdentifier;
+ }
+
+ protected void setUriIdentifier(final String uriIdentifier) {
+ this.uriIdentifier = uriIdentifier;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ protected void setLabel(final String label) {
+ this.label = label;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ protected void setDescription(final String description) {
+ this.description = description;
+ }
+
+ public String getIcon() {
+ return icon;
+ }
+
+ protected void setIcon(final String icon) {
+ this.icon = icon;
+ }
+
+ public long getPosition() {
+ return position;
+ }
+
+ protected void setPosition(final long position) {
+ this.position = position;
+ }
+
+ @Override
+ public int compareTo(final AdminPageModel other) {
+ return Comparator
+ .nullsFirst(
+ Comparator
+ .comparing(AdminPageModel::getPosition)
+ .thenComparing(AdminPageModel::getLabel)
+ ).compare(this, other);
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPagesModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPagesModel.java
new file mode 100644
index 000000000..d459d5a44
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPagesModel.java
@@ -0,0 +1,109 @@
+/*
+ * 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;
+
+import org.libreccm.l10n.GlobalizationHelper;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.RequestScoped;
+import javax.enterprise.inject.Instance;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ * Model for the available admin pages.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("AdminPagesModel")
+public class AdminPagesModel {
+
+ /**
+ * Injection point for the admin pages.
+ */
+ @Inject
+ private Instance adminPages;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ /**
+ * Cache for bundles
+ */
+ private final Map bundles = new HashMap<>();
+
+ /**
+ * Retrieves the available admin pages and converts them into
+ * {@link AdminPageModel}s for usage in the views.
+ *
+ * @return A list of the available admin pages.
+ */
+ public List getAdminPages() {
+ return adminPages
+ .stream()
+ .sorted(
+ (page1, page2) -> Integer.compare(
+ page1.getPosition(), page2.getPosition()
+ )
+ )
+ .map(this::buildAdminPageModel)
+ .collect(Collectors.toList());
+ }
+
+ private AdminPageModel buildAdminPageModel(final AdminPage fromAdminPage) {
+ final ResourceBundle labelBundle = getBundle(
+ fromAdminPage.getLabelBundle()
+ );
+ final ResourceBundle descriptionBundle = getBundle(
+ fromAdminPage.getDescriptionBundle()
+ );
+
+ final AdminPageModel model = new AdminPageModel();
+ model.setUriIdentifier(fromAdminPage.getUriIdentifier());
+ model.setLabel(labelBundle.getString(fromAdminPage.getLabelKey()));
+ model.setDescription(
+ descriptionBundle.getString(
+ fromAdminPage.getDescriptionKey()
+ )
+ );
+ model.setIcon(fromAdminPage.getIcon());
+ model.setPosition(fromAdminPage.getPosition());
+ return model;
+ }
+
+ private ResourceBundle getBundle(final String bundleName) {
+ if (bundles.containsKey(bundleName)) {
+ return bundles.get(bundleName);
+ } else {
+ final ResourceBundle bundle = ResourceBundle.getBundle(
+ bundleName,
+ globalizationHelper.getNegotiatedLocale()
+ );
+ bundles.put(bundleName, bundle);
+ return bundle;
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationController.java
new file mode 100644
index 000000000..3a7cacf61
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationController.java
@@ -0,0 +1,31 @@
+/*
+ * 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.applications;
+
+/**
+ * Interface for controllers providing the UI for managing the instances of
+ * an application.
+ *
+ * @author Jens Pelzetter
+ */
+public interface ApplicationController {
+
+ String getApplication();
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationTypeInfoItem.java b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationTypeInfoItem.java
new file mode 100644
index 000000000..2baa272ae
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationTypeInfoItem.java
@@ -0,0 +1,131 @@
+/*
+ * 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.applications;
+
+import java.util.Objects;
+
+/**
+ * Data Transfer Object providing the information about an application. Used for
+ * rendering the informations the available applications in UI.
+ *
+ * @author Jens Pelzetter
+ */
+public class ApplicationTypeInfoItem implements
+ Comparable {
+
+ /**
+ * Name of the application.
+ */
+ private String name;
+
+ /**
+ * Localized title of the application, if available in the language of the
+ * current user.
+ */
+ private String title;
+
+ /**
+ * Localized title of the application, if available in the language of the
+ * current user.
+ */
+ private String description;
+
+ /**
+ * Is the application a singleton application?
+ */
+ private boolean singleton;
+
+ /**
+ * Number of existing instances of the application.
+ */
+ private long numberOfInstances;
+
+ /**
+ * Link the {@link ApplicationController} implementation of the application,
+ * if an implementation is available.
+ */
+ private String controllerLink;
+
+ protected ApplicationTypeInfoItem() {
+ // Nothing
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ protected void setName(final String name) {
+ this.name = name;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ protected void setTitle(final String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ protected void setDescription(final String description) {
+ this.description = description;
+ }
+
+ public boolean isSingleton() {
+ return singleton;
+ }
+
+ protected void setSingleton(final boolean singleton) {
+ this.singleton = singleton;
+ }
+
+ public long getNumberOfInstances() {
+ return numberOfInstances;
+ }
+
+ protected void setNumberOfInstances(final long numberOfInstances) {
+ this.numberOfInstances = numberOfInstances;
+ }
+
+ public String getControllerLink() {
+ return controllerLink;
+ }
+
+ protected void setControllerLink(final String controllerLink) {
+ this.controllerLink = controllerLink;
+ }
+
+ @Override
+ public int compareTo(final ApplicationTypeInfoItem other) {
+ if (other == null) {
+ return 1;
+ }
+
+ int result = Objects.compare(title, other.getTitle(), String::compareTo);
+ if (result == 0) {
+ result = Objects.compare(name, other.getName(), String::compareTo);
+ }
+
+ return result;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationsController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationsController.java
new file mode 100644
index 000000000..4c62faf24
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationsController.java
@@ -0,0 +1,131 @@
+/*
+ * 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.applications;
+
+import org.libreccm.core.CoreConstants;
+import org.libreccm.l10n.GlobalizationHelper;
+import org.libreccm.l10n.LocalizedTextsUtil;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+import org.libreccm.web.ApplicationManager;
+import org.libreccm.web.ApplicationRepository;
+import org.libreccm.web.ApplicationType;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Controller;
+import javax.mvc.Models;
+import javax.transaction.Transactional;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+/**
+ * Controller for the UI for managing application instances.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Controller
+@Path("/applications")
+public class ApplicationsController {
+
+ @Inject
+ private ApplicationManager appManager;
+
+ @Inject
+ private ApplicationRepository appRepository;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ @Inject
+ private Models models;
+
+ /**
+ * Retrives the avaiable application types, creates
+ * {@link ApplicationTypeInfoItem}s for them and makes them available using
+ * {@link #models}.
+ *
+ * @return The template to render.
+ */
+ @GET
+ @Path("/")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String getApplicationTypes() {
+ final List appTypes = appManager
+ .getApplicationTypes()
+ .entrySet()
+ .stream()
+ .map(Map.Entry::getValue)
+ .map(this::buildTypeInfoItem)
+ .sorted()
+ .collect(Collectors.toList());
+
+ models.put("applicationTypes", appTypes);
+
+ return "org/libreccm/ui/admin/applications/applicationtypes.xhtml";
+ }
+
+ /**
+ * Helper method for building an {@link ApplicationTypeInfoItem} for an
+ * {@link ApplicationType}.
+ *
+ * @param applicationType The application type.
+ *
+ * @return An {@link ApplicationTypeInfoItem} for the provided application
+ * type.
+ */
+ private ApplicationTypeInfoItem buildTypeInfoItem(
+ final ApplicationType applicationType
+ ) {
+ final ApplicationTypeInfoItem item = new ApplicationTypeInfoItem();
+ item.setName(applicationType.name());
+
+ final LocalizedTextsUtil textsUtil = globalizationHelper
+ .getLocalizedTextsUtil(applicationType.descBundle());
+ item.setTitle(textsUtil.getText(applicationType.titleKey()));
+ item.setDescription(textsUtil.getText(applicationType.descKey()));
+ item.setSingleton(applicationType.singleton());
+ item.setNumberOfInstances(
+ appRepository.findByType(applicationType.name()).size()
+ );
+
+ final Class extends ApplicationController> controllerClass
+ = applicationType.applicationController();
+
+ if (!DefaultApplicationController.class.isAssignableFrom(
+ controllerClass
+ )) {
+ item.setControllerLink(
+ String.format(
+ "%s#getApplication",
+ controllerClass.getSimpleName())
+ );
+ }
+
+ return item;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationsPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationsPage.java
new file mode 100644
index 000000000..f59dd840a
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/ApplicationsPage.java
@@ -0,0 +1,98 @@
+/*
+ * 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.applications;
+
+import org.libreccm.ui.admin.AdminConstants;
+import org.libreccm.ui.admin.AdminPage;
+import org.libreccm.web.ApplicationManager;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+/**
+ * {@link AdminPage} for managing applications.
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+public class ApplicationsPage implements AdminPage {
+
+ @Inject
+ private ApplicationManager applicationManager;
+
+ @Override
+ public Set> getControllerClasses() {
+ final Set> classes = new HashSet<>();
+ classes.add(ApplicationsController.class);
+
+ classes.addAll(
+ applicationManager
+ .getApplicationTypes()
+ .entrySet()
+ .stream()
+ .map(type -> type.getValue().applicationController())
+ .collect(Collectors.toSet())
+ );
+
+ return classes;
+ }
+
+ @Override
+ public String getUriIdentifier() {
+ return String.format(
+ "%s#getApplicationTypes",
+ ApplicationsController.class.getSimpleName()
+ );
+ }
+
+ @Override
+ public String getLabelBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getLabelKey() {
+ return "applications.label";
+ }
+
+ @Override
+ public String getDescriptionBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getDescriptionKey() {
+ return "applications.description";
+ }
+
+ @Override
+ public String getIcon() {
+ return "puzzle";
+ }
+
+ @Override
+ public int getPosition() {
+ return 40;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/applications/DefaultApplicationController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/DefaultApplicationController.java
new file mode 100644
index 000000000..56a11f0b3
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/DefaultApplicationController.java
@@ -0,0 +1,43 @@
+/*
+ * 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.applications;
+
+import javax.enterprise.context.RequestScoped;
+import javax.mvc.Controller;
+import javax.ws.rs.Path;
+
+/**
+ * A default implementation of the {@link ApplicationController} used if there
+ * is not implementation of the {@link ApplicationController} interface for an
+ * application.
+ *
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Controller
+@Path("/application")
+public class DefaultApplicationController implements ApplicationController {
+
+ @Override
+ public String getApplication() {
+ return "";
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/applications/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/package-info.java
new file mode 100644
index 000000000..f8a9d5a4e
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/applications/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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
+ */
+/**
+ * UI for managing application instances.
+ */
+package org.libreccm.ui.admin.applications;
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoriesController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoriesController.java
new file mode 100644
index 000000000..1c948edbe
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoriesController.java
@@ -0,0 +1,851 @@
+/*
+ * 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.categories;
+
+import org.libreccm.api.Identifier;
+import org.libreccm.api.IdentifierParser;
+import org.libreccm.categorization.Category;
+import org.libreccm.categorization.CategoryManager;
+import org.libreccm.categorization.CategoryRepository;
+import org.libreccm.categorization.Domain;
+import org.libreccm.categorization.DomainRepository;
+import org.libreccm.core.CoreConstants;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+import org.libreccm.ui.Message;
+import org.libreccm.ui.MessageType;
+import org.libreccm.ui.admin.AdminMessages;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Controller;
+import javax.mvc.Models;
+import javax.transaction.Transactional;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * Primary controller for the UI for managing category systems and categories.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Controller
+@Path("/categorymanager/categories")
+public class CategoriesController {
+
+ @Inject
+ private AdminMessages adminMessages;
+
+ @Inject
+ private CategoryDetailsModel categoryDetailsModel;
+
+ @Inject
+ private CategoryManager categoryManager;
+
+ @Inject
+ private CategoryRepository categoryRepository;
+
+ @Inject
+ private DomainRepository domainRepository;
+
+ @Inject
+ private IdentifierParser identifierParser;
+
+ @Inject
+ private Models models;
+
+ /**
+ * Show details about a category.
+ *
+ * @param categoryIdentifier Identifier of the category to show.
+ *
+ * @return The template to render.
+ */
+ @GET
+ @Path("/{categoryIdentifier}")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String getCategory(
+ @PathParam("categoryIdentifier") final String categoryIdentifier
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categoryIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ categoryDetailsModel.setCategory(result.get());
+ return "org/libreccm/ui/admin/categories/category-details.xhtml";
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(categoryIdentifier)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Show the edit form for a category.
+ *
+ * @param categoryIdentifier Identifier of the category to edit.
+ *
+ * @return The template to render.
+ */
+ @GET
+ @Path("/{categoryIdentifier}/edit")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editCategory(
+ @PathParam("categoryIdentifier") final String categoryIdentifier
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categoryIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ categoryDetailsModel.setCategory(result.get());
+ return "org/libreccm/ui/admin/categories/category-form.xhtml";
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(categoryIdentifier)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Displays the form for creating a new subcategory.
+ *
+ * @param categoryIdentifier The identifier of the parent category.
+ *
+ * @return The template to render.
+ */
+ @GET
+ @Path("/{categoryIdentifier}/subcategories/new")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String newSubCategory(
+ @PathParam("categoryIdentifier") final String categoryIdentifier
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categoryIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ categoryDetailsModel.setParentCategory(result.get());
+ return "org/libreccm/ui/admin/categories/category-form.xhtml";
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(categoryIdentifier)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Moves a category from one parent category to another. The target is
+ * provided
+ *
+ * @param categoryIdentifierParam Identifier of the category to move.
+ * @param targetIdentifierParam Identifier of the target category.
+ *
+ * @return Redirect to the detail page of the target category.
+ */
+ @POST
+ @Path("/{categoryIdentifier}/subcategories/move")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String moveSubCategory(
+ @PathParam("categoryIdentifier") final String categoryIdentifierParam,
+ @FormParam("targetIdentifier") final String targetIdentifierParam
+ ) {
+ final Identifier categoryIdentifier = identifierParser.parseIdentifier(
+ categoryIdentifierParam
+ );
+ final Optional categoryResult;
+ switch (categoryIdentifier.getType()) {
+ case ID:
+ categoryResult = categoryRepository.findById(
+ Long.parseLong(categoryIdentifier.getIdentifier())
+ );
+ break;
+ default:
+ categoryResult = categoryRepository.findByUuid(
+ categoryIdentifier.getIdentifier()
+ );
+ break;
+ }
+ if (!categoryResult.isPresent()) {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(categoryIdentifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+
+ final Identifier targetIdentifier = identifierParser.parseIdentifier(
+ targetIdentifierParam
+ );
+
+ final Optional targetResult;
+ switch (targetIdentifier.getType()) {
+ case ID:
+ targetResult = categoryRepository.findById(
+ Long.parseLong(targetIdentifier.getIdentifier())
+ );
+ break;
+ default:
+ targetResult = categoryRepository.findByUuid(
+ targetIdentifier.getIdentifier()
+ );
+ break;
+ }
+ if (!categoryResult.isPresent()) {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(targetIdentifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+
+ final Category category = categoryResult.get();
+ final Category oldParent = category.getParentCategory();
+ if (oldParent == null) {
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ }
+ final Category target = targetResult.get();
+
+ categoryManager.removeSubCategoryFromCategory(category, oldParent);
+ categoryManager.addSubCategoryToCategory(category, target);
+
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d", target.getObjectId()
+ );
+ }
+
+ /**
+ * Deletes a category.
+ *
+ * @param categoryIdentifier Identifier of the category to remove.
+ *
+ * @return Redirect to the details page of the parent category of the
+ * removed category.
+ */
+ @POST
+ @Path("/{categoryIdentifier}/subcategories/remove")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeSubCategory(
+ @PathParam("categoryIdentifier") final String categoryIdentifier
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categoryIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Category category = result.get();
+ final Category parentCategory = category.getParentCategory();
+ if (parentCategory == null) {
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ }
+ categoryManager.removeSubCategoryFromCategory(category,
+ parentCategory
+ );
+ categoryRepository.delete(category);
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ parentCategory.getObjectId()
+ );
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(categoryIdentifier)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Adds a localized title the a category.
+ *
+ * @param identifierParam Identifier of the category.
+ * @param localeParam The locale of the title.
+ * @param value The localized title.
+ *
+ * @return Redirect to the details page of the category.
+ */
+ @POST
+ @Path("/{identifier}/title/add")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addTitle(
+ @PathParam("identifier") final String identifierParam,
+ @FormParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ identifierParam
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Category category = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ category.getTitle().addValue(locale, value);
+ categoryRepository.save(category);
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(identifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Updates the localized title of a category.
+ *
+ * @param identifierParam Identifier of the category.
+ * @param localeParam The locale of the title.
+ * @param value The localized title.
+ *
+ * @return Redirect to the details page of the category.
+ */
+ @POST
+ @Path("/{identifier}/title/{locale}/edit")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editTitle(
+ @PathParam("identifier") final String identifierParam,
+ @PathParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ identifierParam
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Category category = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ category.getTitle().addValue(locale, value);
+ categoryRepository.save(category);
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(identifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Removes the localized title of a category.
+ *
+ * @param categoryIdentifierParam Identifier of the category.
+ * @param localeParam The locale of the title.
+ *
+ * @return Redirect to the details page of the category.
+ */
+ @POST
+ @Path("/{identifier}/title/{locale}/remove")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeTitle(
+ @PathParam("identifier")
+ final String categoryIdentifierParam,
+ @PathParam("locale") final String localeParam
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categoryIdentifierParam
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Category category = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ category.getTitle().removeValue(locale);
+ categoryRepository.save(category);
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(categoryIdentifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Adds a localized description the a category.
+ *
+ * @param identifierParam Identifier of the category.
+ * @param localeParam The locale of the description
+ * @param value The localized description.
+ *
+ * @return Redirect to the details page of the category.
+ */
+ @POST
+ @Path("/{identifier}decsription/add")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addDescription(
+ @PathParam("identifier") final String identifierParam,
+ @FormParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ identifierParam
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Category category = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ category.getDescription().addValue(locale, value);
+ categoryRepository.save(category);
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(identifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Updates the localized description the a category.
+ *
+ * @param identifierParam Identifier of the category.
+ * @param localeParam The locale of the description
+ * @param value The localized description.
+ *
+ * @return Redirect to the details page of the category.
+ */
+ @POST
+ @Path("/{identifier}/description/{locale}/edit")
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editDescription(
+ @PathParam("identifier") final String identifierParam,
+ @PathParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ identifierParam
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Category category = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ category.getDescription().addValue(locale, value);
+ categoryRepository.save(category);
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(identifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Removes a localized description the a category.
+ *
+ * @param identifierParam Identifier of the category.
+ * @param localeParam The locale of the description
+ *
+ * @return Redirect to the details page of the category.
+ */
+ @POST
+ @Path("/{identifier}/description/{locale}/remove")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeDescription(
+ @PathParam("identifier") final String identifierParam,
+ @PathParam("locale") final String localeParam
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ identifierParam
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Category category = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ category.getDescription().removeValue(locale);
+ categoryRepository.save(category);
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(identifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Changes the order of the subcategories of a category.
+ *
+ * @param categoryIdentifierParam Identifier of the category.
+ * @param subCategoryIdentifierParam Identifier of the sub category to move.
+ * @param direction The direction, either
+ * {@code INCREASE or DECREASE}.
+ *
+ * @return Redirect to the details page of the category.
+ */
+ @POST
+ @Path("/{categoryIdentifier}/subcategories/{subCategoryIdentifier}/reorder")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String reorderSubCategory(
+ @PathParam("categoryIdentifier") final String categoryIdentifierParam,
+ @PathParam("subCategoryIdentifier") final String subCategoryIdentifierParam,
+ @FormParam("direction") final String direction
+ ) {
+ final Identifier categoryIdentifier = identifierParser.parseIdentifier(
+ categoryIdentifierParam
+ );
+ final Identifier subCategoryIdentifier = identifierParser
+ .parseIdentifier(subCategoryIdentifierParam);
+
+ final Optional categoryResult;
+ switch (categoryIdentifier.getType()) {
+ case ID:
+ categoryResult = categoryRepository.findById(
+ Long.parseLong(categoryIdentifier.getIdentifier())
+ );
+ break;
+ default:
+ categoryResult = categoryRepository.findByUuid(
+ categoryIdentifier.getIdentifier()
+ );
+ break;
+ }
+ final Category category;
+ if (categoryResult.isPresent()) {
+ category = categoryResult.get();
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(categoryIdentifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+
+ final Optional subCategoryResult;
+ switch (subCategoryIdentifier.getType()) {
+ case ID:
+ subCategoryResult = categoryRepository.findById(
+ Long.parseLong(subCategoryIdentifier.getIdentifier())
+ );
+ break;
+ default:
+ subCategoryResult = categoryRepository.findByUuid(
+ subCategoryIdentifier.getIdentifier()
+ );
+ break;
+ }
+ final Category subCategory;
+ if (subCategoryResult.isPresent()) {
+ subCategory = subCategoryResult.get();
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(subCategoryIdentifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+
+ switch (direction) {
+ case "DECREASE":
+ categoryManager.decreaseCategoryOrder(subCategory, category);
+ break;
+ case "INCREASE":
+ categoryManager.increaseCategoryOrder(subCategory, category);
+ break;
+ default:
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.invalid_direction.message",
+ Arrays.asList(direction)),
+ MessageType.WARNING
+ )
+ );
+ }
+
+ if (category.getParentCategory() == null) {
+ final Optional categorySystem = domainRepository
+ .findByRootCategory(category);
+ if (categorySystem.isPresent()) {
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ categorySystem.get().getObjectId()
+ );
+ } else {
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ }
+ } else {
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoriesPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoriesPage.java
new file mode 100644
index 000000000..d4594658d
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoriesPage.java
@@ -0,0 +1,86 @@
+/*
+ * 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.categories;
+
+import org.libreccm.ui.admin.AdminConstants;
+import org.libreccm.ui.admin.AdminPage;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.enterprise.context.ApplicationScoped;
+
+/**
+ * {@link AdminPage} implementation for the UI for managing categories.
+ *
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+public class CategoriesPage implements AdminPage {
+
+ @Override
+ public Set> getControllerClasses() {
+ final Set> classes = new HashSet<>();
+ classes.add(CategorySystemsController.class);
+ classes.add(CategorySystemFormController.class);
+ classes.add(CategoriesController.class);
+ classes.add(CategoryFormController.class);
+ return classes;
+ }
+
+ @Override
+ public String getUriIdentifier() {
+ return String.format(
+ "%s#getCategorySystems",
+ CategorySystemsController.class.getSimpleName()
+ );
+ }
+
+ @Override
+ public String getLabelBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getLabelKey() {
+ return "categories.label";
+ }
+
+ @Override
+ public String getDescriptionBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getDescriptionKey() {
+ return "categories.description";
+ }
+
+ @Override
+ public String getIcon() {
+ return "diagram-3-fill";
+ }
+
+ @Override
+ public int getPosition() {
+ return 20;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryDetailsModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryDetailsModel.java
new file mode 100644
index 000000000..6ada58e99
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryDetailsModel.java
@@ -0,0 +1,327 @@
+/*
+ * 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.categories;
+
+import org.libreccm.categorization.Category;
+import org.libreccm.categorization.CategoryManager;
+import org.libreccm.categorization.Domain;
+import org.libreccm.categorization.DomainRepository;
+import org.libreccm.l10n.GlobalizationHelper;
+import org.libreccm.ui.Message;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.transaction.Transactional;
+
+/**
+ * Model for the details of a category.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CategoryDetailsModel")
+public class CategoryDetailsModel {
+
+ @Inject
+ private CategoryManager categoryManager;
+
+ @Inject
+ private DomainRepository domainRepository;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ private long categoryId;
+
+ private String uuid;
+
+ private String uniqueId;
+
+ private String name;
+
+ private String path;
+
+ private Map title;
+
+ private List unusedTitleLocales;
+
+ private Map description;
+
+ private List unusedDescriptionLocales;
+
+ private boolean enabled;
+
+ private boolean visible;
+
+ private boolean abstractCategory;
+
+ private List subCategories;
+
+ private CategoryNodeModel parentCategory;
+
+ private CategoryPathModel categoryPath;
+
+ private long categoryOrder;
+
+ private final List messages;
+
+ private Set invalidFields;
+
+ public CategoryDetailsModel() {
+ this.messages = new ArrayList<>();
+ }
+
+ public long getCategoryId() {
+ return categoryId;
+ }
+
+ public String getIdentifier() {
+ return String.format("ID-%d", categoryId);
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public String getUniqueId() {
+ return uniqueId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public Map getTitle() {
+ return Collections.unmodifiableMap(title);
+ }
+
+ public List getUnusedTitleLocales() {
+ return Collections.unmodifiableList(unusedTitleLocales);
+ }
+
+ public boolean hasUnusedTitleLocales() {
+ return !unusedTitleLocales.isEmpty();
+ }
+
+ public Map getDescription() {
+ return Collections.unmodifiableMap(description);
+ }
+
+ public List getUnusedDescriptionLocales() {
+ return Collections.unmodifiableList(unusedDescriptionLocales);
+ }
+
+ public boolean hasUnusedDescriptionLocales() {
+ return !unusedDescriptionLocales.isEmpty();
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ public boolean isAbstractCategory() {
+ return abstractCategory;
+ }
+
+ public List getSubCategories() {
+ return Collections.unmodifiableList(subCategories);
+ }
+
+ public CategoryNodeModel getParentCategory() {
+ return parentCategory;
+ }
+
+ protected void setParentCategory(final Category parent) {
+ parentCategory = buildCategoryNodeModel(parent);
+ }
+
+ public CategoryPathModel getCategoryPath() {
+ return categoryPath;
+ }
+
+ public long getCategoryOrder() {
+ return categoryOrder;
+ }
+
+ public boolean isNew() {
+ return categoryId == 0;
+ }
+
+ public List getMessages() {
+ return Collections.unmodifiableList(messages);
+ }
+
+ public void addMessage(final Message message) {
+ messages.add(message);
+ }
+
+ public Set getInvalidFields() {
+ return Collections.unmodifiableSet(invalidFields);
+ }
+
+ protected void addInvalidField(final String invalidField) {
+ invalidFields.add(invalidField);
+ }
+
+ protected void setInvalidFields(final Set invalidFields) {
+ this.invalidFields = new HashSet<>(invalidFields);
+ }
+
+ /**
+ * Sets the model to the properties of the provided category.
+ *
+ * @param category The category.
+ */
+ @Transactional(Transactional.TxType.REQUIRED)
+ protected void setCategory(final Category category) {
+ Objects.requireNonNull(category);
+
+ categoryId = category.getObjectId();
+ uuid = category.getUuid();
+ uniqueId = category.getUniqueId();
+ name = category.getName();
+ path = categoryManager.getCategoryPath(category);
+
+ final List availableLocales = globalizationHelper
+ .getAvailableLocales();
+ title = category
+ .getTitle()
+ .getValues()
+ .entrySet()
+ .stream()
+ .collect(
+ Collectors.toMap(
+ entry -> entry.getKey().toString(),
+ entry -> entry.getValue()
+ )
+ );
+ final Set titleLocales = category
+ .getTitle()
+ .getAvailableLocales();
+ unusedTitleLocales = availableLocales
+ .stream()
+ .filter(locale -> !titleLocales.contains(locale))
+ .map(Locale::toString)
+ .sorted()
+ .collect(Collectors.toList());
+
+ description = category
+ .getDescription()
+ .getValues()
+ .entrySet()
+ .stream()
+ .collect(
+ Collectors.toMap(
+ entry -> entry.getKey().toString(),
+ entry -> entry.getValue()
+ )
+ );
+ final Set descriptionLocales = category
+ .getDescription()
+ .getAvailableLocales();
+ unusedDescriptionLocales = availableLocales
+ .stream()
+ .filter(locale -> !descriptionLocales.contains(locale))
+ .map(Locale::toString)
+ .sorted()
+ .collect(Collectors.toList());
+
+ enabled = category.isEnabled();
+ visible = category.isVisible();
+ abstractCategory = category.isAbstractCategory();
+ subCategories = category
+ .getSubCategories()
+ .stream()
+ .map(this::buildCategoryNodeModel)
+ .sorted()
+ .collect(Collectors.toList());
+ if (category.getParentCategory() != null) {
+ parentCategory
+ = buildCategoryNodeModel(category.getParentCategory());
+ }
+ categoryPath = buildCategoryPathModel(category);
+ categoryOrder = category.getCategoryOrder();
+ }
+
+ private DomainNodeModel buildDomainNodeModel(final Domain domain) {
+ final DomainNodeModel model = new DomainNodeModel();
+ model.setDomainId(domain.getObjectId());
+ model.setUuid(domain.getUuid());
+ model.setDomainKey(domain.getDomainKey());
+
+ return model;
+ }
+
+ private CategoryNodeModel buildCategoryNodeModel(final Category category) {
+ final CategoryNodeModel model = new CategoryNodeModel();
+ model.setCategoryId(category.getObjectId());
+ model.setUuid(category.getUuid());
+ model.setUniqueId(category.getUniqueId());
+ model.setName(category.getName());
+ model.setPath(categoryManager.getCategoryPath(category));
+ model.setCategoryOrder(category.getCategoryOrder());
+ model.setEnabled(category.isEnabled());
+ model.setVisible(category.isVisible());
+ model.setAbstractCategory(category.isAbstractCategory());
+ return model;
+ }
+
+ private CategoryPathModel buildCategoryPathModel(final Category category) {
+ return buildCategoryPathModel(category, new CategoryPathModel());
+ }
+
+ private CategoryPathModel buildCategoryPathModel(
+ final Category category,
+ final CategoryPathModel categoryPathModel
+ ) {
+ categoryPathModel.addCategoryAtBegin(buildCategoryNodeModel(category));
+ final Category parent = category.getParentCategory();
+ if (parent == null) {
+ final Optional domain = domainRepository
+ .findByRootCategory(category);
+ if (domain.isPresent()) {
+ categoryPathModel.setDomain(buildDomainNodeModel(domain.get()));
+ }
+ return categoryPathModel;
+ } else {
+ return buildCategoryPathModel(parent, categoryPathModel);
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryFormController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryFormController.java
new file mode 100644
index 000000000..3ff5219bb
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryFormController.java
@@ -0,0 +1,230 @@
+/*
+ * 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.categories;
+
+import org.libreccm.api.Identifier;
+import org.libreccm.api.IdentifierParser;
+import org.libreccm.categorization.Category;
+import org.libreccm.categorization.CategoryManager;
+import org.libreccm.categorization.CategoryRepository;
+import org.libreccm.categorization.Domain;
+import org.libreccm.categorization.DomainRepository;
+import org.libreccm.core.CoreConstants;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+import org.libreccm.ui.Message;
+import org.libreccm.ui.MessageType;
+import org.libreccm.ui.admin.AdminMessages;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Controller;
+import javax.transaction.Transactional;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+/**
+ * Controller processing the POST requests from the form for creating and
+ * editing categories.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Controller
+@Path("/categorymanager/categories")
+public class CategoryFormController {
+
+ @Inject
+ private AdminMessages adminMessages;
+
+ @Inject
+ private CategoryDetailsModel categoryDetailsModel;
+
+ @Inject
+ private CategoryManager categoryManager;
+
+ @Inject
+ private CategoryRepository categoryRepository;
+
+ @Inject
+ private DomainRepository domainRepository;
+
+ @Inject
+ private IdentifierParser identifierParser;
+
+ @FormParam("uniqueId")
+ private String uniqueId;
+
+ @FormParam("name")
+ private String name;
+
+ @FormParam("enabled")
+ private String enabled;
+
+ @FormParam("visible")
+ private String visible;
+
+ @FormParam("abstractCategory")
+ private String abstractCategory;
+
+ /**
+ * Create a new category.
+ *
+ * @param parentCategoryIdentifier Identifier of the parent category.
+ * @return Redirect to the details page of the parent category.
+ */
+ @POST
+ @Path("/{parentCategoryIdentifier}/new")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String createCategory(
+ @PathParam("parentCategoryIdentifier") final String parentCategoryIdentifier
+ ) {
+ final Identifier parentIdentifier = identifierParser.parseIdentifier(
+ parentCategoryIdentifier
+ );
+ final Optional parentResult;
+ switch (parentIdentifier.getType()) {
+ case ID:
+ parentResult = categoryRepository.findById(
+ Long.parseLong(
+ parentIdentifier.getIdentifier()
+ )
+ );
+ break;
+ default:
+ parentResult = categoryRepository.findByUuid(
+ parentIdentifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (parentResult.isPresent()) {
+ final Category parentCategory = parentResult.get();
+ final Category category = new Category();
+ category.setUniqueId(uniqueId);
+ category.setName(name);
+ category.setEnabled(enabled != null);
+ category.setVisible(visible != null);
+ category.setAbstractCategory(abstractCategory != null);
+
+ categoryRepository.save(category);
+ categoryManager.addSubCategoryToCategory(category, parentCategory);
+
+ if (parentCategory.getParentCategory() == null) {
+ final Optional categorySystem = domainRepository
+ .findByRootCategory(parentCategory);
+ if (categorySystem.isPresent()) {
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ categorySystem.get().getObjectId()
+ );
+ } else {
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ parentCategory.getObjectId()
+ );
+ }
+ } else {
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ parentCategory.getObjectId()
+ );
+ }
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(parentCategoryIdentifier)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Updates a category with the data from the form.
+ *
+ * @param categoryIdentifierParam Identifier of the category to update.
+ * @return Redirect to the details page of the category.
+ */
+ @POST
+ @Path("/{categoryIdentifier}/edit")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String updateCategory(
+ @PathParam("categoryIdentifier")
+ final String categoryIdentifierParam
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categoryIdentifierParam
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = categoryRepository.findById(
+ Long.parseLong(
+ identifier.getIdentifier()
+ )
+ );
+ break;
+ default:
+ result = categoryRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Category category = result.get();
+ category.setUniqueId(uniqueId);
+ category.setName(name);
+ category.setEnabled(enabled != null);
+ category.setVisible(visible != null);
+ category.setAbstractCategory(abstractCategory != null);
+
+ categoryRepository.save(category);
+
+ return String.format(
+ "redirect:categorymanager/categories/ID-%d",
+ category.getObjectId()
+ );
+ } else {
+ categoryDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categories.not_found.message",
+ Arrays.asList(categoryIdentifierParam)
+ ), MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/category-not-found.xhtml";
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryNodeModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryNodeModel.java
new file mode 100644
index 000000000..d915b3426
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryNodeModel.java
@@ -0,0 +1,142 @@
+/*
+ * 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.categories;
+
+import java.util.Objects;
+
+/**
+ * A DTO with the of a category shown in the UI.
+ *
+ * @author Jens Pelzetter
+ */
+public class CategoryNodeModel implements Comparable {
+
+ private long categoryId;
+
+ private String uuid;
+
+ private String uniqueId;
+
+ private String name;
+
+ private String path;
+
+ private boolean enabled;
+
+ private boolean visible;
+
+ private boolean abstractCategory;
+
+ private long categoryOrder;
+
+ public long getCategoryId() {
+ return categoryId;
+ }
+
+ protected void setCategoryId(final long categoryId) {
+ this.categoryId = categoryId;
+ }
+
+ public String getIdentifier() {
+ return String.format("ID-%d", categoryId);
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ protected void setUuid(final String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getUniqueId() {
+ return uniqueId;
+ }
+
+ protected void setUniqueId(final String uniqueId) {
+ this.uniqueId = uniqueId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ protected void setName(final String name) {
+ this.name = name;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ protected void setPath(final String path) {
+ this.path = path;
+ }
+
+ public long getCategoryOrder() {
+ return categoryOrder;
+ }
+
+ protected void setCategoryOrder(final long categoryOrder) {
+ this.categoryOrder = categoryOrder;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ protected void setEnabled(final boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isVisible() {
+ return visible;
+ }
+
+ protected void setVisible(final boolean visible) {
+ this.visible = visible;
+ }
+
+ public boolean isAbstractCategory() {
+ return abstractCategory;
+ }
+
+ protected void setAbstractCategory(final boolean abstractCategory) {
+ this.abstractCategory = abstractCategory;
+ }
+
+ @Override
+ public int compareTo(final CategoryNodeModel other) {
+ int result = Long.compare(
+ categoryOrder,
+ Objects.requireNonNull(other).getCategoryOrder()
+ );
+
+ if (result == 0) {
+ result = Objects.compare(
+ name,
+ Objects.requireNonNull(other).getName(),
+ String::compareTo
+ );
+ }
+
+ return result;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryPathModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryPathModel.java
new file mode 100644
index 000000000..c50c2df55
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategoryPathModel.java
@@ -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.categories;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Model for displaying the path of category in the UI.
+ *
+ * @author Jens Pelzetter
+ */
+public class CategoryPathModel {
+
+ private DomainNodeModel domain;
+
+ private List categories;
+
+ public CategoryPathModel() {
+ categories = new ArrayList<>();
+ }
+
+ public DomainNodeModel getDomain() {
+ return domain;
+ }
+
+ protected void setDomain(final DomainNodeModel domain) {
+ this.domain = domain;
+ }
+
+ public List getCategories() {
+ return Collections.unmodifiableList(categories);
+ }
+
+ protected void addCategory(final CategoryNodeModel category) {
+ categories.add(category);
+ }
+
+ protected void addCategoryAtBegin(final CategoryNodeModel category) {
+ categories.add(0, category);
+ }
+
+ protected void setCategories(final List categories) {
+ this.categories = new ArrayList<>(categories);
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemDetailsModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemDetailsModel.java
new file mode 100644
index 000000000..2c136f59f
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemDetailsModel.java
@@ -0,0 +1,362 @@
+/*
+ * 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.categories;
+
+import org.libreccm.categorization.Category;
+import org.libreccm.categorization.CategoryManager;
+import org.libreccm.categorization.Domain;
+import org.libreccm.categorization.DomainOwnership;
+import org.libreccm.l10n.GlobalizationHelper;
+import org.libreccm.ui.Message;
+import org.libreccm.web.ApplicationRepository;
+import org.libreccm.web.CcmApplication;
+
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.transaction.Transactional;
+
+/**
+ * Model for the details of a category system (Domain)
+ *
+ * @see org.libreccm.categorization.Domain
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CategorySystemDetailsModel")
+public class CategorySystemDetailsModel {
+
+ @Inject
+ private ApplicationRepository applicationRepository;
+
+ @Inject
+ private CategoryManager categoryManager;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ private long categorySystemId;
+
+ private String uuid;
+
+ private String domainKey;
+
+ private String uri;
+
+ private String version;
+
+ private String released;
+
+ private Map title;
+
+ private List unusedTitleLocales;
+
+ private Map description;
+
+ private List unusedDescriptionLocales;
+
+ private List owners;
+
+ private List ownerOptions;
+
+ private String rootIdentifier;
+
+ private List categories;
+
+ private final List messages;
+
+ private Set invalidFields;
+
+ public CategorySystemDetailsModel() {
+ messages = new ArrayList<>();
+ invalidFields = new HashSet<>();
+ }
+
+ public long getCategorySystemId() {
+ return categorySystemId;
+ }
+
+ protected void setCategorySystemId(final long categorySystemId) {
+ this.categorySystemId = categorySystemId;
+ }
+
+ public String getIdentifier() {
+ return String.format("ID-%d", categorySystemId);
+ }
+
+ public String getRootIdentifier() {
+ return rootIdentifier;
+ }
+
+ protected void setRootIdentifier(final String rootIdentifier) {
+ this.rootIdentifier = rootIdentifier;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ protected void setUuid(final String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getDomainKey() {
+ return domainKey;
+ }
+
+ protected void setDomainKey(final String domainKey) {
+ this.domainKey = domainKey;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ protected void setUri(final String uri) {
+ this.uri = uri;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ protected void setVersion(final String version) {
+ this.version = version;
+ }
+
+ public String getReleased() {
+ return released;
+ }
+
+ protected void setReleased(final String released) {
+ this.released = released;
+ }
+
+ protected void setReleased(final LocalDate released) {
+ if (released == null) {
+ this.released = "";
+ } else {
+ this.released = DateTimeFormatter.ISO_DATE.format(released);
+ }
+ }
+
+ public Map getTitle() {
+ return Collections.unmodifiableMap(title);
+ }
+
+ public List getUnusedTitleLocales() {
+ return Collections.unmodifiableList(unusedTitleLocales);
+ }
+
+ public boolean hasUnusedTitleLocales() {
+ return !unusedTitleLocales.isEmpty();
+ }
+
+ public Map getDescription() {
+ return Collections.unmodifiableMap(description);
+ }
+
+ public List getUnusedDescriptionLocales() {
+ return Collections.unmodifiableList(unusedDescriptionLocales);
+ }
+
+ public boolean hasUnusedDescriptionLocales() {
+ return !unusedDescriptionLocales.isEmpty();
+ }
+
+ public List getOwners() {
+ return Collections.unmodifiableList(owners);
+ }
+
+ public List getOwnerOptions() {
+ return Collections.unmodifiableList(ownerOptions);
+ }
+
+ public List getCategories() {
+ return Collections.unmodifiableList(categories);
+ }
+
+ public boolean isNew() {
+ return categorySystemId == 0;
+ }
+
+ public List getMessages() {
+ return Collections.unmodifiableList(messages);
+ }
+
+ public void addMessage(final Message message) {
+ messages.add(message);
+ }
+
+ public Set getInvalidFields() {
+ return Collections.unmodifiableSet(invalidFields);
+ }
+
+ protected void addInvalidField(final String invalidField) {
+ invalidFields.add(invalidField);
+ }
+
+ protected void setInvalidFields(final Set invalidFields) {
+ this.invalidFields = new HashSet<>(invalidFields);
+ }
+
+ /**
+ * Sets the properties of this model using the provided {@link Domain}.
+ * @param domain The domain to display.
+ */
+ @Transactional(Transactional.TxType.REQUIRED)
+ protected void setCategorySystem(final Domain domain) {
+ Objects.requireNonNull(domain);
+
+ categorySystemId = domain.getObjectId();
+ uuid = domain.getUuid();
+ domainKey = domain.getDomainKey();
+ uri = domain.getUri();
+ version = domain.getVersion();
+ if (domain.getReleased() == null) {
+ released = "";
+ } else {
+ released = DateTimeFormatter.ISO_DATE
+ .withZone(ZoneOffset.systemDefault())
+ .format(domain.getReleased());
+ }
+
+ final List availableLocales = globalizationHelper
+ .getAvailableLocales();
+ title = domain
+ .getTitle()
+ .getValues()
+ .entrySet()
+ .stream()
+ .collect(
+ Collectors.toMap(
+ entry -> entry.getKey().toString(),
+ entry -> entry.getValue()
+ )
+ );
+ final Set titleLocales = domain
+ .getTitle()
+ .getAvailableLocales();
+ unusedTitleLocales = availableLocales
+ .stream()
+ .filter(locale -> !titleLocales.contains(locale))
+ .map(Locale::toString)
+ .sorted()
+ .collect(Collectors.toList());
+
+ description = domain
+ .getDescription()
+ .getValues()
+ .entrySet()
+ .stream()
+ .collect(
+ Collectors.toMap(
+ entry -> entry.getKey().toString(),
+ entry -> entry.getValue()
+ )
+ );
+ final Set descriptionLocales = domain
+ .getDescription()
+ .getAvailableLocales();
+ unusedDescriptionLocales = availableLocales
+ .stream()
+ .filter(locale -> !descriptionLocales.contains(locale))
+ .map(Locale::toString)
+ .sorted()
+ .collect(Collectors.toList());
+
+ owners = domain
+ .getOwners()
+ .stream()
+ .map(this::buildOwnerRow)
+ .sorted()
+ .collect(Collectors.toList());
+
+ final List ownerApplications = domain
+ .getOwners()
+ .stream()
+ .map(DomainOwnership::getOwner)
+ .collect(Collectors.toList());
+
+ ownerOptions = applicationRepository
+ .findAll()
+ .stream()
+ .filter(application -> !ownerApplications.contains(application))
+ .map(CategorySystemOwnerOption::new)
+ .sorted()
+ .collect(Collectors.toList());
+
+
+ rootIdentifier = String.format("UUID-%s", domain.getRoot().getUuid());
+
+ categories = domain
+ .getRoot()
+ .getSubCategories()
+ .stream()
+ .map(this::buildCategoryTableRow)
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+ private CategorySystemOwnerRow buildOwnerRow(
+ final DomainOwnership ownership
+ ) {
+ final CategorySystemOwnerRow ownerRow = new CategorySystemOwnerRow();
+ ownerRow.setOwnershipId(ownership.getOwnershipId());
+ ownerRow.setUuid(ownership.getOwner().getUuid());
+ ownerRow.setContext(ownership.getContext());
+ ownerRow.setOwnerOrder(ownership.getOwnerOrder());
+ if (ownership.getOwner().getDisplayName() == null) {
+ ownerRow.setOwnerAppName(ownership.getOwner().getApplicationType());
+ } else {
+ ownerRow.setOwnerAppName(ownership.getOwner().getDisplayName());
+ }
+
+ return ownerRow;
+ }
+
+ private CategoryNodeModel buildCategoryTableRow(final Category category) {
+ final CategoryNodeModel row = new CategoryNodeModel();
+ row.setCategoryId(category.getObjectId());
+ row.setUuid(category.getUuid());
+ row.setUniqueId(category.getUniqueId());
+ row.setName(category.getName());
+ row.setPath(categoryManager.getCategoryPath(category));
+ row.setEnabled(category.isEnabled());
+ row.setVisible(category.isVisible());
+ row.setAbstractCategory(category.isAbstractCategory());
+ row.setCategoryOrder(category.getCategoryOrder());
+ return row;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemFormController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemFormController.java
new file mode 100644
index 000000000..8d491c256
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemFormController.java
@@ -0,0 +1,231 @@
+/*
+ * 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.categories;
+
+import org.apache.commons.validator.routines.UrlValidator;
+import org.libreccm.api.Identifier;
+import org.libreccm.api.IdentifierParser;
+import org.libreccm.categorization.Domain;
+import org.libreccm.categorization.DomainManager;
+import org.libreccm.categorization.DomainRepository;
+import org.libreccm.core.CoreConstants;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+import org.libreccm.ui.Message;
+import org.libreccm.ui.MessageType;
+import org.libreccm.ui.admin.AdminMessages;
+
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Controller;
+import javax.transaction.Transactional;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+/**
+ * Controller for processing the {@code POST} requests from the form for
+ * creating and editing category systems.
+ *
+ * @author Jens Pelzetter
+ */
+@Controller
+@Path("/categorymanager/categorysystems")
+@RequestScoped
+public class CategorySystemFormController {
+
+ @Inject
+ private AdminMessages adminMessages;
+
+ @Inject
+ private CategorySystemDetailsModel categorySystemDetailsModel;
+
+ @Inject
+ private DomainManager domainManager;
+
+ @Inject
+ private DomainRepository domainRepository;
+
+ @Inject
+ private IdentifierParser identifierParser;
+
+ @FormParam("domainKey")
+ private String domainKey;
+
+ @FormParam("uri")
+ private String uri;
+
+ @FormParam("version")
+ private String version;
+
+ @FormParam("released")
+ private String released;
+
+ /**
+ * Creates a new category system (domain).
+ *
+ * @return Redirect to the list of category systems.
+ */
+ @POST
+ @Path("/new")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String createCategorySystem() {
+
+ if (!isValidUri()) {
+ categorySystemDetailsModel.setDomainKey(domainKey);
+ categorySystemDetailsModel.setUri(uri);
+ categorySystemDetailsModel.setVersion(version);
+ categorySystemDetailsModel.setReleased(released);
+
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.get("categorysystems.form.errors.uri_invalid"),
+ MessageType.DANGER)
+ );
+ categorySystemDetailsModel.addInvalidField("uri");
+ return "org/libreccm/ui/admin/categories/categorysystem-form.xhtml";
+ }
+
+ final Domain domain = domainManager.createDomain(domainKey, domainKey);
+ domain.setUri(uri);
+ domain.setVersion(version);
+ if (released == null || released.isEmpty()) {
+ domain.setReleased(null);
+ } else {
+ domain.setReleased(convertReleased());
+ }
+ domainRepository.save(domain);
+
+ return "redirect:/categorymanager/categorysystems";
+ }
+
+ /**
+ * Update a category with the data from the form.
+ *
+ * @param identifierParam Identifier of the category system to update.
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path("{categorySystemIdentifier}/edit")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String updateCategorySystem(
+ @PathParam("categorySystemIdentifier")
+ final String identifierParam
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ identifierParam
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(identifier.getIdentifier());
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ if (!isValidUri()) {
+ categorySystemDetailsModel.setDomainKey(domainKey);
+ categorySystemDetailsModel.setUri(uri);
+ categorySystemDetailsModel.setVersion(version);
+ categorySystemDetailsModel.setReleased(released);
+
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.get(
+ "categorysystems.form.errors.uri_invalid"),
+ MessageType.DANGER)
+ );
+ categorySystemDetailsModel.addInvalidField("uri");
+ return "org/libreccm/ui/admin/categories/categorysystem-form.xhtml";
+ }
+ final Domain domain = result.get();
+ domain.setDomainKey(domainKey);
+ domain.setUri(uri);
+ domain.setVersion(version);
+ if (released == null || released.isEmpty()) {
+ domain.setReleased(null);
+ } else {
+ domain.setReleased(convertReleased());
+ }
+ domainRepository.save(domain);
+
+ return String.format(
+ "redirect:/categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(identifierParam)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Helper method for converting the {@link #released} date to an ISO 8601
+ * formatted string.
+ *
+ * @return The released date in ISO 8601 format.
+ */
+ private LocalDate convertReleased() {
+ return LocalDate.parse(
+ released,
+ DateTimeFormatter.ISO_DATE.withZone(ZoneOffset.systemDefault())
+ );
+ }
+
+ /**
+ * Helper method for validating a URI.
+ *
+ * @return {@code true} if the URI is valid, {@code false} otherwise.
+ */
+ private boolean isValidUri() {
+ final UrlValidator urlValidator = new UrlValidator();
+ return urlValidator.isValid(uri);
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemOwnerOption.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemOwnerOption.java
new file mode 100644
index 000000000..a0362daf3
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemOwnerOption.java
@@ -0,0 +1,70 @@
+/*
+ * 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.categories;
+
+import org.libreccm.web.CcmApplication;
+
+import java.util.Objects;
+
+/**
+ * DTO for the options for selecting the owner applications of a category system.
+ *
+ * @author Jens Pelzetter
+ */
+public class CategorySystemOwnerOption
+ implements Comparable {
+
+ private final long applicationId;
+
+ private final String applicationUuid;
+
+ private final String applicationName;
+
+ public CategorySystemOwnerOption(final CcmApplication application) {
+ applicationId = application.getObjectId();
+ applicationUuid = application.getUuid();
+ if (application.getDisplayName() == null) {
+ applicationName = application.getApplicationType();
+ } else {
+ applicationName = application.getDisplayName();
+ }
+ }
+
+ public long getApplicationId() {
+ return applicationId;
+ }
+
+ public String getApplicationUuid() {
+ return applicationUuid;
+ }
+
+ public String getApplicationName() {
+ return applicationName;
+ }
+
+ @Override
+ public int compareTo(final CategorySystemOwnerOption other) {
+ return Objects.compare(
+ applicationName,
+ Objects.requireNonNull(other).getApplicationName(),
+ String::compareTo
+ );
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemOwnerRow.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemOwnerRow.java
new file mode 100644
index 000000000..c258b2619
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemOwnerRow.java
@@ -0,0 +1,84 @@
+/*
+ * 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.categories;
+
+/**
+ * Data for a row in the table of owner applications of a category system.
+ *
+ * @author Jens Pelzetter
+ */
+public class CategorySystemOwnerRow
+ implements Comparable{
+
+ private long ownershipId;
+
+ private String uuid;
+
+ private String ownerAppName;
+
+ private String context;
+
+ private long ownerOrder;
+
+ public long getOwnershipId() {
+ return ownershipId;
+ }
+
+ void setOwnershipId(final long ownershipId) {
+ this.ownershipId = ownershipId;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ void setUuid(final String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getOwnerAppName() {
+ return ownerAppName;
+ }
+
+ void setOwnerAppName(final String ownerAppName) {
+ this.ownerAppName = ownerAppName;
+ }
+
+ public String getContext() {
+ return context;
+ }
+
+ void setContext(final String context) {
+ this.context = context;
+ }
+
+ public long getOwnerOrder() {
+ return ownerOrder;
+ }
+
+ void setOwnerOrder(final long ownerOrder) {
+ this.ownerOrder = ownerOrder;
+ }
+
+ @Override
+ public int compareTo(final CategorySystemOwnerRow other) {
+ return Long.compare(ownerOrder, other.getOwnerOrder());
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemTableRow.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemTableRow.java
new file mode 100644
index 000000000..3cf5693b4
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemTableRow.java
@@ -0,0 +1,118 @@
+/*
+ * 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.categories;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Data for a row in the table of category systems.
+ *
+ * @author Jens Pelzetter
+ */
+public class CategorySystemTableRow implements
+ Comparable {
+
+ private long domainId;
+
+ private String domainKey;
+
+ private String uri;
+
+ private Map title;
+
+ private String version;
+
+ private String released;
+
+ public long getDomainId() {
+ return domainId;
+ }
+
+ void setDomainId(final long domainId) {
+ this.domainId = domainId;
+ }
+
+ public String getIdentifier() {
+ return String.format("ID-%d", domainId);
+ }
+
+ public String getDomainKey() {
+ return domainKey;
+ }
+
+ void setDomainKey(final String domainKey) {
+ this.domainKey = domainKey;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ void setUri(final String uri) {
+ this.uri = uri;
+ }
+
+ public Map getTitle() {
+ return Collections.unmodifiableMap(title);
+ }
+
+ void setTitle(final Map title) {
+ this.title = new HashMap<>(title);
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ void setVersion(final String version) {
+ this.version = version;
+ }
+
+ public String getReleased() {
+ return released;
+ }
+
+ void setReleased(final String released) {
+ this.released = released;
+ }
+
+ @Override
+ public int compareTo(final CategorySystemTableRow other) {
+ int result;
+ result = Objects.compare(
+ domainKey, other.getDomainKey(), String::compareTo
+ );
+
+ if (result == 0) {
+ result = Objects.compare(uri, uri, String::compareTo);
+ }
+
+ if (result == 0) {
+ result = Objects.compare(
+ domainId, other.getDomainId(), Long::compare
+ );
+ }
+
+ return result;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemsController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemsController.java
new file mode 100644
index 000000000..b70335f61
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemsController.java
@@ -0,0 +1,852 @@
+/*
+ * 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.categories;
+
+import org.libreccm.api.Identifier;
+import org.libreccm.api.IdentifierParser;
+import org.libreccm.categorization.Domain;
+import org.libreccm.categorization.DomainManager;
+import org.libreccm.categorization.DomainRepository;
+import org.libreccm.core.CoreConstants;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+import org.libreccm.ui.Message;
+import org.libreccm.ui.MessageType;
+import org.libreccm.ui.admin.AdminMessages;
+import org.libreccm.web.ApplicationRepository;
+import org.libreccm.web.CcmApplication;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Controller;
+import javax.mvc.Models;
+import javax.transaction.Transactional;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+/**
+ * Controller for the UI for managing category systems.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Controller
+@Path("/categorymanager/categorysystems")
+public class CategorySystemsController {
+
+ @Inject
+ private AdminMessages adminMessages;
+
+ @Inject
+ private CategorySystemDetailsModel categorySystemDetailsModel;
+
+ @Inject
+ private ApplicationRepository applicationRepository;
+
+ @Inject
+ private DomainManager domainManager;
+
+ @Inject
+ private DomainRepository domainRepository;
+
+ @Inject
+ private IdentifierParser identifierParser;
+
+ @Inject
+ private Models models;
+
+ /**
+ * Show a list of all available category systems.
+ *
+ * @return The template to use.
+ */
+ @GET
+ @Path("/")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String getCategorySystems() {
+ return "org/libreccm/ui/admin/categories/categorysystems.xhtml";
+ }
+
+ /**
+ * Display the details of a category system.
+ *
+ * @param categorySystemIdentifier Identifier of the category system to
+ * show.
+ *
+ * @return The template to use.
+ */
+ @GET
+ @Path("/{categorySystemIdentifier}/details")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String getCategorySystemDetails(
+ @PathParam("categorySystemIdentifier")
+ final String categorySystemIdentifier
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier())
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ categorySystemDetailsModel.setCategorySystem(result.get());
+ return "org/libreccm/ui/admin/categories/categorysystem-details.xhtml";
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Show the form for creating a new category system.
+ *
+ * @return The template to use.
+ */
+ @GET
+ @Path("/new")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String newCategorySystem() {
+ return "org/libreccm/ui/admin/categories/categorysystem-form.xhtml";
+ }
+
+ /**
+ * Edit a category system.
+ *
+ * @param categorySystemIdentifier Identifier of the category system to
+ * edit.
+ *
+ * @return The template to use.
+ */
+ @GET
+ @Path("/{categorySystemIdentifier}/edit")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editCategorySystem(
+ @PathParam("categorySystemIdentifier")
+ final String categorySystemIdentifier
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ categorySystemDetailsModel.setCategorySystem(result.get());
+ return "org/libreccm/ui/admin/categories/categorysystem-form.xhtml";
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Delete a category system and all its categories.
+ *
+ * @param categorySystemIdentifier Identifier of the category system to
+ * delete.
+ * @param confirmed Was the deletion confirmed by the user?
+ *
+ * @return Redirect to the categorysystems overview.
+ */
+ @POST
+ @Path("/{categorySystemIdentifier}/delete")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String deleteCategorySystem(
+ @PathParam("categorySystemIdentifier")
+ final String categorySystemIdentifier,
+ @FormParam("confirmed") final String confirmed
+ ) {
+ if (Objects.equals(confirmed, "true")) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ domainRepository.delete(result.get());
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+ return "redirect:categorymanager/categorysystems";
+ }
+
+ /**
+ * Adds a localized title the a category system.
+ *
+ * @param categorySystemIdentifier Identifier of the category system.
+ * @param localeParam The locale of the title.
+ * @param value The localized title.
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path("/{identifier}/title/add")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addTitle(
+ @PathParam("identifier") final String categorySystemIdentifier,
+ @FormParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Domain domain = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ domain.getTitle().addValue(locale, value);
+ domainRepository.save(domain);
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Updates a localized title the a category system.
+ *
+ * @param categorySystemIdentifier Identifier of the category system.
+ * @param localeParam The locale of the title.
+ * @param value The localized title.
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path("/{identifier}/title/${locale}/edit")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editTitle(
+ @PathParam("identifier") final String categorySystemIdentifier,
+ @PathParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Domain domain = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ domain.getTitle().addValue(locale, value);
+ domainRepository.save(domain);
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Removes a localized title the a category system.
+ *
+ * @param categorySystemIdentifier Identifier of the category system.
+ * @param localeParam The locale of the title.
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path("/{identifier}/title/${locale}/remove")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeTitle(
+ @PathParam("identifier") final String categorySystemIdentifier,
+ @PathParam("locale") final String localeParam,
+ @FormParam("confirmed")
+ final String confirmed
+ ) {
+
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Domain domain = result.get();
+
+ if (Objects.equals(confirmed, "true")) {
+ final Locale locale = new Locale(localeParam);
+ domain.getTitle().removeValue(locale);
+ domainRepository.save(domain);
+ }
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Adds a localized description the a category system.
+ *
+ * @param categorySystemIdentifier Identifier of the category system.
+ * @param localeParam The locale of the description.
+ * @param value The localized description.
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path("/{identifier}/description/add")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addDescription(
+ @PathParam("identifier") final String categorySystemIdentifier,
+ @FormParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Domain domain = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ domain.getDescription().addValue(locale, value);
+ domainRepository.save(domain);
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Updates a localized description the a category system.
+ *
+ * @param categorySystemIdentifier Identifier of the category system.
+ * @param localeParam The locale of the description.
+ * @param value The localized description.
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path(
+ "categorysystems/{identifier}/description/${locale}/edit"
+ )
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editDescription(
+ @PathParam("identifier") final String categorySystemIdentifier,
+ @PathParam("locale") final String localeParam,
+ @FormParam("value") final String value
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Domain domain = result.get();
+
+ final Locale locale = new Locale(localeParam);
+ domain.getDescription().addValue(locale, value);
+ domainRepository.save(domain);
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Removes a localized description of a category system.
+ *
+ * @param categorySystemIdentifier Identifier of the category system.
+ * @param localeParam The locale of the description.
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path(
+ "categorysystems/{identifier}/description/${locale}/remove")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeDescription(
+ @PathParam("identifier") final String categorySystemIdentifier,
+ @PathParam("locale") final String localeParam,
+ @FormParam("confirmed")
+ final String confirmed
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional result;
+ switch (identifier.getType()) {
+ case ID:
+ result = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ result = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ result = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (result.isPresent()) {
+ final Domain domain = result.get();
+
+ if (Objects.equals(confirmed, "true")) {
+ final Locale locale = new Locale(localeParam);
+ domain.getDescription().removeValue(locale);
+ domainRepository.save(domain);
+ }
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Adds an owner to a category system.
+ *
+ * @param categorySystemIdentifier Identifier of teh category system.
+ * @param applicationUuid UUID of the new owner.
+ * @param context An optional context.
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path("/{categorySystemIdentifier}/owners/add")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addOwner(
+ @PathParam("categorySystemIdentifier")
+ final String categorySystemIdentifier,
+ @FormParam("applicationUuid") final String applicationUuid,
+ @FormParam("context") final String context
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional domainResult;
+ switch (identifier.getType()) {
+ case ID:
+ domainResult = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ domainResult = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ domainResult = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (domainResult.isPresent()) {
+ final Domain domain = domainResult.get();
+
+ final Optional appResult = applicationRepository
+ .findByUuid(applicationUuid);
+ if (!appResult.isPresent()) {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.add_owner.not_found.message",
+ Arrays.asList(applicationRepository)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/application-not-found.xhtml";
+ }
+
+ final CcmApplication owner = appResult.get();
+ if (context == null
+ || context.isEmpty()
+ || context.matches("\\s*")) {
+ domainManager.addDomainOwner(owner, domain);
+ } else {
+ domainManager.addDomainOwner(owner, domain, context);
+ }
+
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+ /**
+ * Remove an owner from a category system.
+ *
+ * @param categorySystemIdentifier Identifier of teh category system.
+ * @param applicationUuid UUID of the owner to remove.
+ * @param confirmed Was the deletion confirmed by the user?
+ *
+ * @return Redirect to the details page of the category system.
+ */
+ @POST
+ @Path("/{categorySystemIdentifier}/owners/${applicationUuid}/remove")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeOwner(
+ @PathParam("categorySystemIdentifier")
+ final String categorySystemIdentifier,
+ @PathParam("applicationUuid") final String applicationUuid,
+ @FormParam("confirmed") final String confirmed
+ ) {
+ final Identifier identifier = identifierParser.parseIdentifier(
+ categorySystemIdentifier
+ );
+ final Optional domainResult;
+ switch (identifier.getType()) {
+ case ID:
+ domainResult = domainRepository.findById(
+ Long.parseLong(identifier.getIdentifier()
+ )
+ );
+ break;
+ case UUID:
+ domainResult = domainRepository.findByUuid(
+ identifier.getIdentifier()
+ );
+ break;
+ default:
+ domainResult = domainRepository.findByDomainKey(
+ identifier.getIdentifier()
+ );
+ break;
+ }
+
+ if (domainResult.isPresent()) {
+ final Domain domain = domainResult.get();
+
+ final Optional appResult = applicationRepository
+ .findByUuid(applicationUuid);
+ if (!appResult.isPresent()) {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.add_owner.not_found.message",
+ Arrays.asList(applicationRepository)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/application-not-found.xhtml";
+ }
+
+ if (Objects.equals(confirmed, "true")) {
+ final CcmApplication owner = appResult.get();
+ domainManager.removeDomainOwner(owner, domain);
+ }
+
+ return String.format(
+ "redirect:categorymanager/categorysystems/ID-%d/details",
+ domain.getObjectId()
+ );
+ } else {
+ categorySystemDetailsModel.addMessage(
+ new Message(
+ adminMessages.getMessage(
+ "categorysystems.not_found.message",
+ Arrays.asList(categorySystemIdentifier)
+ ),
+ MessageType.WARNING
+ )
+ );
+ return "org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml";
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemsTableModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemsTableModel.java
new file mode 100644
index 000000000..62d56642c
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/CategorySystemsTableModel.java
@@ -0,0 +1,112 @@
+/*
+ * 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.categories;
+
+import org.libreccm.categorization.Domain;
+import org.libreccm.categorization.DomainRepository;
+import org.libreccm.core.CoreConstants;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.transaction.Transactional;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+/**
+ * Model providing the data for the table of category systems.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CategorySystemsTableModel")
+public class CategorySystemsTableModel {
+
+ @Inject
+ private DomainRepository domainRepository;
+
+ /**
+ * Get all available category systems
+ *
+ * @return A list of
+ * {@link org.libreccm.ui.admin.categories.CategorySystemTableRow}
+ * items, one for each available category.
+ */
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ @Transactional
+ public List getCategorySystems() {
+ return domainRepository
+ .findAll()
+ .stream()
+ .map(this::buildTableRow)
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Helper method for building a
+ * {@link org.libreccm.ui.admin.categories.CategorySystemTableRow} instance
+ * for a category system.
+ *
+ * @param domain The domain (category system) to convert.
+ *
+ * @return A {@link org.libreccm.ui.admin.categories.CategorySystemTableRow}
+ * instance for the category.
+ */
+ private CategorySystemTableRow buildTableRow(final Domain domain) {
+ final CategorySystemTableRow row = new CategorySystemTableRow();
+
+ row.setDomainId(domain.getObjectId());
+ row.setDomainKey(domain.getDomainKey());
+ row.setUri(domain.getUri());
+ row.setVersion(domain.getVersion());
+ if (domain.getReleased() != null) {
+ row.setReleased(
+ DateTimeFormatter.ISO_DATE
+ .withZone(ZoneId.systemDefault())
+ .format(domain.getReleased())
+ );
+ }
+ row.setTitle(
+ domain
+ .getTitle()
+ .getValues()
+ .entrySet()
+ .stream()
+ .collect(
+ Collectors.toMap(
+ entry -> entry.getKey().toString(),
+ entry -> entry.getValue()
+ )
+ )
+ );
+ return row;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/DomainNodeModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/DomainNodeModel.java
new file mode 100644
index 000000000..e48aada08
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/DomainNodeModel.java
@@ -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.categories;
+
+import org.libreccm.categorization.Domain;
+
+/**
+ * DTO with the data about a {@link Domain} shown in the UI.
+ *
+ * @author Jens Pelzetter
+ */
+public class DomainNodeModel {
+
+ private long domainId;
+
+ private String uuid;
+
+ private String domainKey;
+
+ public long getDomainId() {
+ return domainId;
+ }
+
+ protected void setDomainId(final long domainId) {
+ this.domainId = domainId;
+ }
+
+ public String getIdentifier() {
+ return String.format("ID-%s", domainId);
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ protected void setUuid(final String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getDomainKey() {
+ return domainKey;
+ }
+
+ protected void setDomainKey(final String domainKey) {
+ this.domainKey = domainKey;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/categories/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/package-info.java
new file mode 100644
index 000000000..8a48ddc23
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/categories/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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
+ */
+/**
+ * UI for managing category systems (domains) and categories.
+ */
+package org.libreccm.ui.admin.categories;
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationController.java
new file mode 100644
index 000000000..1a6028cc6
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationController.java
@@ -0,0 +1,106 @@
+/*
+ * 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.configuration;
+
+import org.libreccm.configuration.ConfigurationInfo;
+import org.libreccm.configuration.ConfigurationManager;
+import org.libreccm.core.CoreConstants;
+import org.libreccm.l10n.GlobalizationHelper;
+import org.libreccm.l10n.LocalizedTextsUtil;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+
+import java.util.List;
+import java.util.Objects;
+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.GET;
+import javax.ws.rs.Path;
+
+/**
+ * Controller for the UI for managing the configuration of CCM.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Controller
+@Path("/configuration")
+public class ConfigurationController {
+
+ @Inject
+ private ConfigurationManager confManager;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ @Inject
+ private Models models;
+
+ /**
+ * Show all available configurations (groups of settings).
+ *
+ * @return The template to use.
+ */
+ @GET
+ @Path("/")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String getSettings() {
+ final List configurationClasses = confManager
+ .findAllConfigurations()
+ .stream()
+ .map(confManager::getConfigurationInfo)
+ .map(this::buildTableEntry)
+ .sorted()
+ .collect(Collectors.toList());
+
+ models.put("configurationClasses", configurationClasses);
+
+ return "org/libreccm/ui/admin/configuration/configuration.xhtml";
+ }
+
+ /**
+ * Helper method for converting a
+ * {@link org.libreccm.configuration.ConfigurationInfo} instance into a
+ * {@link org.libreccm.ui.admin.configuration.ConfigurationTableEntry}
+ * instance.
+ *
+ * @param confInfo Configuration info to convert.
+ *
+ * @return A {@link ConfigurationTableEntry} for the configuration.
+ */
+ private ConfigurationTableEntry buildTableEntry(
+ final ConfigurationInfo confInfo
+ ) {
+ Objects.requireNonNull(confInfo);
+ final ConfigurationTableEntry entry = new ConfigurationTableEntry();
+ entry.setName(confInfo.getName());
+ final LocalizedTextsUtil util = globalizationHelper
+ .getLocalizedTextsUtil(confInfo.getDescBundle());
+ entry.setTitle(util.getText(confInfo.getTitleKey()));
+ entry.setDescription(util.getText(confInfo.getDescKey()));
+
+ return entry;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationPage.java
new file mode 100644
index 000000000..1b0020b94
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationPage.java
@@ -0,0 +1,83 @@
+/*
+ * 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.configuration;
+
+import org.libreccm.ui.admin.AdminConstants;
+import org.libreccm.ui.admin.AdminPage;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.enterprise.context.ApplicationScoped;
+
+/**
+ * {@link AdminPage} implementation providing the UI for managing the
+ * configuration of CCM.
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+public class ConfigurationPage implements AdminPage {
+
+ @Override
+ public Set> getControllerClasses() {
+ final Set> classes = new HashSet<>();
+ classes.add(ConfigurationController.class);
+ classes.add(SettingsController.class);
+ return classes;
+ }
+
+ @Override
+ public String getUriIdentifier() {
+ return String.format(
+ "%s#getSettings", ConfigurationController.class.getSimpleName()
+ );
+ }
+
+ @Override
+ public String getLabelBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getLabelKey() {
+ return "configuration.label";
+ }
+
+ @Override
+ public String getDescriptionBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getDescriptionKey() {
+ return "configuration.description";
+ }
+
+ @Override
+ public String getIcon() {
+ return "gear-fill";
+ }
+
+ @Override
+ public int getPosition() {
+ return 30;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationTableEntry.java b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationTableEntry.java
new file mode 100644
index 000000000..b713dfd9e
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/ConfigurationTableEntry.java
@@ -0,0 +1,78 @@
+/*
+ * 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.configuration;
+
+import java.util.Objects;
+
+/**
+ * A row in the table of available configurations.
+ *
+ * @author Jens Pelzetter
+ */
+public class ConfigurationTableEntry
+ implements Comparable {
+
+ private String name;
+
+ private String title;
+
+ private String description;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(final String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ @Override
+ public int compareTo(final ConfigurationTableEntry other) {
+ int result = Objects.compare(
+ title,
+ Objects.requireNonNull(other).getTitle(),
+ String::compareTo
+ );
+ if (result == 0) {
+ result = Objects.compare(
+ name,
+ Objects.requireNonNull(other).getName(),
+ String::compareTo
+ );
+ }
+ return result;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/SettingsController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/SettingsController.java
new file mode 100644
index 000000000..04b17048b
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/SettingsController.java
@@ -0,0 +1,635 @@
+/*
+ * 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.configuration;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.libreccm.configuration.ConfigurationInfo;
+import org.libreccm.configuration.ConfigurationManager;
+import org.libreccm.configuration.SettingInfo;
+import org.libreccm.configuration.SettingManager;
+import org.libreccm.core.CoreConstants;
+import org.libreccm.l10n.GlobalizationHelper;
+import org.libreccm.l10n.LocalizedString;
+import org.libreccm.l10n.LocalizedTextsUtil;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Controller;
+import javax.mvc.Models;
+import javax.transaction.Transactional;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+/**
+ * Controller for the UI for viewing and editing settings.
+ *
+ * @author Jens Pelzetter
+ */
+@Controller
+@RequestScoped
+@Path("/configuration/{configurationClass}")
+public class SettingsController {
+
+ private static final Logger LOGGER = LogManager.getLogger(
+ SettingsController.class
+ );
+
+ @Inject
+ private ConfigurationManager confManager;
+
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ @Inject
+ private Models models;
+
+ @Inject
+ private SettingManager settingManager;
+
+ /**
+ * Show all settings of a configuration.
+ *
+ * @param configurationClass The configuration class
+ * @return The template to use.
+ */
+ @GET
+ @Path("/")
+ @Transactional(Transactional.TxType.REQUIRED)
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String showSettings(
+ @PathParam("configurationClass") final String configurationClass
+ ) {
+ final Class> confClass;
+ try {
+ confClass = Class.forName(configurationClass);
+ } catch (ClassNotFoundException ex) {
+ models.put("configurationClass", configurationClass);
+ return "org/libreccm/ui/admin/configuration/configuration-class-not-found.xhtml";
+ }
+
+ final ConfigurationInfo confInfo = confManager.getConfigurationInfo(
+ confClass
+ );
+
+ final LocalizedTextsUtil textUtil = globalizationHelper
+ .getLocalizedTextsUtil(confInfo.getDescBundle());
+ models.put(
+ "confLabel",
+ textUtil.getText(confInfo.getTitleKey())
+ );
+ models.put(
+ "configurationDesc",
+ textUtil.getText(confInfo.getDescKey())
+ );
+
+ final Object configuration = confManager.findConfiguration(confClass);
+
+ final List settings = confInfo
+ .getSettings()
+ .entrySet()
+ .stream()
+ .map(Map.Entry::getValue)
+ .map(settingInfo -> buildSettingsTableEntry(settingInfo,
+ configuration))
+ .sorted()
+ .collect(Collectors.toList());
+
+ models.put("configurationClass", configurationClass);
+
+ models.put("settings", settings);
+
+ models.put("BigDecimalClassName", BigDecimal.class.getName());
+ models.put("BooleanClassName", Boolean.class.getName());
+ models.put("DoubleClassName", Double.class.getName());
+ models.put("FloatClassName", Float.class.getName());
+ models.put("IntegerClassName", Integer.class.getName());
+ models.put("ListClassName", List.class.getName());
+ models.put("LongClassName", Long.class.getName());
+ models.put("LocalizedStringClassName", LocalizedString.class.getName());
+ models.put("SetClassName", Set.class.getName());
+ models.put("StringClassName", String.class.getName());
+
+ models.put("IntegerMaxValue", Integer.toString(Integer.MAX_VALUE));
+ models.put("IntegerMinValue", Integer.toString(Integer.MIN_VALUE));
+ models.put("LongMaxValue", Long.toString(Long.MAX_VALUE));
+ models.put("LongMinValue", Long.toString(Long.MIN_VALUE));
+ models.put("DoubleMaxValue", Double.toString(Double.MAX_VALUE));
+ models.put("DoubleMinValue", Double.toString(Double.MIN_VALUE));
+
+ return "org/libreccm/ui/admin/configuration/settings.xhtml";
+ }
+
+ /**
+ * Helper method for building a {@link SettingsTableEntry} for a setting.
+ *
+ * @param settingInfo The setting to convert.
+ * @param configuration The configuration to which the settings belongs.
+ * @return A {@link SettingsTableEntry} for the setting.
+ */
+ @Transactional(Transactional.TxType.REQUIRED)
+ private SettingsTableEntry buildSettingsTableEntry(
+ final SettingInfo settingInfo,
+ final Object configuration
+ ) {
+ Objects.requireNonNull(settingInfo);
+ Objects.requireNonNull(configuration);
+
+ final LocalizedTextsUtil textsUtil = globalizationHelper
+ .getLocalizedTextsUtil(settingInfo.getDescBundle());
+
+ String value;
+ try {
+ final Field field = configuration
+ .getClass()
+ .getDeclaredField(settingInfo.getName());
+ field.setAccessible(true);
+ final Object valueObj = field.get(configuration);
+ if (valueObj instanceof List) {
+ @SuppressWarnings("unchecked")
+ final List list = (List) valueObj;
+ value = list
+ .stream()
+ .collect(Collectors.joining("\n"));
+ } else if (valueObj instanceof LocalizedString) {
+ final LocalizedString localizedStr = (LocalizedString) valueObj;
+ value = localizedStr
+ .getValues()
+ .entrySet()
+ .stream()
+ .map(
+ entry -> String.format(
+ "%s: %s",
+ entry.getKey().toString(), entry.getValue()
+ )
+ )
+ .sorted()
+ .collect(Collectors.joining("\n"));
+ } else if (valueObj instanceof Set) {
+ @SuppressWarnings("unchecked")
+ final Set set = (Set) valueObj;
+ value = set
+ .stream()
+ .collect(Collectors.joining("\n"));
+ } else {
+ value = Objects.toString(valueObj);
+ }
+ } catch (NoSuchFieldException | IllegalAccessException | SecurityException ex) {
+ LOGGER.error(
+ "Failed to get value for field {} of configuration {}.",
+ settingInfo.getName(),
+ configuration.getClass().getName()
+ );
+ LOGGER.error(ex);
+ value = "?err?";
+ }
+ final SettingsTableEntry entry = new SettingsTableEntry();
+ entry.setName(settingInfo.getName());
+ entry.setValue(value);
+ entry.setValueType(settingInfo.getValueType());
+ entry.setDefaultValue(settingInfo.getDefaultValue());
+ entry.setLabel(textsUtil.getText(settingInfo.getLabelKey()));
+ entry.setDescription(textsUtil.getText(settingInfo.getDescKey()));
+
+ return entry;
+ }
+
+ @POST
+ @Path("/{settingName}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String updateSettingValue(
+ @PathParam("configurationClass")
+ final String configurationClassName,
+ @PathParam("settingName")
+ final String settingName,
+ @FormParam("settingValue")
+ final String valueParam
+ ) {
+ final Class> confClass;
+ try {
+ confClass = Class.forName(configurationClassName);
+ } catch (ClassNotFoundException ex) {
+ models.put("configurationClass", configurationClassName);
+ return "org/libreccm/ui/admin/configuration/configuration-class-not-found.xhtml";
+ }
+ final Object conf = confManager.findConfiguration(confClass);
+ final SettingInfo settingInfo = settingManager.getSettingInfo(
+ confClass, settingName
+ );
+
+ final String valueType = settingInfo.getValueType();
+ if (valueType.equals(BigDecimal.class.getName())) {
+ return updateBigDecimalSetting(
+ configurationClassName,
+ confClass,
+ conf,
+ settingName,
+ valueType,
+ valueParam
+ );
+ } else if (valueType.equals(Boolean.class.getName())
+ || valueType.equals("boolean")) {
+ final boolean value = valueParam != null;
+ return updateBooleanSetting(
+ configurationClassName,
+ confClass,
+ conf,
+ settingName,
+ valueType,
+ value
+ );
+ } else if (valueType.equals(Double.class.getName())
+ || valueType.equals("double")) {
+ return updateDoubleSetting(
+ configurationClassName,
+ confClass,
+ conf,
+ settingName,
+ valueType,
+ valueParam
+ );
+ } else if (valueType.equals(LocalizedString.class.getName())) {
+ return updateLocalizedStringSetting(
+ configurationClassName,
+ confClass,
+ conf,
+ settingName,
+ valueType,
+ valueParam
+ );
+ } else if (valueType.equals(Long.class.getName())
+ || valueType.equals("long")) {
+ return updateLongSetting(
+ configurationClassName,
+ confClass,
+ conf,
+ settingName,
+ valueType,
+ valueParam
+ );
+ } else if (valueType.equals(List.class.getName())) {
+ return updateStringListSetting(
+ configurationClassName,
+ confClass,
+ conf,
+ settingName,
+ valueType,
+ valueParam
+ );
+ } else if (valueType.equals(Set.class.getName())) {
+ return updateStringSetSetting(
+ configurationClassName,
+ confClass,
+ conf,
+ settingName,
+ valueType,
+ valueParam
+ );
+ } else if (valueType.equals(String.class.getName())) {
+ return updateStringSetting(
+ configurationClassName,
+ confClass,
+ conf,
+ settingName,
+ valueType,
+ valueParam
+ );
+ } else {
+ models.put("configurationClass", configurationClassName);
+ models.put("settingName", settingName);
+ models.put("valueType", valueType);
+ return "org/libreccm/ui/admin/configuration/unsupported-setting-type.xhtml";
+ }
+ }
+
+ @POST
+ @Path("/{settingName}/reset")
+ @Transactional(Transactional.TxType.REQUIRED)
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String resetSettingValue(
+ @PathParam("configurationClass")
+ final String configurationClassName,
+ @PathParam("settingName")
+ final String settingName,
+ @FormParam("confirmed")
+ final String confirmed
+ ) {
+ if ("true".equals(confirmed)) {
+ final Class> configurationClass;
+ try {
+ configurationClass = Class.forName(
+ configurationClassName
+ );
+ } catch (ClassNotFoundException ex) {
+ models.put("configurationClass", configurationClassName);
+ return "org/libreccm/ui/admin/configuration/configuration-class-not-found.xhtml";
+ }
+ final SettingInfo settingInfo = settingManager.getSettingInfo(
+ configurationClass, settingName
+ );
+ return updateSettingValue(
+ configurationClassName,
+ settingName,
+ settingInfo.getDefaultValue()
+ );
+ } else {
+ return buildRedirectAfterUpdateSettingTarget(configurationClassName);
+ }
+ }
+
+ private String updateBigDecimalSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final String valueParam
+ ) {
+ final BigDecimal value;
+ try {
+ value = new BigDecimal(valueParam);
+ } catch (NumberFormatException ex) {
+ return buildInvalidTypeErrorTarget(
+ configurationClassName,
+ settingName,
+ valueType,
+ valueParam
+ );
+ }
+ return updateSetting(
+ configurationClassName,
+ configurationClass,
+ configuration,
+ settingName,
+ valueType,
+ value
+ );
+ }
+
+ private String updateBooleanSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final boolean value
+ ) {
+ return updateSetting(
+ configurationClassName,
+ configurationClass,
+ configuration,
+ settingName,
+ valueType,
+ value
+ );
+ }
+
+ private String updateDoubleSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final String valueParam
+ ) {
+ final Double value;
+ try {
+ value = Double.valueOf(valueParam);
+ } catch (NumberFormatException ex) {
+ return buildInvalidTypeErrorTarget(
+ configurationClassName,
+ settingName,
+ valueType,
+ valueParam
+ );
+ }
+ return updateSetting(
+ configurationClassName,
+ configurationClass,
+ configuration,
+ settingName,
+ valueType,
+ value
+ );
+ }
+
+ private String updateLocalizedStringSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final String valueParam
+ ) {
+ final LocalizedString value = new LocalizedString();
+ final String[] lines = valueParam.split("\n");
+ for (final String line : lines) {
+ final String[] tokens = line.split(":");
+ if (tokens.length != 2) {
+ continue;
+ }
+ final Locale locale = new Locale(tokens[0]);
+ final String localeValue = tokens[1].trim();
+ value.addValue(locale, localeValue);
+ }
+ return updateSetting(
+ configurationClassName,
+ configurationClass,
+ configuration,
+ settingName,
+ valueType,
+ value
+ );
+ }
+
+ private String updateLongSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final String valueParam
+ ) {
+ final Long value;
+ try {
+ value = Long.valueOf(valueParam, 10);
+ } catch (NumberFormatException ex) {
+ return buildInvalidTypeErrorTarget(
+ configurationClassName,
+ settingName,
+ valueType,
+ valueParam
+ );
+ }
+ return updateSetting(
+ configurationClassName,
+ configurationClass,
+ configuration,
+ settingName,
+ valueType,
+ value
+ );
+ }
+
+ private String updateStringListSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final String valueParam
+ ) {
+ final String[] tokens = valueParam.split("\n");
+ final List value = Arrays
+ .asList(tokens)
+ .stream()
+ .map(String::trim)
+ .collect(Collectors.toList());
+ return updateSetting(
+ configurationClassName,
+ configurationClass,
+ configuration,
+ settingName,
+ valueType,
+ value
+ );
+ }
+
+ private String updateStringSetSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final String valueParam
+ ) {
+ final String[] tokens = valueParam.split(",");
+ final Set value = new HashSet<>(Arrays.asList(tokens));
+ return updateSetting(
+ configurationClassName,
+ configurationClass,
+ configuration,
+ settingName,
+ valueType,
+ value
+ );
+ }
+
+ private String updateStringSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final String value
+ ) {
+ return updateSetting(
+ configurationClassName,
+ configurationClass,
+ configuration,
+ settingName,
+ valueType,
+ value
+ );
+ }
+
+ private String updateSetting(
+ final String configurationClassName,
+ final Class> configurationClass,
+ final Object configuration,
+ final String settingName,
+ final String valueType,
+ final Object value
+ ) {
+ try {
+ final Field field = configurationClass.getDeclaredField(
+ settingName
+ );
+ field.setAccessible(true);
+ field.set(configuration, value);
+ confManager.saveConfiguration(configuration);
+ return buildRedirectAfterUpdateSettingTarget(
+ configurationClassName
+ );
+ } catch (NoSuchFieldException ex) {
+ return buildSettingNotFoundErrorTarget(
+ configurationClassName,
+ settingName,
+ valueType);
+ } catch (SecurityException | IllegalAccessException ex) {
+ LOGGER.error("Failed to update setting.", ex);
+ models.put("configurationClass", configurationClassName);
+ models.put("settingName", settingName);
+ return "org/libreccm/ui/admin/configuration/failed-to-update-setting.xhtml";
+ }
+ }
+
+ private String buildInvalidTypeErrorTarget(
+ final String configurationClassName,
+ final String settingName,
+ final String valueType,
+ final String valueParam
+ ) {
+ models.put("configurationClass", configurationClassName);
+ models.put("settingName", settingName);
+ models.put("valueType", valueType);
+ models.put("valueParam", valueParam);
+ return "org/libreccm/ui/admin/configuration/invalid-setting-value.xhtml";
+ }
+
+ private String buildSettingNotFoundErrorTarget(
+ final String configurationClassName,
+ final String settingName,
+ final String valueType
+ ) {
+ models.put("configurationClass", configurationClassName);
+ models.put("settingName", settingName);
+ models.put("valueType", valueType);
+ return "org/libreccm/ui/admin/configuration/setting-not-found.xhtml";
+ }
+
+ private String buildRedirectAfterUpdateSettingTarget(
+ final String configurationClassName
+ ) {
+ return String.format(
+ "redirect:configuration/%s", configurationClassName
+ );
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/SettingsTableEntry.java b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/SettingsTableEntry.java
new file mode 100644
index 000000000..bf723f1dd
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/SettingsTableEntry.java
@@ -0,0 +1,107 @@
+/*
+ * 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.configuration;
+
+import java.util.Objects;
+
+/**
+ * Data for row in the table of settings of a configuration.
+ *
+ * @author Jens Pelzetter
+ */
+public class SettingsTableEntry implements Comparable {
+
+ private String name;
+
+ private String valueType;
+
+ private String defaultValue;
+
+ private String value;
+
+ private String label;
+
+ private String description;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public String getValueType() {
+ return valueType;
+ }
+
+ public void setValueType(final String valueType) {
+ this.valueType = valueType;
+ }
+
+ public String getDefaultValue() {
+ return defaultValue;
+ }
+
+ public void setDefaultValue(final String defaultValue) {
+ this.defaultValue = defaultValue;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(final String label) {
+ this.label = label;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(final String description) {
+ this.description = description;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ @Override
+ public int compareTo(final SettingsTableEntry other) {
+ int result = Objects.compare(
+ label,
+ Objects.requireNonNull(other).getLabel(),
+ String::compareTo
+ );
+ if (result == 0) {
+ result = Objects.compare(
+ name,
+ other.getName(),
+ String::compareTo
+ );
+ }
+ return result;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/package-info.java
new file mode 100644
index 000000000..b17aa4980
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/configuration/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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
+ */
+/**
+ * UI for editing the configuration of LibreCCM.
+ */
+package org.libreccm.ui.admin.configuration;
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/DashboardController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/DashboardController.java
new file mode 100644
index 000000000..efadf444b
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/DashboardController.java
@@ -0,0 +1,52 @@
+/*
+ * 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.dashboard;
+
+import org.libreccm.core.CoreConstants;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+
+import javax.enterprise.context.RequestScoped;
+import javax.mvc.Controller;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+/**
+ * Controller for the dashboard page (start page) of the Admin UI:
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Controller
+@Path("/")
+public class DashboardController {
+
+ /**
+ * Show the dashboard page.
+ *
+ * @return The template to use.
+ */
+ @GET
+ @Path("/")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String getDashboard() {
+ return "org/libreccm/ui/admin/dashboard.xhtml";
+ }
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/DashboardPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/DashboardPage.java
new file mode 100644
index 000000000..42dde7292
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/DashboardPage.java
@@ -0,0 +1,81 @@
+/*
+ * 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.dashboard;
+
+import org.libreccm.ui.admin.AdminConstants;
+import org.libreccm.ui.admin.AdminPage;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.enterprise.context.ApplicationScoped;
+
+/**
+ * {@link AdminPage} implementation for the dashboard page.
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+public class DashboardPage implements AdminPage {
+
+ @Override
+ public Set> getControllerClasses() {
+ final Set> classes = new HashSet<>();
+ classes.add(DashboardController.class);
+ return classes;
+ }
+
+ @Override
+ public String getUriIdentifier() {
+ return String.format(
+ "%s#getDashboard", DashboardController.class.getSimpleName()
+ );
+ }
+
+ @Override
+ public String getLabelBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getLabelKey() {
+ return "dashboard.label";
+ }
+
+ @Override
+ public String getDescriptionBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getDescriptionKey() {
+ return "dashboard.description";
+ }
+
+ @Override
+ public String getIcon() {
+ return "house-fill";
+ }
+
+ @Override
+ public int getPosition() {
+ return 0;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/package-info.java
new file mode 100644
index 000000000..362349c53
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/dashboard/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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
+ */
+/**
+ * Start page of the Admin UI.
+ */
+package org.libreccm.ui.admin.dashboard;
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ExportTask.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ExportTask.java
new file mode 100644
index 000000000..b4fb8d8c2
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ExportTask.java
@@ -0,0 +1,84 @@
+/*
+ * 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 java.time.LocalDate;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Data for an export task.
+ *
+ * @author Jens Pelzetter
+ */
+public class ExportTask {
+
+ /**
+ * Name of the export archive.
+ */
+ private final String name;
+
+ /**
+ * When was the export task started?
+ */
+ private final LocalDate started;
+
+ /**
+ * The entities to export.
+ */
+ private final Collection entities;
+
+ /**
+ * The status of the export task.
+ */
+ private final ExportTaskStatus status;
+
+ public ExportTask(
+ final String name,
+ final LocalDate started,
+ final Collection entities,
+ final ExportTaskStatus status
+ ) {
+ this.name = name;
+ this.started = started;
+ this.entities = entities;
+ this.status = status;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public LocalDate getStarted() {
+ return started;
+ }
+
+ public Collection getEntities() {
+ return Collections.unmodifiableCollection(entities);
+ }
+
+ public ExportTaskStatus getStatus() {
+ return status;
+ }
+
+
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ExportTaskStatus.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ExportTaskStatus.java
new file mode 100644
index 000000000..0a1168e1b
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ExportTaskStatus.java
@@ -0,0 +1,153 @@
+/*
+ * 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.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * Status of an export task.
+ *
+ * @author Jens Pelzetter
+ */
+public class ExportTaskStatus implements Comparable {
+
+ /**
+ * Name of the export archive.
+ */
+ private String name;
+
+ /**
+ * When was the task started?
+ */
+ private LocalDateTime started;
+
+ /**
+ * Status of the export task.
+ */
+ private ImExportTaskStatus status;
+
+ /**
+ * If the proces throw an exception, it is stored here.
+ */
+ private Throwable exception;
+
+ public String getName() {
+ return name;
+ }
+
+ protected void setName(final String name) {
+ this.name = name;
+ }
+
+ public LocalDateTime getStarted() {
+ return started;
+ }
+
+ protected void setStarted(final LocalDateTime started) {
+ this.started = started;
+ }
+
+ public ImExportTaskStatus getStatus() {
+ return status;
+ }
+
+ protected void setStatus(final ImExportTaskStatus 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 = 97 * hash + Objects.hashCode(name);
+ hash = 97 * hash + Objects.hashCode(started);
+ hash = 97 * 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 ExportTaskStatus)) {
+ return false;
+ }
+ final ExportTaskStatus other = (ExportTaskStatus) 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) {
+ return obj instanceof ExportTaskStatus;
+ }
+
+ @Override
+ public int compareTo(final ExportTaskStatus other) {
+ return Comparator
+ .nullsFirst(Comparator
+ .comparing(ExportTaskStatus::getName)
+ .thenComparing(ExportTaskStatus::getStarted)
+ .thenComparing(ExportTaskStatus::getStatus)
+ )
+ .compare(this, other);
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "%s{ "
+ + "name = %s, "
+ + "started = %s, "
+ + "status = %s, "
+ + "expection = %s"
+ + " }",
+ super.toString(),
+ name,
+ DateTimeFormatter.ISO_DATE_TIME.withZone(
+ ZoneId.systemDefault()
+ ).format(started),
+ Objects.toString(status),
+ Objects.toString(exception)
+ );
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportController.java
new file mode 100644
index 000000000..7afe02994
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportController.java
@@ -0,0 +1,279 @@
+/*
+ * 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.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.imexport.ImportManifest;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.RequiresPrivilege;
+
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+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.POST;
+import javax.ws.rs.Path;
+
+/**
+ * Controller for the Import/Export UI.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Controller
+@Path("/imexport")
+public class ImExportController {
+
+ @Inject
+ private ImportExport importExport;
+
+ @Inject
+ private ImportExportTaskManager taskManager;
+
+ @Inject
+ private Models models;
+
+ /**
+ * Provides the main page with an overview of all running import/export
+ * processes.
+ *
+ * @return
+ */
+ @GET
+ @Path("/")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String getImExportDashboard() {
+ return "org/libreccm/ui/admin/imexport/imexport.xhtml";
+ }
+
+ /**
+ * UI for starting exports.
+ *
+ * @return The template to use.
+ */
+ @GET
+ @Path("/export")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String exportEntities() {
+ models.put(
+ "exportableEntities",
+ importExport
+ .getExportableEntityTypes()
+ .stream()
+ .map(EntityImExporterTreeNode::getEntityImExporter)
+ .map(AbstractEntityImExporter::getEntityClass)
+ .map(Class::getName)
+ .sorted()
+ .collect(
+ Collectors.toMap(
+ clazz -> clazz,
+ clazz -> clazz,
+ this::noDuplicateKeys,
+ TreeMap::new
+ )
+ )
+ //.collect(Collectors.toList())
+ );
+
+ return "org/libreccm/ui/admin/imexport/export.xhtml";
+ }
+
+ /**
+ * Starts an export.
+ *
+ * @param selectedEntitiesParam The entity types selected for export.
+ * @param exportName The name of the export archive.
+ *
+ * @return Redirect to the main import/export page.
+ */
+ @POST
+ @Path("/export")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String exportEntities(
+ @FormParam("selectedEntities") final String[] selectedEntitiesParam,
+ @FormParam("exportName") final String exportName
+ ) {
+ final Set selectedEntities = Arrays
+ .stream(selectedEntitiesParam)
+ .collect(Collectors.toSet());
+
+ final Set selectedNodes = importExport
+ .getExportableEntityTypes()
+ .stream()
+ .filter(
+ node -> selectedEntities.contains(
+ node.getEntityImExporter().getEntityClass().getName()
+ )
+ )
+ .collect(Collectors.toSet());
+
+ final Set exportNodes = addRequiredEntities(
+ new HashSet<>(selectedNodes)
+ );
+
+ final Set> exportTypes = exportNodes
+ .stream()
+ .map(node -> node.getEntityImExporter().getEntityClass())
+ .collect(Collectors.toSet());
+
+ taskManager.exportEntities(exportTypes, exportName);
+
+ return "redirect:imexport";
+ }
+
+ /**
+ * Displays the import page that allows to select a import archive.
+ *
+ * @return The template to use.
+ */
+ @GET
+ @Path("/import")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String importEntities() {
+ 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";
+ }
+
+ /**
+ * Execute an import.
+ *
+ * @param importArchive The name of the import archive to use.
+ *
+ * @return Redirect to to the main import/export page.
+ */
+ @POST
+ @Path("/import")
+ @AuthorizationRequired
+ @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
+ public String importEntities(
+ @FormParam("archive") final String importArchive
+ ) {
+ taskManager.importEntities(importArchive);
+
+ return "redirect:imexport";
+ }
+
+ /**
+ * Merge function for {@link Collectors#toMap(java.util.function.Function, java.util.function.Function, java.util.function.BinaryOperator, java.util.function.Supplier).
+ *
+ * @param str1 First key
+ * @param str2 Second key
+ *
+ * @return First key.
+ *
+ * @throws RuntimeException if both keys are equal.
+ */
+ private String noDuplicateKeys(final String str1, final String str2) {
+ if (str1.equals(str2)) {
+ throw new RuntimeException("No duplicate keys allowed.");
+ } else {
+ return str1;
+ }
+ }
+
+ /**
+ * Helper method for adding required entities to an export task. Some entity
+ * types require also other entity types. This method traverses through the
+ * selected entity types of an export and adds required entity types if
+ * necessary.
+ *
+ * @param selectedNodes The selected entity types.
+ *
+ * @return The final list of exported types.
+ */
+ private Set addRequiredEntities(
+ final Set selectedNodes
+ ) {
+ boolean foundRequiredNodes = false;
+ final Set 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;
+ }
+ }
+
+ /**
+ * Helper function to build an
+ * {@link org.libreccm.ui.admin.imexport.ImportOption} instance from a
+ * {@link org.libreccm.imexport.ImportManifest}.
+ *
+ * @param manifest The manifest to map to a
+ * {@link org.libreccm.ui.admin.imexport.ImportOption}.
+ *
+ * @return An {@link org.libreccm.ui.admin.imexport.ImportOption} instance.
+ */
+ 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(", "))
+ )
+ );
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportPage.java
new file mode 100644
index 000000000..2b00defd0
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportPage.java
@@ -0,0 +1,79 @@
+/*
+ * 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.ui.admin.AdminConstants;
+import org.libreccm.ui.admin.AdminPage;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.enterprise.context.ApplicationScoped;
+
+/**
+ * Provides the UI for importing and exporting entities.
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+public class ImExportPage implements AdminPage {
+ @Override
+ public Set> getControllerClasses() {
+ final Set> classes = new HashSet<>();
+ classes.add(ImExportController.class);
+ return classes;
+ }
+
+ @Override
+ public String getUriIdentifier() {
+ return String.format(
+ "%s#getImExportDashboard", ImExportController.class.getSimpleName()
+ );
+ }
+
+ @Override
+ public String getLabelBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getLabelKey() {
+ return "imexport.label";
+ }
+
+ @Override
+ public String getDescriptionBundle() {
+ return AdminConstants.ADMIN_BUNDLE;
+ }
+
+ @Override
+ public String getDescriptionKey() {
+ return "imexport.description";
+ }
+
+ @Override
+ public String getIcon() {
+ return "arrow-left-right";
+ }
+
+ @Override
+ public int getPosition() {
+ return 60;
+ }
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportTaskStatus.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportTaskStatus.java
new file mode 100644
index 000000000..9b9b4de8e
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportTaskStatus.java
@@ -0,0 +1,41 @@
+/*
+ * 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;
+
+/**
+ * Enumeration for the possible states of an export or import task.
+ *
+ * @author Jens Pelzetter
+ */
+public enum ImExportTaskStatus {
+
+ /**
+ * An error occured during the process.
+ */
+ ERROR,
+ /**
+ * The import or export task is finished.
+ */
+ FINISHED,
+ /**
+ * The task is still running.
+ */
+ RUNNING,
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportTasks.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportTasks.java
new file mode 100644
index 000000000..4e7722729
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImExportTasks.java
@@ -0,0 +1,72 @@
+/*
+ * 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 javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.event.ObservesAsync;
+import javax.inject.Inject;
+import javax.transaction.Transactional;
+
+/**
+ * Listens for CDI events fired by {@link org.libreccm.ui.admin.imexport.ImportExportTaskManager}
+ * and executes tasks.
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+public class ImExportTasks {
+
+ @Inject
+ private ImportExport importExport;
+
+ /**
+ * Listens for {@link org.libreccm.ui.admin.imexport.ExportTask}s.
+ *
+ * @param task The task to execute.
+ * @return The task.
+ */
+ @Transactional(Transactional.TxType.REQUIRED)
+ public ExportTask exportEntities(@ObservesAsync final ExportTask task) {
+ final Collection entities = task.getEntities();
+ final String exportName = task.getName();
+
+ importExport.exportEntities(entities, exportName);
+ task.getStatus().setStatus(ImExportTaskStatus.FINISHED);
+ return task;
+ }
+
+ /**
+ * Listens for {@link org.libreccm.ui.admin.imexport.ImportTask}s.
+ *
+ * @param task The task to execute.
+ * @return The task.
+ */
+ @Transactional(Transactional.TxType.REQUIRED)
+ public void importEntitites(@ObservesAsync final ImportTask task) {
+ final String importName = task.getName();
+
+ importExport.importEntities(importName);
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportExportTaskManager.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportExportTaskManager.java
new file mode 100644
index 000000000..23a93d3b9
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportExportTaskManager.java
@@ -0,0 +1,252 @@
+/*
+ * 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.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.libreccm.imexport.Exportable;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+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.CompletionStage;
+
+import javax.ejb.Schedule;
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.event.Event;
+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;
+
+/**
+ * Provides the backend for importing and exporting entities.
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+@Named("ImportExportTaskManager")
+public class ImportExportTaskManager {
+
+ private static final Logger LOGGER = LogManager.getLogger(
+ ImportExportTaskManager.class
+ );
+
+ /**
+ * Entity manager used for some special queries. To execute import and
+ * export tasks concurrently CDI events are used which are processed by the
+ * {@link ImExportTasks} class.
+ */
+ @Inject
+ private EntityManager entityManager;
+
+ /**
+ * CDI event sender for export tasks.
+ */
+ @Inject
+ private Event exportTaskSender;
+
+ /**
+ * CDI event sender for import tasks.
+ */
+ @Inject
+ private Event importTaskSender;
+
+ /**
+ * Status of all active export tasks.
+ */
+ private final SortedSet exportTasks;
+
+ /**
+ * Status of all active import tasks.
+ */
+ private final SortedSet importTasks;
+
+ public ImportExportTaskManager() {
+ exportTasks = new TreeSet<>(
+ Comparator.comparing(
+ ExportTaskStatus::getStarted)
+ .thenComparing(ExportTaskStatus::getName)
+ );
+ importTasks = new TreeSet<>(
+ Comparator.comparing(
+ ImportTaskStatus::getStarted)
+ .thenComparing(ImportTaskStatus::getName)
+ );
+ }
+
+ /**
+ * Returns the active export tasks.
+ *
+ * @return All active export tasks.
+ */
+ public SortedSet getExportTasks() {
+ return Collections.unmodifiableSortedSet(exportTasks);
+ }
+
+ /**
+ * Returns the active import tasks.
+ *
+ * @return All active import tasks.
+ */
+ public SortedSet