diff --git a/.gitignore b/.gitignore index 56e4e14b7..144b13113 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ target .settings .tscache *.vscode +/ccm-core/nbproject/ diff --git a/ccm-bundle-devel-wildfly/faces-config.NavData b/ccm-bundle-devel-wildfly/faces-config.NavData new file mode 100644 index 000000000..298bfc50a --- /dev/null +++ b/ccm-bundle-devel-wildfly/faces-config.NavData @@ -0,0 +1,6 @@ + + + + + + diff --git a/ccm-bundle-devel-wildfly/pom.xml b/ccm-bundle-devel-wildfly/pom.xml index fe0fefee9..3fb923da7 100644 --- a/ccm-bundle-devel-wildfly/pom.xml +++ b/ccm-bundle-devel-wildfly/pom.xml @@ -213,7 +213,15 @@ ccm-core jar - views/ + WEB-INF/ + + + + org.libreccm + ccm-shortcuts + jar + + WEB-INF/ @@ -224,6 +232,14 @@ resources/ + + org.librecms + ccm-cms + jar + + WEB-INF/ + + org.librecms ccm-cms diff --git a/ccm-bundle-devel-wildfly/src/main/resources/log4j2.xml b/ccm-bundle-devel-wildfly/src/main/resources/log4j2.xml index ad3d929fb..e5dac78bf 100644 --- a/ccm-bundle-devel-wildfly/src/main/resources/log4j2.xml +++ b/ccm-bundle-devel-wildfly/src/main/resources/log4j2.xml @@ -101,5 +101,11 @@ + + + + diff --git a/ccm-bundle-devel-wildfly/src/main/webapp/WEB-INF/jboss-web.xml b/ccm-bundle-devel-wildfly/src/main/webapp/WEB-INF/jboss-web.xml index 3b2f84d32..5cf554701 100644 --- a/ccm-bundle-devel-wildfly/src/main/webapp/WEB-INF/jboss-web.xml +++ b/ccm-bundle-devel-wildfly/src/main/webapp/WEB-INF/jboss-web.xml @@ -1,4 +1,5 @@ /libreccm + UTF-8 diff --git a/ccm-bundle-devel-wildfly/src/main/webapp/WEB-INF/web.xml b/ccm-bundle-devel-wildfly/src/main/webapp/WEB-INF/web.xml index 8ee4f3591..a3c67e4f7 100644 --- a/ccm-bundle-devel-wildfly/src/main/webapp/WEB-INF/web.xml +++ b/ccm-bundle-devel-wildfly/src/main/webapp/WEB-INF/web.xml @@ -15,6 +15,10 @@ ccm.distribution libreccm + + resteasy.resources + org.jboss.resteasy.plugins.stats.RegistryStatsResource + @@ -65,8 +69,12 @@ javax.faces.FACELETS_SKIP_COMMENTS true + + PARAMETER_ENCODING + UTF-8 + - - + LibreCCM Core - + Lesser GPL 2.1 http://www.gnu.org/licenses/old-licenses/lgpl-2.1 - + org.seleniumhq.selenium @@ -59,13 +60,13 @@ hibernate-core provided - + org.hibernate hibernate-envers provided - + org.hibernate.validator hibernate-validator @@ -84,13 +85,13 @@ org.glassfish javax.el - + org.hibernate hibernate-search-orm provided - + javax.xml.bind jaxb-api @@ -101,7 +102,7 @@ jaxb-runtime provided - + javax.mvc javax.mvc-api @@ -111,12 +112,12 @@ krazo-core provided - + org.eclipse.krazo.ext krazo-freemarker provided - + @@ -124,7 +125,7 @@ org.flywaydb flyway-core - + org.apache.logging.log4j @@ -134,7 +135,7 @@ org.apache.logging.log4j log4j-api - + commons-beanutils commons-beanutils @@ -151,7 +152,11 @@ commons-lang commons-lang - + + commons-validator + commons-validator + + oro oro @@ -160,18 +165,18 @@ org.bouncycastle bcprov-jdk16 - + net.sf.jtidy jtidy - + junit junit test - + org.hamcrest hamcrest-core @@ -182,20 +187,20 @@ hamcrest-library test - + org.libreccm ccm-testutils ${project.parent.version} test - + nl.jqno.equalsverifier equalsverifier test - + org.jboss.arquillian.junit arquillian-junit-container @@ -211,12 +216,12 @@ shrinkwrap-resolver-impl-maven test - + org.apache.maven maven-artifact - + org.apache.shiro shiro-core @@ -225,7 +230,7 @@ org.apache.shiro shiro-web - + io.jsonwebtoken jjwt-api @@ -238,33 +243,33 @@ io.jsonwebtoken jjwt-jackson - + com.h2database h2 test - + org.reflections reflections - + - + org.freemarker freemarker - + net.sf.saxon Saxon-HE - + @@ -283,17 +288,17 @@ com.fasterxml.jackson.datatype jackson-datatype-jdk8 - + com.fasterxml.jackson.dataformat jackson-dataformat-xml - + com.fasterxml.jackson.dataformat jackson-dataformat-csv - + ccm-core - + src/main/resources true + + ./target/generated-resources + - + src/test/resources @@ -317,7 +325,7 @@ ${project.build.directory}/generated-resources - + org.apache.maven.plugins @@ -330,6 +338,39 @@ ${project.build.sourceEncoding} + + com.github.eirslett + frontend-maven-plugin + + ../node + + + + Install node.js and NPM + + install-node-and-npm + + + v12.18.3 + + + + npm install + + npm + + + + build + + npm + + + run build + + + + org.apache.maven.plugins maven-surefire-plugin @@ -394,7 +435,7 @@ - + @@ -443,7 +484,7 @@ spotbugs-exclude.xml - + org.apache.maven.plugins maven-pmd-plugin @@ -585,7 +626,7 @@ - + - + run-its-with-wildfly-h2mem @@ -795,7 +836,7 @@ Saxon-HE - + @@ -808,7 +849,7 @@ ${project.build.directory}/generated-resources - + de.jpdigital @@ -906,7 +947,7 @@ - + @@ -924,7 +965,7 @@ - + run-its-with-wildfly-pgsql @@ -943,7 +984,7 @@ provided - + @@ -956,7 +997,7 @@ ${project.build.directory}/generated-resources - + de.jpdigital @@ -1085,7 +1126,7 @@ - + @@ -1103,7 +1144,7 @@ - + run-its-in-remote-wildfly-h2mem @@ -1117,7 +1158,7 @@ Saxon-HE - + @@ -1130,7 +1171,7 @@ ${project.build.directory}/generated-resources - + de.jpdigital @@ -1178,7 +1219,7 @@ - + @@ -1196,7 +1237,7 @@ - + run-its-in-remote-wildfly-pgsql @@ -1210,7 +1251,7 @@ Saxon-HE - + @@ -1223,7 +1264,7 @@ ${project.build.directory}/generated-resources - + de.jpdigital @@ -1271,7 +1312,7 @@ - + @@ -1289,7 +1330,7 @@ - + - + diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java b/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java index bff6acabb..b75adb31e 100755 --- a/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java +++ b/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java @@ -41,32 +41,59 @@ import java.util.stream.Collectors; * @author Jens Pelzetter */ @Configuration( - descKey = "bebop.config.description") + descBundle = "com.arsdigita.bebop.BebopConfig", + descKey = "description", + titleKey = "title" +) public final class BebopConfig { - @Setting + @Setting( + descKey = "presenterClassName.desc", + labelKey = "presenterClassName.label" + ) private String presenterClassName = PageTransformer.class.getName(); - @Setting + @Setting( + descKey = "basePageClassName.desc", + labelKey = "basePageClassName.label" + ) private String basePageClassName = SimplePage.class.getName(); - @Setting + @Setting( + descKey = "tidyConfigFile.desc", + labelKey = "tidyConfigFile.label" + ) private String tidyConfigFile = "com/arsdigita/bebop/parameters/tidy.properties"; - @Setting + @Setting( + descKey = "fancyErrors.desc", + labelKey = "fancyErrors.label" + ) private Boolean fancyErrors = false; - @Setting + @Setting( + descKey = "dcpOnButtons.desc", + labelKey = "dcpOnButtons.label" + ) private Boolean dcpOnButtons = true; - @Setting + @Setting( + descKey = "dcpOnLinks.desc", + labelKey = "dcpOnLinks.label" + ) private Boolean dcpOnLinks = false; - @Setting + @Setting( + descKey = "treeSelectEnabled.desc", + labelKey = "treeSelectEnabled.label" + ) private Boolean treeSelectEnabled = false; - @Setting + @Setting( + descKey = "dhtmlEditors.desc", + labelKey = "dhtmlEditors.label" + ) private Set dhtmlEditors = new HashSet<>( Arrays.asList(new String[]{BebopConstants.BEBOP_XINHAEDITOR, BebopConstants.BEBOP_FCKEDITOR, @@ -74,14 +101,23 @@ public final class BebopConfig { BebopConstants.BEBOP_CCMEDITOR, BebopConstants.BEBOP_TINYMCE_EDITOR})); - @Setting + @Setting( + descKey = "defaultDhtmlEditor.desc", + labelKey = "defaultDhtmlEditor.label" + ) private String defaultDhtmlEditor = BebopConstants.BEBOP_TINYMCE_EDITOR; - @Setting + @Setting( + descKey = "dhtmlEditorSrcFile.desc", + labelKey = "dhtmlEditorSrcFile.label" + ) // private String dhtmlEditorSrcFile = "/ccm-editor/ccm-editor-loader.js"; private String dhtmlEditorSrcFile = "/webjars/tinymce/4.8.2/tinymce.js"; - @Setting + @Setting( + descKey = "showClassName.desc", + labelKey = "showClassName.label" + ) private Boolean showClassName = false; public static BebopConfig getConfig() { diff --git a/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherConfig.java b/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherConfig.java index fbc41278f..2a4e44466 100755 --- a/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherConfig.java +++ b/ccm-core/src/main/java/com/arsdigita/dispatcher/DispatcherConfig.java @@ -28,19 +28,35 @@ import org.libreccm.configuration.Setting; * * @author Jens Pelzetter */ -@Configuration +@Configuration( + descBundle = "com.arsdigita.dispatcher.DispatcherConfig", + descKey = "description", + titleKey = "title" +) public final class DispatcherConfig { - @Setting + @Setting( + descKey = "cachingActive.desc", + labelKey = "cachingActive.label" + ) private Boolean cachingActive = true; - @Setting + @Setting( + descKey = "defaultExpiry.desc", + labelKey = "defaultExpiry.label" + ) private Integer defaultExpiry = 259200; - @Setting + @Setting( + descKey = "staticUrlPrefix.desc", + labelKey = "statusUrlPrefix.label" + ) private String staticUrlPrefix = "/STATICII/"; - @Setting + @Setting( + descKey = "defaultPageClass.desc", + labelKey = "defaultPageClass.label" + ) private String defaultPageClass = "com.arsdigita.bebop.Page"; public static DispatcherConfig getConfig() { diff --git a/ccm-core/src/main/java/com/arsdigita/ui/admin/categories/DomainForm.java b/ccm-core/src/main/java/com/arsdigita/ui/admin/categories/DomainForm.java index 3e2e6ff93..f5204dafb 100644 --- a/ccm-core/src/main/java/com/arsdigita/ui/admin/categories/DomainForm.java +++ b/ccm-core/src/main/java/com/arsdigita/ui/admin/categories/DomainForm.java @@ -35,6 +35,8 @@ import org.libreccm.categorization.DomainManager; import org.libreccm.categorization.DomainRepository; import org.libreccm.cdi.utils.CdiUtil; +import java.time.LocalDate; + import static com.arsdigita.ui.admin.AdminUiConstants.*; /** @@ -187,7 +189,8 @@ class DomainForm extends Form { } final String versionData = data.getString(VERSION); final java.util.Date releasedData = (java.util.Date) data.get( - RELEASED); + RELEASED + ); final String rootCategoryNameData = data.getString( ROOT_CATEGORY_NAME); @@ -207,7 +210,7 @@ class DomainForm extends Form { domain.setDomainKey(domainKeyData); domain.setUri(domainUriData); domain.setVersion(versionData); - domain.setReleased(releasedData); + domain.setReleased(LocalDate.from(releasedData.toInstant())); domainRepository.save(domain); } diff --git a/ccm-core/src/main/java/org/libreccm/api/ApiConstants.java b/ccm-core/src/main/java/org/libreccm/api/ApiConstants.java new file mode 100644 index 000000000..b8e5c7d09 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/api/ApiConstants.java @@ -0,0 +1,35 @@ +/* + * 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.api; + +/** + * + * @author Jens Pelzetter + */ +public final class ApiConstants { + + private ApiConstants() { + // Nothing + } + + public static final String IDENTIFIER_PREFIX_ID = "ID-"; + + public static final String IDENTIFIER_PREFIX_UUID = "UUID-"; + +} diff --git a/ccm-core/src/main/java/org/libreccm/api/Identifier.java b/ccm-core/src/main/java/org/libreccm/api/Identifier.java new file mode 100644 index 000000000..319ce1021 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/api/Identifier.java @@ -0,0 +1,46 @@ +/* + * 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.api; + +/** + * + * @author Jens Pelzetter + */ +public class Identifier { + + private final IdentifierType type; + + private final String identifier; + + protected Identifier( + final IdentifierType type, final String identifier + ) { + this.type = type; + this.identifier = identifier; + } + + public IdentifierType getType() { + return type; + } + + public String getIdentifier() { + return identifier; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/api/IdentifierParser.java b/ccm-core/src/main/java/org/libreccm/api/IdentifierParser.java new file mode 100644 index 000000000..da2e48d9b --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/api/IdentifierParser.java @@ -0,0 +1,51 @@ +/* + * 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.api; + +import java.util.Objects; + +import javax.enterprise.context.Dependent; + +/** + * + * @author Jens Pelzetter + */ +@Dependent +public class IdentifierParser { + + public Identifier parseIdentifier(final String identifierParam) { + Objects.requireNonNull(identifierParam, "identifier param is null."); + + if (identifierParam.startsWith(ApiConstants.IDENTIFIER_PREFIX_ID)) { + final String identifier = identifierParam + .substring(ApiConstants.IDENTIFIER_PREFIX_ID.length()); + return new Identifier(IdentifierType.ID, identifier); + } else if (identifierParam.startsWith( + ApiConstants.IDENTIFIER_PREFIX_UUID)) { + final String identifier = identifierParam + .substring(ApiConstants.IDENTIFIER_PREFIX_UUID.length()); + return new Identifier(IdentifierType.UUID, identifier); + } else { + return new Identifier( + IdentifierType.PROPERTY, identifierParam + ); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/api/IdentifierType.java b/ccm-core/src/main/java/org/libreccm/api/IdentifierType.java new file mode 100644 index 000000000..eaf85737b --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/api/IdentifierType.java @@ -0,0 +1,29 @@ +/* + * 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.api; + +/** + * + * @author Jens Pelzetter + */ +public enum IdentifierType { + ID, + UUID, + PROPERTY +} diff --git a/ccm-core/src/main/java/org/libreccm/categorization/Categorization.java b/ccm-core/src/main/java/org/libreccm/categorization/Categorization.java index 5b8b0f528..f5556b22f 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/Categorization.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/Categorization.java @@ -54,6 +54,11 @@ import javax.persistence.Table; @Entity @Table(name = "CATEGORIZATIONS", schema = DB_SCHEMA) @NamedQueries({ + @NamedQuery( + name = "Categorization.findById", + query + = "SELECT c FROM Categorization c WHERE c.categorizationId = :categorizationId" + ), @NamedQuery( name = "Categorization.findByUuid", query = "SELECT c FROM Categorization c WHERE c.uuid = :uuid" @@ -62,36 +67,31 @@ import javax.persistence.Table; name = "Categorization.find", query = "SELECT c FROM Categorization c " + "WHERE c.category = :category " - + "AND c.categorizedObject = :object") - , + + "AND c.categorizedObject = :object"), @NamedQuery( name = "Categorization.isAssignedTo", query = "SELECT (CASE WHEN COUNT(c) > 0 THEN true ELSE false END) " + "FROM Categorization c " + "WHERE c.category = :category " - + "AND c.categorizedObject = :object") - , + + "AND c.categorizedObject = :object"), @NamedQuery( name = "Categorization.isAssignedToWithType", query = "SELECT (CASE WHEN COUNT(c) > 0 THEN true ELSE false END) " + "FROM Categorization c " + "WHERE c.category = :category " + "AND c.categorizedObject = :object " - + "AND c.type = :type") - , + + "AND c.type = :type"), @NamedQuery( name = "Categorization.findIndexObject", query = "SELECT c.categorizedObject FROM Categorization c " + "WHERE c.category = :category " - + "AND c.indexObject = TRUE") - , + + "AND c.indexObject = TRUE"), @NamedQuery( name = "Categorization.findIndexObjectCategorization", query = "SELECT c FROM Categorization c " + "WHERE c.category = :category " + "AND c.indexObject = TRUE" - ) - , + ), @NamedQuery( name = "Categorization.hasIndexObject", query = "SELECT (CASE WHEN COUNT(c.categorizedObject) > 0 THEN true " diff --git a/ccm-core/src/main/java/org/libreccm/categorization/CategorizationImExporter.java b/ccm-core/src/main/java/org/libreccm/categorization/CategorizationImExporter.java index ebb62da5b..aa7e64555 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/CategorizationImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/CategorizationImExporter.java @@ -23,17 +23,20 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Instance; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.TypedQuery; import javax.transaction.Transactional; /** * Exporter/Importer for {@link Categorization} entities. - * + * * @author Jens Pelzetter */ @RequestScoped @@ -48,17 +51,15 @@ public class CategorizationImExporter private Instance dependenciesProviders; @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return Categorization.class; } @Override protected Set> getRequiredEntities() { - final Set> entities = new HashSet<>(); entities.add(Category.class); - + dependenciesProviders.forEach( provider -> entities.addAll(provider.getCategorizableEntities()) ); @@ -69,8 +70,27 @@ public class CategorizationImExporter @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final Categorization entity) { - entityManager.persist(entity); } + @Override + protected Categorization reloadEntity(final Categorization entity) { + try { + return entityManager.createNamedQuery( + "Categorization.findById", + Categorization.class + ).setParameter( + "categorizationId", + Objects.requireNonNull(entity).getCategorizationId() + ).getSingleResult(); + } catch (NoResultException ex) { + throw new IllegalArgumentException( + String.format( + "Categorization entity %s was not found in the database.", + Objects.toString(entity) + ) + ); + } + } + } diff --git a/ccm-core/src/main/java/org/libreccm/categorization/CategoryImExporter.java b/ccm-core/src/main/java/org/libreccm/categorization/CategoryImExporter.java index bf1278104..e851ce5e0 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/CategoryImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/CategoryImExporter.java @@ -23,6 +23,7 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; @@ -42,14 +43,12 @@ public class CategoryImExporter extends AbstractEntityImExporter { private CategoryRepository categoryRepository; @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return Category.class; } @Override protected Set> getRequiredEntities() { - final Set> entities = new HashSet<>(); entities.add(Domain.class); @@ -59,8 +58,21 @@ public class CategoryImExporter extends AbstractEntityImExporter { @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final Category entity) { - categoryRepository.save(entity); } + @Override + protected Category reloadEntity(final Category entity) { + return categoryRepository + .findById(Objects.requireNonNull(entity).getObjectId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Category entity %s does not exist in the database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/categorization/CategoryManager.java b/ccm-core/src/main/java/org/libreccm/categorization/CategoryManager.java index 41dc93296..91c752963 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/CategoryManager.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/CategoryManager.java @@ -872,7 +872,7 @@ public class CategoryManager implements Serializable { * The first entry in the list is the root category, the last entry is the * provided category. * - * @param ofCategory The category for whic the tree is generated. + * @param ofCategory The category for which the tree is generated. * * @return A list of a categories in the path of the provided category. */ diff --git a/ccm-core/src/main/java/org/libreccm/categorization/Domain.java b/ccm-core/src/main/java/org/libreccm/categorization/Domain.java index de95bee8f..505408780 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/Domain.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/Domain.java @@ -42,9 +42,10 @@ import static org.libreccm.core.CoreConstants.DB_SCHEMA; import org.libreccm.imexport.Exportable; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; -import java.util.Date; import java.util.List; import java.util.Objects; @@ -63,8 +64,6 @@ import javax.persistence.NamedQuery; import javax.persistence.NamedSubgraph; import javax.persistence.OneToMany; import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; import javax.validation.constraints.NotBlank; /** @@ -83,21 +82,33 @@ import javax.validation.constraints.NotBlank; @Entity @Table(name = "CATEGORY_DOMAINS", schema = DB_SCHEMA) @NamedQueries({ - @NamedQuery(name = "Domain.findByKey", - query = "SELECT d FROM Domain d WHERE d.domainKey = :key"), - @NamedQuery(name = "Domain.findByUri", - query = "SELECT d FROM Domain d WHERE d.uri = :uri"), - @NamedQuery(name = "Domain.findByUuid", - query = "SELECT d FROM Domain d WHERE d.uuid = :uuid"), - @NamedQuery(name = "Domain.findAll", - query = "SELECT d FROM Domain d ORDER BY d.domainKey"), + @NamedQuery( + name = "Domain.findByKey", + query = "SELECT d FROM Domain d WHERE d.domainKey = :key" + ), + @NamedQuery( + name = "Domain.findByUri", + query = "SELECT d FROM Domain d WHERE d.uri = :uri" + ), + @NamedQuery( + name = "Domain.findByUuid", + query = "SELECT d FROM Domain d WHERE d.uuid = :uuid" + ), + @NamedQuery( + name = "Domain.findByRootCategory", + query = "SELECT d FROM Domain d WHERE d.root = :root" + ), + @NamedQuery( + name = "Domain.findAll", + query = "SELECT d FROM Domain d ORDER BY d.domainKey" + ), @NamedQuery( name = "Domain.search", query - = "SELECT d FROM Domain d " - + "WHERE d.domainKey LIKE CONCAT (LOWER(:term), '%') " - + "OR d.uri LIKE CONCAT (LOWER(:term), '%') " - + "ORDER BY d.domainKey") + = "SELECT d FROM Domain d " + + "WHERE d.domainKey LIKE CONCAT (LOWER(:term), '%') " + + "OR d.uri LIKE CONCAT (LOWER(:term), '%') " + + "ORDER BY d.domainKey") }) @NamedEntityGraphs({ @NamedEntityGraph( @@ -188,7 +199,6 @@ public class Domain extends CcmObject implements Serializable, Exportable { * A version string for the {@code Domain}. */ @Column(name = "VERSION", nullable = true) - @NotBlank @XmlElement(name = "version", namespace = CAT_XML_NS) private String version; @@ -196,9 +206,8 @@ public class Domain extends CcmObject implements Serializable, Exportable { * A timestamp for the release date of the {@code Domain}. */ @Column(name = "RELEASED") - @Temporal(TemporalType.TIMESTAMP) @XmlElement(name = "released", namespace = CAT_XML_NS) - private Date released; + private LocalDate released; /** * The root category of the domain. @@ -267,20 +276,12 @@ public class Domain extends CcmObject implements Serializable, Exportable { this.version = version; } - public Date getReleased() { - if (released == null) { - return null; - } else { - return new Date(released.getTime()); - } + public LocalDate getReleased() { + return released; } - public void setReleased(final Date released) { - if (released == null) { - this.released = null; - } else { - this.released = new Date(released.getTime()); - } + public void setReleased(final LocalDate released) { + this.released = released; } public Category getRoot() { @@ -399,18 +400,24 @@ public class Domain extends CcmObject implements Serializable, Exportable { @Override public String toString(final String data) { + final String releasedStr; + if (released == null) { + releasedStr = ""; + } else { + releasedStr = DateTimeFormatter.ISO_DATE.format(released); + } return String.format( ", domainKey = \"%s\", " + "uri = \"%s\", " + "title = \"%s\", " + "version = \"%s\", " - + "released = %tF %Jens Pelzetter */ @RequestScoped @@ -39,10 +40,10 @@ public class DomainImExporter extends AbstractEntityImExporter { @Inject private DomainRepository domainRepository; - + @Override - protected Class getEntityClass() { - + public Class getEntityClass() { + return Domain.class; } @@ -54,10 +55,22 @@ public class DomainImExporter extends AbstractEntityImExporter { @Override protected Set> getRequiredEntities() { - + return Collections.emptySet(); } - - - + + @Override + protected Domain reloadEntity(final Domain entity) { + return domainRepository + .findById(Objects.requireNonNull(entity).getObjectId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Domain entity %s was not found in the database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/categorization/DomainManager.java b/ccm-core/src/main/java/org/libreccm/categorization/DomainManager.java index f34e48779..fe44dd540 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/DomainManager.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/DomainManager.java @@ -119,6 +119,41 @@ public class DomainManager implements Serializable { domainRepo.save(domain); } + /** + * Adds a {@code CcmApplication} to the owners of a {@link Domain}.If the + provided {@code CcmApplication} is already an owner of the provided + {@code Domain} the method does nothing. + * + * @param application The {@code CcmApplication} to add to the owners of the + * {@code Domain}. + * @param domain The {@code Domain} to which owners the + * {@code CcmApplication is added}. + * @param context Context for the mapping + */ + @AuthorizationRequired + @RequiresPrivilege(CategorizationConstants.PRIVILEGE_MANAGE_DOMAINS) + @Transactional(Transactional.TxType.REQUIRED) + public void addDomainOwner( + final CcmApplication application, + final Domain domain, + final String context + ) { + final DomainOwnership ownership = new DomainOwnership(); + ownership.setUuid(UUID.randomUUID().toString()); + ownership.setDomain(domain); + ownership.setOwner(application); + ownership.setContext(context); + ownership.setOwnerOrder(domain.getOwners().size() + 1); + ownership.setDomainOrder(application.getDomains().size() + 1); + + application.addDomain(ownership); + domain.addOwner(ownership); + + entityManager.persist(ownership); + applicationRepo.save(application); + domainRepo.save(domain); + } + /** * Removes a {@code CcmApplication} from the owners of a {@code Domain}. If * the provided {@code CcmApplication} is not an owner of the provided diff --git a/ccm-core/src/main/java/org/libreccm/categorization/DomainOwnership.java b/ccm-core/src/main/java/org/libreccm/categorization/DomainOwnership.java index db2a9ff65..09a77f170 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/DomainOwnership.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/DomainOwnership.java @@ -54,11 +54,15 @@ import javax.xml.bind.annotation.XmlElement; @Entity @Table(name = "DOMAIN_OWNERSHIPS", schema = DB_SCHEMA) @NamedQueries({ + @NamedQuery( + name = "DomainOwnership.findById", + query + = "SELECT o FROM DomainOwnership o WHERE o.ownershipId = :ownershipId" + ), @NamedQuery( name = "DomainOwnership.findByUuid", query = "SELECT o FROM DomainOwnership o WHERE o.uuid = :uuid" - ) - , + ), @NamedQuery( name = "DomainOwnership.findByOwnerAndDomain", query = "SELECT o FROM DomainOwnership o " @@ -77,7 +81,7 @@ public class DomainOwnership implements Serializable, Exportable { @Column(name = "OWNERSHIP_ID") @GeneratedValue(strategy = GenerationType.AUTO) private long ownershipId; - + @Column(name = "UUID", unique = true, nullable = false) @XmlElement(name = "uuid", namespace = CoreConstants.CORE_XML_NS) private String uuid; @@ -132,7 +136,7 @@ public class DomainOwnership implements Serializable, Exportable { public void setUuid(final String uuid) { this.uuid = uuid; } - + public CcmApplication getOwner() { return owner; } diff --git a/ccm-core/src/main/java/org/libreccm/categorization/DomainOwnershipImExporter.java b/ccm-core/src/main/java/org/libreccm/categorization/DomainOwnershipImExporter.java index 124cc643f..65dd84feb 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/DomainOwnershipImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/DomainOwnershipImExporter.java @@ -24,17 +24,19 @@ import org.libreccm.imexport.Processes; import org.libreccm.web.CcmApplication; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.NoResultException; import javax.transaction.Transactional; /** * Exporter/Importer for {@link DomainOwnership} entities. - * - * + * + * * @author Jens Pelzetter */ @RequestScoped @@ -46,21 +48,18 @@ public class DomainOwnershipImExporter private EntityManager entityManager; @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return DomainOwnership.class; } @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final DomainOwnership entity) { - entityManager.persist(entity); } @Override protected Set> getRequiredEntities() { - final Set> classes = new HashSet<>(); classes.add(CcmApplication.class); classes.add(Domain.class); @@ -68,4 +67,26 @@ public class DomainOwnershipImExporter return classes; } + @Override + protected DomainOwnership reloadEntity(final DomainOwnership entity) { + try { + return entityManager + .createNamedQuery( + "DomainOwnership.findById", + DomainOwnership.class + ) + .setParameter( + "ownershipId", + Objects.requireNonNull(entity.getOwnershipId()) + ).getSingleResult(); + } catch (NoResultException ex) { + throw new IllegalArgumentException( + String.format( + "DomainOwnership entity %s not found in database.", + Objects.toString(entity) + ) + ); + } + } + } diff --git a/ccm-core/src/main/java/org/libreccm/categorization/DomainRepository.java b/ccm-core/src/main/java/org/libreccm/categorization/DomainRepository.java index 17681c952..caff64b7e 100644 --- a/ccm-core/src/main/java/org/libreccm/categorization/DomainRepository.java +++ b/ccm-core/src/main/java/org/libreccm/categorization/DomainRepository.java @@ -55,7 +55,7 @@ public class DomainRepository extends AbstractEntityRepository { public String getIdAttributeName() { return "objectId"; } - + @Override public Long getIdOfEntity(final Domain entity) { return entity.getObjectId(); @@ -87,6 +87,7 @@ public class DomainRepository extends AbstractEntityRepository { * @return The {@code Domain} identified by {@code domainKey} or * {@code null} if there is no such {@code Domain}. */ + @Transactional(Transactional.TxType.REQUIRED) public Optional findByDomainKey(final String domainKey) { final TypedQuery query = getEntityManager() .createNamedQuery("Domain.findByKey", Domain.class); @@ -111,6 +112,7 @@ public class DomainRepository extends AbstractEntityRepository { * @return The {@code Domain} identified by the provided URI or {@code null} * if there is so such {@code Domain}. */ + @Transactional(Transactional.TxType.REQUIRED) public Domain findByUri(final URI uri) { final TypedQuery query = getEntityManager() .createNamedQuery("Domain.findByUri", Domain.class); @@ -126,18 +128,37 @@ public class DomainRepository extends AbstractEntityRepository { * * @return An optional either with the found item or empty */ + @Transactional(Transactional.TxType.REQUIRED) public Optional findByUuid(final String uuid) { - final TypedQuery query = getEntityManager() - .createNamedQuery("Domain.findByUuid", Domain.class); - query.setParameter("uuid", uuid); - try { - return Optional.of(query.getSingleResult()); + return Optional.of( + getEntityManager() + .createNamedQuery("Domain.findByUuid", Domain.class) + .setParameter("uuid", uuid) + .getSingleResult() + ); } catch (NoResultException ex) { return Optional.empty(); } } + @Transactional(Transactional.TxType.REQUIRED) + public Optional findByRootCategory(final Category rootCategory) { + try { + return Optional.of( + getEntityManager() + .createNamedQuery( + "Domain.findByRootCategory", Domain.class + ) + .setParameter("root", rootCategory) + .getSingleResult() + ); + } catch (NoResultException ex) { + return Optional.empty(); + } + } + + @Transactional(Transactional.TxType.REQUIRED) public List search(final String term) { final TypedQuery query = getEntityManager() .createNamedQuery("Domain.search", Domain.class); diff --git a/ccm-core/src/main/java/org/libreccm/configuration/AbstractSetting.java b/ccm-core/src/main/java/org/libreccm/configuration/AbstractSetting.java index 7c4d1355d..fb81f9c86 100644 --- a/ccm-core/src/main/java/org/libreccm/configuration/AbstractSetting.java +++ b/ccm-core/src/main/java/org/libreccm/configuration/AbstractSetting.java @@ -18,15 +18,27 @@ */ package org.libreccm.configuration; -import org.hibernate.validator.constraints.NotBlank; -import javax.persistence.*; import javax.validation.constraints.Pattern; + import java.io.Serializable; import java.util.Objects; import static org.libreccm.core.CoreConstants.DB_SCHEMA; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import javax.validation.constraints.NotBlank; + /** * Abstract base class for all settings. * diff --git a/ccm-core/src/main/java/org/libreccm/configuration/ConfigurationInfo.java b/ccm-core/src/main/java/org/libreccm/configuration/ConfigurationInfo.java index 1cd59e094..e7390b405 100644 --- a/ccm-core/src/main/java/org/libreccm/configuration/ConfigurationInfo.java +++ b/ccm-core/src/main/java/org/libreccm/configuration/ConfigurationInfo.java @@ -21,7 +21,15 @@ package org.libreccm.configuration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.*; +import java.util.Collections; +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.TreeMap; + + /** * Describes a configuration. Useful for generating user interfaces. diff --git a/ccm-core/src/main/java/org/libreccm/configuration/SettingManager.java b/ccm-core/src/main/java/org/libreccm/configuration/SettingManager.java index a237ed80a..257dc46af 100644 --- a/ccm-core/src/main/java/org/libreccm/configuration/SettingManager.java +++ b/ccm-core/src/main/java/org/libreccm/configuration/SettingManager.java @@ -22,18 +22,25 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.util.Strings; import org.libreccm.core.CoreConstants; +import org.libreccm.l10n.LocalizedString; import org.libreccm.security.AuthorizationRequired; import org.libreccm.security.RequiresPrivilege; +import java.lang.reflect.Constructor; + import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import javax.transaction.Transactional; + import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; /** * @@ -106,10 +113,10 @@ public class SettingManager { /** * Create a {@link SettingInfo} instance for a setting. * - * @param configuration The configuration class to which the settings - * belongs. - * @param name The name of the setting for which the - * {@link SettingInfo} is generated. + * @param configurationClass The configuration class to which the settings + * belongs. + * @param name The name of the setting for which the + * {@link SettingInfo} is generated. * * @return The {@link SettingInfo} for the provided configuration class. */ @@ -117,28 +124,29 @@ public class SettingManager { "PMD.CyclomaticComplexity", "PMD.StandardCyclomaticComplexity"}) public SettingInfo getSettingInfo( - final Class configuration, - final String name) { - if (configuration == null) { + final Class configurationClass, + final String name + ) { + if (configurationClass == null) { throw new IllegalArgumentException("Configuration can't be null"); } - if (configuration.getAnnotation(Configuration.class) == null) { + if (configurationClass.getAnnotation(Configuration.class) == null) { throw new IllegalArgumentException(String.format( "The class \"%s\" of the provided object is not annotated " + "with \"%s\".", - configuration.getClass().getName(), + configurationClass.getClass().getName(), Configuration.class.getName())); } final Field field; try { - field = configuration.getDeclaredField(name); + field = configurationClass.getDeclaredField(name); } catch (SecurityException | NoSuchFieldException ex) { LOGGER.warn(String.format( "Failed to generate SettingInfo for field \"%s\" of " + "configuration \"%s\". Ignoring field.", - configuration.getClass().getName(), + configurationClass.getClass().getName(), name), ex); return null; @@ -163,23 +171,61 @@ public class SettingManager { settingInfo.setValueType(field.getType().getName()); try { - final Object conf = configuration.newInstance(); - settingInfo.setDefaultValue(Objects.toString(field.get(conf))); - } catch (InstantiationException | IllegalAccessException ex) { + final Constructor constructor = configurationClass + .getConstructor(); + final Object configuration = constructor.newInstance(); + final Object defaultValueObj = field.get(configuration); + final String defaultValue; + if (defaultValueObj instanceof List) { + @SuppressWarnings("unchecked") + final List defaultValueList + = (List) defaultValueObj; + defaultValue = defaultValueList + .stream() + .collect(Collectors.joining("\n")); + } else if (defaultValueObj instanceof LocalizedString) { + final LocalizedString defaultValueLstr + = (LocalizedString) defaultValueObj; + defaultValue = defaultValueLstr + .getValues() + .entrySet() + .stream() + .map( + entry -> String.format( + "%s: %s", + entry.getKey().toString(), entry.getValue() + ) + ) + .collect(Collectors.joining("\n")); + } else if (defaultValueObj instanceof Set) { + @SuppressWarnings("unchecked") + final Set defaultValueSet + = (Set) defaultValueObj; + defaultValue = defaultValueSet + .stream() + .collect(Collectors.joining("\n")); + } else { + defaultValue = Objects.toString(defaultValueObj); + } + settingInfo.setDefaultValue(defaultValue); + } catch (NoSuchMethodException + | InstantiationException + | InvocationTargetException + | IllegalAccessException ex) { LOGGER.warn(String.format("Failed to create instance of \"%s\" to " + "get default values.", - configuration.getName()), + configurationClass.getName()), ex); } - settingInfo.setConfClass(configuration.getName()); - settingInfo.setDescBundle(getDescBundle(configuration)); + settingInfo.setConfClass(configurationClass.getName()); + settingInfo.setDescBundle(getDescBundle(configurationClass)); if (Strings.isBlank(settingAnnotation.labelKey())) { settingInfo.setLabelKey(String.join(".", field.getName(), "label")); } else { - settingInfo.setLabelKey(name); + settingInfo.setLabelKey(settingAnnotation.labelKey()); } if (Strings.isBlank(settingAnnotation.descKey())) { @@ -227,6 +273,61 @@ public class SettingManager { } } + public Object getDefaultValue( + final Class configurationClass, + final String settingName + ) { + if (configurationClass == null) { + throw new IllegalArgumentException("Configuration can't be null"); + } + + if (configurationClass.getAnnotation(Configuration.class) == null) { + throw new IllegalArgumentException(String.format( + "The class \"%s\" of the provided object is not annotated " + + "with \"%s\".", + configurationClass.getClass().getName(), + Configuration.class.getName())); + } + + final Field field; + try { + field = configurationClass.getDeclaredField(settingName); + } catch (SecurityException | NoSuchFieldException ex) { + LOGGER.warn(String.format( + "Failed to generate SettingInfo for field \"%s\" of " + + "configuration \"%s\". Ignoring field.", + configurationClass.getClass().getName(), + settingName), + ex); + return null; + } + + //Make the field accessible even if it has a private modifier + field.setAccessible(true); + + if (field.getAnnotation(Setting.class) == null) { + return null; + } + + try { + final Constructor constructor = configurationClass + .getConstructor(); + final Object configuration = constructor.newInstance(); + final Object defaultValueObj = field.get(configuration); + + return defaultValueObj; + } catch (NoSuchMethodException + | IllegalAccessException + | InstantiationException + | InvocationTargetException ex) { + LOGGER.warn(String.format("Failed to create instance of \"%s\" to " + + "get default values.", + configurationClass.getName()), + ex); + return null; + } + } + /** * Low level method of saving a setting. * diff --git a/ccm-core/src/main/java/org/libreccm/core/CcmCore.java b/ccm-core/src/main/java/org/libreccm/core/CcmCore.java index d63127fbd..35dfbe403 100644 --- a/ccm-core/src/main/java/org/libreccm/core/CcmCore.java +++ b/ccm-core/src/main/java/org/libreccm/core/CcmCore.java @@ -47,11 +47,9 @@ import java.io.IOException; import java.io.InputStream; import java.util.Properties; - - /** * Describes the {@code ccm-core} module. - * + * * @author Jens Pelzetter */ @Module(applicationTypes = { @@ -59,22 +57,19 @@ import java.util.Properties; descBundle = "com.arsdigita.ui.login.LoginResources", singleton = true, creator = LoginApplicationCreator.class, - servlet = LoginServlet.class) - , + servlet = LoginServlet.class), @ApplicationType(name = AdminUiConstants.ADMIN_APP_TYPE, descBundle = "com.arsdigita.ui.admin.AdminResources", singleton = true, creator = AdminApplicationCreator.class, - servlet = AdminServlet.class) - , + servlet = AdminServlet.class), @ApplicationType(name = "org.libreccm.ui.admin.AdminFaces", descBundle = "com.arsdigita.ui.admin.AdminResources", singleton = true, creator = AdminJsfApplicationCreator.class, - servletPath = "/admin-jsf/admin.xhtml")}, - pageModelComponentModels = { - - }, + servletPath = "/admin-jsf/admin.xhtml") + }, + pageModelComponentModels = {}, configurations = { com.arsdigita.bebop.BebopConfig.class, com.arsdigita.dispatcher.DispatcherConfig.class, @@ -107,17 +102,17 @@ public class CcmCore implements CcmModule { LOGGER.info("Setting up admin application (/ccm/admin/)..."); final AdminApplicationSetup adminSetup - = new AdminApplicationSetup(event); + = new AdminApplicationSetup(event); adminSetup.setup(); LOGGER.info("Setting up admin-jsf application (/ccm/admin-jsf/)..."); final AdminJsfApplicationSetup adminJsfSetup - = new AdminJsfApplicationSetup(event); + = new AdminJsfApplicationSetup(event); adminJsfSetup.setup(); LOGGER.info("Setting up login application..."); final LoginApplicationSetup loginSetup - = new LoginApplicationSetup(event); + = new LoginApplicationSetup(event); loginSetup.setup(); LOGGER.info("Importing category domains from bundle (if any)..."); diff --git a/ccm-core/src/main/java/org/libreccm/core/EmailAddress.java b/ccm-core/src/main/java/org/libreccm/core/EmailAddress.java index 3d012d596..c5dcfbdd6 100644 --- a/ccm-core/src/main/java/org/libreccm/core/EmailAddress.java +++ b/ccm-core/src/main/java/org/libreccm/core/EmailAddress.java @@ -18,8 +18,6 @@ */ package org.libreccm.core; -import org.hibernate.validator.constraints.Email; -import org.hibernate.validator.constraints.NotBlank; import javax.persistence.Column; import javax.persistence.Embeddable; @@ -33,6 +31,8 @@ import static org.libreccm.core.CoreConstants.CORE_XML_NS; import javax.json.Json; import javax.json.JsonObjectBuilder; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; /** * An embeddable entity for storing email addresses. diff --git a/ccm-core/src/main/java/org/libreccm/core/ResourceTypeImExporter.java b/ccm-core/src/main/java/org/libreccm/core/ResourceTypeImExporter.java index db7767350..771e3d05a 100644 --- a/ccm-core/src/main/java/org/libreccm/core/ResourceTypeImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/core/ResourceTypeImExporter.java @@ -23,6 +23,7 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; import javax.inject.Inject; @@ -39,22 +40,36 @@ public class ResourceTypeImExporter private ResourceTypeRepository repository; @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return ResourceType.class; } @Override protected void saveImportedEntity(final ResourceType entity) { - repository.save(entity); } @Override protected Set> getRequiredEntities() { - return Collections.emptySet(); } + + @Override + protected ResourceType reloadEntity(final ResourceType entity) { + return repository + .findById( + Objects.requireNonNull(entity).getResourceTypeId() + ) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "The provided ResourceType %s was not found in the " + + "database.", + Objects.toString(entity) + ) + ) + ); + } diff --git a/ccm-core/src/main/java/org/libreccm/files/CcmFiles.java b/ccm-core/src/main/java/org/libreccm/files/CcmFiles.java index 33a130516..06b635d7b 100644 --- a/ccm-core/src/main/java/org/libreccm/files/CcmFiles.java +++ b/ccm-core/src/main/java/org/libreccm/files/CcmFiles.java @@ -172,9 +172,10 @@ public class CcmFiles { if (adapter.isConfigured()) { return adapter; } else { - throw new UnexpectedErrorException( + throw new CcmFilesNotConfiguredException( "Only the default FileSystemAdapter is available but is " - + "not correctly configured."); + + "not correctly configured." + ); } } } @@ -196,7 +197,9 @@ public class CcmFiles { final String dataPath = filesConf.getDataPath(); if (dataPath == null || dataPath.trim().isEmpty()) { - throw new UnexpectedErrorException("dataPath is not configured."); + throw new CcmFilesNotConfiguredException( + "dataPath is not configured." + ); } if (dataPath.endsWith("/")) { diff --git a/ccm-core/src/main/java/org/libreccm/files/CcmFilesNotConfiguredException.java b/ccm-core/src/main/java/org/libreccm/files/CcmFilesNotConfiguredException.java new file mode 100644 index 000000000..ad77ded70 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/files/CcmFilesNotConfiguredException.java @@ -0,0 +1,67 @@ +/* + * 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.files; + +/** + * + * @author Jens Pelzetter + */ +public class CcmFilesNotConfiguredException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new instance of CcmFilesNotConfiguredException without detail message. + */ + public CcmFilesNotConfiguredException() { + super(); + } + + + /** + * Constructs an instance of CcmFilesNotConfiguredException with the specified detail message. + * + * @param msg The detail message. + */ + public CcmFilesNotConfiguredException(final String msg) { + super(msg); + } + + /** + * Constructs an instance of CcmFilesNotConfiguredException which wraps the + * specified exception. + * + * @param exception The exception to wrap. + */ + public CcmFilesNotConfiguredException(final Exception exception) { + super(exception); + } + + /** + * Constructs an instance of CcmFilesNotConfiguredException with the specified message which also wraps the + * specified exception. + * + * @param msg The detail message. + * @param exception The exception to wrap. + */ + public CcmFilesNotConfiguredException(final String msg, final Exception exception) { + super(msg, exception); + } +} diff --git a/ccm-core/src/main/java/org/libreccm/imexport/AbstractEntityImExporter.java b/ccm-core/src/main/java/org/libreccm/imexport/AbstractEntityImExporter.java index b3ecd72b0..02ea6ca81 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/AbstractEntityImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/AbstractEntityImExporter.java @@ -53,7 +53,7 @@ public abstract class AbstractEntityImExporter { * * @return The Entity class which is handled by the implementation. */ - protected abstract Class getEntityClass(); + public abstract Class getEntityClass(); /** * A set of entities which should be processed before this implementation is @@ -62,7 +62,6 @@ public abstract class AbstractEntityImExporter { * containers usually create a {@link java.lang.reflect.Proxy} class and * there is no portable way to unproxy a class. * - * * @return A {@link Set} of exportable entity classes which should be * processed before the entities which are processed by this * implementation. If the implementation has no dependencies an @@ -72,7 +71,6 @@ public abstract class AbstractEntityImExporter { @Transactional(Transactional.TxType.REQUIRED) public T importEntity(final String data) throws ImportExpection { - try { final T entity = objectMapper.readValue(data, getEntityClass()); saveImportedEntity(entity); @@ -80,14 +78,26 @@ public abstract class AbstractEntityImExporter { } catch (IOException ex) { throw new ImportExpection(ex); } - } + protected abstract void saveImportedEntity(T entity); + + /** + * Export an entity (as JSON). There should be no need to overwrite this + * method. + * + * @param entity The entity to export. + * + * @return The entity as JSON + * + * @throws ExportException If an error occurs. + */ @Transactional(Transactional.TxType.REQUIRED) public String exportEntity(final Exportable entity) throws ExportException { - + @SuppressWarnings("unchecked") + final T export = reloadEntity((T) entity); try { - return objectMapper.writeValueAsString(entity); + return objectMapper.writeValueAsString(export); } catch (JsonProcessingException ex) { throw new ExportException(String.format( "Failed to export entity \"%s\" of type \"%s\".", @@ -95,9 +105,17 @@ public abstract class AbstractEntityImExporter { getEntityClass().getName()), ex); } - } - protected abstract void saveImportedEntity(T entity); + /** + * Reloads the entity to export. Entities become detacted for several + * reasons before they are passed to the null {@link #exportEntity(org.libreccm.imexport.Exportable) method. The + * implementation of this should reload the passed entity. + * + * @param entity The entity to reload. + * + * @return The reloaded entity + */ + protected abstract T reloadEntity(final T entity); } diff --git a/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java index 52686e3f1..d5925e6d0 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeManager.java @@ -254,16 +254,12 @@ final class EntityImExporterTreeManager { //Check if the nodes list has an entry for the required module. if (!nodes.containsKey(requiredClass.getName())) { - LOGGER.fatal("Required EntityImExporter for \"{}\" no found.", requiredClass.getName()); throw new DependencyException(String.format( "EntityImExporter for type \"%s\" depends on type \"%s\" " + "but no EntityImExporter for type \"%s\" is available.", - node - .getEntityImExporter() - .getClass() - .getAnnotation(Processes.class).value().getName(), + node.getEntityImExporter().getEntityClass(), requiredClass.getName(), requiredClass.getName())); } diff --git a/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeNode.java b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeNode.java index 012c57ad2..1173dc42f 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeNode.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/EntityImExporterTreeNode.java @@ -22,92 +22,93 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * A node in the dependency tree managed by {@link EntityImExporterTreeManager}. - * + * * @author Jens Pelzetter */ -final class EntityImExporterTreeNode { - +public final class EntityImExporterTreeNode { + private AbstractEntityImExporter entityImExporter; - + private List dependentImExporters; - + private List dependsOn; - - public EntityImExporterTreeNode() { - + + protected EntityImExporterTreeNode() { super(); - + dependentImExporters = new ArrayList<>(); dependsOn = new ArrayList<>(); } - + public EntityImExporterTreeNode( final AbstractEntityImExporter entityImExporter) { - + this(); this.entityImExporter = entityImExporter; } - + public AbstractEntityImExporter getEntityImExporter() { - + return entityImExporter; } - + void setEntityImExporter( final AbstractEntityImExporter entityImExporter) { - + this.entityImExporter = entityImExporter; } - + public List getDependentImExporters() { return Collections.unmodifiableList(dependentImExporters); } - + void setDependentImExporters( final List dependentImExporters) { - + this.dependentImExporters = new ArrayList<>(dependentImExporters); } - + void addDependentImExporter(final EntityImExporterTreeNode node) { - + dependentImExporters.add(node); } - + void removeDependentImExporter(final EntityImExporterTreeNode node) { - + dependentImExporters.remove(node); } - + public List getDependsOn() { - + return Collections.unmodifiableList(dependsOn); } - + void setDependsOn(final List dependsOn) { - + this.dependsOn = new ArrayList<>(dependsOn); } - + void addDependsOn(final EntityImExporterTreeNode node) { - + dependsOn.add(node); } - + void removeDependsOn(final EntityImExporterTreeNode node) { - + dependsOn.remove(node); } @Override public int hashCode() { int hash = 7; - hash = 47 - * hash - + Objects.hashCode(this.entityImExporter.getClass().getName()); + hash = 47 * hash + + Objects.hashCode( + this.entityImExporter.getClass().getName() + ); return hash; } @@ -127,7 +128,30 @@ final class EntityImExporterTreeNode { this.entityImExporter.getClass().getName(), other.getEntityImExporter().getClass().getName()); } - - - + + @Override + public String toString() { + return String.format( + "%s{ " + + "entityImExporter: %s, " + + "dependentImExporters: [%s], " + + "dependsOn: [%s]" + + " }", + super.toString(), + entityImExporter.getEntityClass().toString(), + dependentImExporters + .stream() + .map(EntityImExporterTreeNode::getEntityImExporter) + .map(AbstractEntityImExporter::getEntityClass) + .map(Class::getName) + .collect(Collectors.joining(", ")), + dependsOn + .stream() + .map(EntityImExporterTreeNode::getEntityImExporter) + .map(AbstractEntityImExporter::getEntityClass) + .map(Class::getName) + .collect(Collectors.joining(", ")) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java b/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java index 4a57a6593..bb79e0d17 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/ImportExport.java @@ -38,6 +38,7 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -59,6 +60,7 @@ import javax.json.JsonObjectBuilder; import javax.json.JsonReader; import javax.json.JsonString; import javax.json.JsonWriter; +import javax.transaction.Transactional; /** * Central service for importing and exporting entities. @@ -75,6 +77,23 @@ public class ImportExport { @Any private Instance> imExporters; + public List getExportableEntityTypes() { + try { + final EntityImExporterTreeManager treeManager + = new EntityImExporterTreeManager(); + final List tree = treeManager + .generateTree( + imExporters + .stream() + .collect(Collectors.toList()) + ); + + return tree; + } catch (DependencyException ex) { + throw new UnexpectedErrorException(ex); + } + } + /** * Exports the provided entities. The export will be written to a to the * {@code exports} directory in the CCM files directory. If {@code split} is @@ -89,9 +108,10 @@ public class ImportExport { * * @see CcmFilesConfiguration#dataPath */ - public void exportEntities(final List entities, - final String exportName) { - + @Transactional(Transactional.TxType.REQUIRED) + public void exportEntities( + final Collection entities, final String exportName + ) { final JsonObjectBuilder manifestBuilder = Json.createObjectBuilder(); manifestBuilder.add("created", LocalDateTime.now(ZoneId.of("UTC")).toString()); @@ -145,83 +165,12 @@ public class ImportExport { for (final Map.Entry> entry : typeEntityMap.entrySet()) { - createExportedEntities(exportName, entry.getKey(), entry.getValue()); } } - @SuppressWarnings("unchecked") - private JsonArrayBuilder createExportedEntities( - final String exportName, - final String type, - final List entities) { - - final JsonArrayBuilder filesArrayBuilder = Json.createArrayBuilder(); - - final Class clazz; - try { - clazz = (Class) Class.forName(type); - } catch (ClassNotFoundException ex) { - throw new UnexpectedErrorException(ex); - } - - final Instance> instance = imExporters - .select(new ProcessesLiteral(clazz)); - - final AbstractEntityImExporter imExporter; - if (instance.isUnsatisfied()) { - throw new UnexpectedErrorException(String.format( - "No EntityImExporter for entity type \"%s\" available.", - type)); - } else if (instance.isAmbiguous()) { - throw new UnexpectedErrorException(String.format( - "Instance reference for EntityImExporter for entity " - + "type \"%s\" is ambiguous.", - type)); - } else { - imExporter = instance.get(); - } - - for (Exportable entity : entities) { - - final String filename = String.format("%s.json", entity.getUuid()); - final OutputStream outputStream; - try { - outputStream = ccmFiles.createOutputStream(String.format( - "exports/%s/%s/%s", - exportName, - type, - filename)); - filesArrayBuilder.add(filename); - } catch (FileAccessException - | InsufficientPermissionsException ex) { - throw new UnexpectedErrorException(ex); - } - - final String exportedEntity; - try { - exportedEntity = imExporter.exportEntity(entity); - } catch (ExportException ex) { - throw new UnexpectedErrorException(ex); - } - try (final OutputStreamWriter writer = new OutputStreamWriter( - outputStream, StandardCharsets.UTF_8)) { - - writer.write(exportedEntity); - - } catch (IOException ex) { - throw new UnexpectedErrorException(ex); - } -// try (JsonWriter writer = Json.createWriter(outputStream)) { -// writer.writeObject(exportedEntity); -// } - } - - return filesArrayBuilder; - } - /** * Imports all entities from the files in the {@link imports} directory * inside the CCM files data directory. The data to import can either be a @@ -235,8 +184,8 @@ public class ImportExport { * * @see CcmFilesConfiguration#dataPath */ + @Transactional(Transactional.TxType.REQUIRED) public void importEntities(final String importName) { - final String importsPath = String.format("imports/%s", importName); try { @@ -253,15 +202,15 @@ public class ImportExport { throw new UnexpectedErrorException(ex); } - final List> imExportersList - = new ArrayList<>(); - imExporters.forEach(imExporter -> imExportersList.add(imExporter)); - try { final EntityImExporterTreeManager treeManager - = new EntityImExporterTreeManager(); + = new EntityImExporterTreeManager(); final List tree = treeManager - .generateTree(imExportersList); + .generateTree( + imExporters + .stream() + .collect(Collectors.toList()) + ); final List orderedNodes = treeManager .orderImExporters(tree); @@ -332,15 +281,13 @@ public class ImportExport { type, fileName); try (final InputStream inputStream - = ccmFiles.createInputStream(filePath)) { + = ccmFiles.createInputStream(filePath)) { final String data = new BufferedReader( new InputStreamReader(inputStream, StandardCharsets.UTF_8)) .lines() .collect(Collectors.joining("\n")); -// final JsonReader reader = Json.createReader(inputStream); -// final JsonObject data = reader.readObject(); imExporter.importEntity(data); } catch (IOException @@ -372,6 +319,71 @@ public class ImportExport { .collect(Collectors.toList()); } + @SuppressWarnings("unchecked") + private JsonArrayBuilder createExportedEntities( + final String exportName, + final String type, + final List entities) { + + final Class clazz; + try { + clazz = (Class) Class.forName(type); + } catch (ClassNotFoundException ex) { + throw new UnexpectedErrorException(ex); + } + + final Instance> instance = imExporters + .select(new ProcessesLiteral(clazz)); + + final AbstractEntityImExporter imExporter; + if (instance.isUnsatisfied()) { + throw new UnexpectedErrorException(String.format( + "No EntityImExporter for entity type \"%s\" available.", + type)); + } else if (instance.isAmbiguous()) { + throw new UnexpectedErrorException(String.format( + "Instance reference for EntityImExporter for entity " + + "type \"%s\" is ambiguous.", + type)); + } else { + imExporter = instance.get(); + } + + final JsonArrayBuilder filesArrayBuilder = Json.createArrayBuilder(); + for (Exportable entity : entities) { + final String filename = String.format("%s.json", entity.getUuid()); + final OutputStream outputStream; + try { + outputStream = ccmFiles.createOutputStream(String.format( + "exports/%s/%s/%s", + exportName, + type, + filename)); + filesArrayBuilder.add(filename); + } catch (FileAccessException + | InsufficientPermissionsException ex) { + throw new UnexpectedErrorException(ex); + } + + final String exportedEntity; + try { + exportedEntity = imExporter.exportEntity(entity); + } catch (ExportException ex) { + throw new UnexpectedErrorException(ex); + } + try (final OutputStreamWriter writer = new OutputStreamWriter( + outputStream, StandardCharsets.UTF_8)) { + + writer.write(exportedEntity); + + } catch (IOException ex) { + throw new UnexpectedErrorException(ex); + } + } + + return filesArrayBuilder; + } + private boolean isImportArchive(final String path) { final String manifestPath = String.format("imports/%s/ccm-export.json", @@ -390,8 +402,9 @@ public class ImportExport { private ImportManifest createImportManifest(final String path) { - final String manifestPath = String.format("imports/%s/ccm-export.json", - path); + final String manifestPath = String.format( + "imports/%s/ccm-export.json", path + ); try (final InputStream inputStream = ccmFiles .createInputStream(manifestPath)) { @@ -400,24 +413,33 @@ public class ImportExport { final JsonObject manifestJson = reader.readObject(); if (!manifestJson.containsKey("created")) { - throw new IllegalArgumentException(String.format( - "The manifest file \"%s\" is malformed. " - + "Key \"created\" is missing.", - manifestPath)); + throw new IllegalArgumentException( + String.format( + "The manifest file \"%s\" is malformed. " + + "Key \"created\" is missing.", + manifestPath + ) + ); } if (!manifestJson.containsKey("onServer")) { - throw new IllegalArgumentException(String.format( - "The manifest file \"%s\" is malformed. " - + "Key \"onServer\" is missing.", - manifestPath)); + throw new IllegalArgumentException( + String.format( + "The manifest file \"%s\" is malformed. " + + "Key \"onServer\" is missing.", + manifestPath + ) + ); } if (!manifestJson.containsKey("types")) { - throw new IllegalArgumentException(String.format( - "The manifest file \"%s\" is malformed. " - + "Key \"types\" is missing.", - manifestPath)); + throw new IllegalArgumentException( + String.format( + "The manifest file \"%s\" is malformed. " + + "Key \"types\" is missing.", + manifestPath + ) + ); } final LocalDateTime created = LocalDateTime @@ -429,14 +451,16 @@ public class ImportExport { // .collect(Collectors.toList()); final JsonArray typesArray = manifestJson.getJsonArray("types"); final List types = new ArrayList<>(); - for(int i = 0; i < typesArray.size(); i++) { + for (int i = 0; i < typesArray.size(); i++) { types.add(typesArray.getString(i)); } return new ImportManifest( + path, Date.from(created.atZone(ZoneId.of("UTC")).toInstant()), onServer, - types); + types + ); } catch (IOException | FileAccessException | FileDoesNotExistException diff --git a/ccm-core/src/main/java/org/libreccm/imexport/ImportManifest.java b/ccm-core/src/main/java/org/libreccm/imexport/ImportManifest.java index dbcf6fe66..05c12e78b 100644 --- a/ccm-core/src/main/java/org/libreccm/imexport/ImportManifest.java +++ b/ccm-core/src/main/java/org/libreccm/imexport/ImportManifest.java @@ -23,25 +23,36 @@ import java.util.Date; import java.util.List; /** - * Java class containg the properties of an parsed import manifest. - * + * Java class containg the properties of an parsed import manifest. + * * @author Jens Pelzetter */ public class ImportManifest { + private final String importName; + private final Date created; + private final String onServer; + private final List types; - public ImportManifest(final Date created, - final String onServer, - final List types) { - + public ImportManifest( + final String importName, + final Date created, + final String onServer, + final List types + ) { + this.importName = importName; this.created = created; this.onServer = onServer; this.types = types; } + public String getImportName() { + return importName; + } + public Date getCreated() { return new Date(created.getTime()); } diff --git a/ccm-core/src/main/java/org/libreccm/security/GroupImExporter.java b/ccm-core/src/main/java/org/libreccm/security/GroupImExporter.java index 9d0253018..f3185f2ee 100644 --- a/ccm-core/src/main/java/org/libreccm/security/GroupImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/security/GroupImExporter.java @@ -23,6 +23,7 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; @@ -32,37 +33,48 @@ import javax.transaction.Transactional; /** * Exporter/Importer for {@link Group}s. - * + * * @author Jens Pelzetter */ @RequestScoped @Processes(Group.class) public class GroupImExporter extends AbstractEntityImExporter { - + @Inject private EntityManager entityManager; - + @Inject private GroupRepository groupRepository; - + @Override - protected Class getEntityClass() { + public Class getEntityClass() { return Group.class; } - + @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final Group entity) { - entity.setPartyId(0); -// groupRepository.save(entity); entityManager.persist(entity); } - + @Override protected Set> getRequiredEntities() { - return Collections.emptySet(); } - + + @Override + protected Group reloadEntity(final Group entity) { + return groupRepository + .findById(Objects.requireNonNull(entity).getPartyId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Group entity %s was not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/security/GroupMembership.java b/ccm-core/src/main/java/org/libreccm/security/GroupMembership.java index 533f97c36..580713cd6 100644 --- a/ccm-core/src/main/java/org/libreccm/security/GroupMembership.java +++ b/ccm-core/src/main/java/org/libreccm/security/GroupMembership.java @@ -56,14 +56,25 @@ import javax.persistence.Table; @Entity @Table(name = "GROUP_MEMBERSHIPS", schema = DB_SCHEMA) @NamedQueries({ - @NamedQuery(name = "GroupMembership.findByUuid", - query = "SELECT m FROM GroupMembership m WHERE m.uuid = :uuid"), - @NamedQuery(name = "GroupMembership.findByGroupAndUser", - query = "SELECT m FROM GroupMembership m " - + "WHERE m.member = :member AND m.group = :group")}) + @NamedQuery( + name = "GroupMembership.findById", + query + = "SELECT m FROM GroupMembership m WHERE m.membershipId = :membershipId" + ), + @NamedQuery( + name = "GroupMembership.findByUuid", + query = "SELECT m FROM GroupMembership m WHERE m.uuid = :uuid" + ), + @NamedQuery( + name = "GroupMembership.findByGroupAndUser", + query = "SELECT m FROM GroupMembership m " + + "WHERE m.member = :member AND m.group = :group") +}) @XmlRootElement(name = "group-membership", namespace = CORE_XML_NS) -@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, - property = "uuid") +@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "uuid" +) public class GroupMembership implements Serializable, Exportable { private static final long serialVersionUID = 83192968306850665L; diff --git a/ccm-core/src/main/java/org/libreccm/security/GroupMembershipImExporter.java b/ccm-core/src/main/java/org/libreccm/security/GroupMembershipImExporter.java index 8f26d5c91..a9a513591 100644 --- a/ccm-core/src/main/java/org/libreccm/security/GroupMembershipImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/security/GroupMembershipImExporter.java @@ -23,10 +23,12 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.NoResultException; import javax.transaction.Transactional; /** @@ -36,21 +38,19 @@ import javax.transaction.Transactional; * @author Jens Pelzetter */ @Processes(GroupMembership.class) -public class GroupMembershipImExporter +public class GroupMembershipImExporter extends AbstractEntityImExporter { - + @Inject private EntityManager entityManager; @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return GroupMembership.class; } @Override protected Set> getRequiredEntities() { - final Set> entities = new HashSet<>(); entities.add(User.class); entities.add(Group.class); @@ -61,9 +61,30 @@ public class GroupMembershipImExporter @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final GroupMembership entity) { - entity.setMembershipId(0); entityManager.persist(entity); } + @Override + protected GroupMembership reloadEntity(final GroupMembership entity) { + try { + return entityManager + .createNamedQuery( + "GroupMembership.findById", GroupMembership.class + ) + .setParameter( + "membershipId", + Objects.requireNonNull(entity).getMembershipId() + ) + .getSingleResult(); + } catch (NoResultException ex) { + throw new IllegalArgumentException( + String.format( + "GroupMembership entity %s does not exist in the database.", + Objects.toString(entity) + ) + ); + } + } + } diff --git a/ccm-core/src/main/java/org/libreccm/security/PermissionImExporter.java b/ccm-core/src/main/java/org/libreccm/security/PermissionImExporter.java index 48275d0dc..fdf832a8f 100644 --- a/ccm-core/src/main/java/org/libreccm/security/PermissionImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/security/PermissionImExporter.java @@ -23,6 +23,7 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; @@ -30,36 +31,46 @@ import javax.inject.Inject; /** * Exporter/Importer for {@link Permission}s. - * + * * @author Jens Pelzetter */ @RequestScoped @Processes(Permission.class) -public class PermissionImExporter extends AbstractEntityImExporter{ +public class PermissionImExporter extends AbstractEntityImExporter { @Inject private PermissionRepository permissionRepository; - + @Override - protected Class getEntityClass() { + public Class getEntityClass() { return Permission.class; } @Override protected void saveImportedEntity(final Permission entity) { - permissionRepository.save(entity); } @Override protected Set> getRequiredEntities() { - final Set> classes = new HashSet<>(); classes.add(Role.class); - + return classes; } - - - + + @Override + protected Permission reloadEntity(final Permission entity) { + return permissionRepository + .findById(Objects.requireNonNull(entity).getPermissionId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Permission entity %s not found in the database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/security/RoleImExporter.java b/ccm-core/src/main/java/org/libreccm/security/RoleImExporter.java index 05f6cdc45..ace2a41ac 100644 --- a/ccm-core/src/main/java/org/libreccm/security/RoleImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/security/RoleImExporter.java @@ -18,42 +18,85 @@ */ package org.libreccm.security; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.libreccm.core.UnexpectedErrorException; import org.libreccm.imexport.AbstractEntityImExporter; +import org.libreccm.imexport.ExportException; import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; +import javax.enterprise.context.Dependent; import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.transaction.Transactional; /** * Exporter/Importer for {@link Role}s. - * + * * @author Jens Pelzetter */ +@Dependent @Processes(Role.class) public class RoleImExporter extends AbstractEntityImExporter { + private static final Logger LOGGER = LogManager.getLogger( + RoleImExporter.class); + + @Inject + private EntityManager entityManager; + @Inject private RoleRepository roleRepository; - + @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return Role.class; } + @Transactional(Transactional.TxType.REQUIRED) + @Override + public String exportEntity(final Exportable entity) throws ExportException { + final Role role = roleRepository + .findById(((Role) entity).getRoleId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Provided entity %d does not exist in database.", + entity) + ) + ); + role.getDescription().getValues().forEach((locale, value) -> LOGGER + .info("{}: {}", locale, value)); + return super.exportEntity(entity); + } + @Override protected void saveImportedEntity(final Role entity) { - roleRepository.save(entity); } @Override protected Set> getRequiredEntities() { - return Collections.emptySet(); } - + + @Override + protected Role reloadEntity(final Role entity) { + return roleRepository + .findById(Objects.requireNonNull(entity).getRoleId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Role entity %s not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/security/RoleMembership.java b/ccm-core/src/main/java/org/libreccm/security/RoleMembership.java index 6aebd8d9c..af8c01d28 100644 --- a/ccm-core/src/main/java/org/libreccm/security/RoleMembership.java +++ b/ccm-core/src/main/java/org/libreccm/security/RoleMembership.java @@ -56,11 +56,20 @@ import javax.persistence.Table; @Entity @Table(name = "ROLE_MEMBERSHIPS", schema = DB_SCHEMA) @NamedQueries({ - @NamedQuery(name = "RoleMembership.findByUuid", - query = "SELECT m FROM RoleMembership m WHERE m.uuid = :uuid"), - @NamedQuery(name = "RoleMembership.findByRoleAndMember", - query = "SELECT m FROM RoleMembership m " - + "WHERE m.member = :member AND m.role = :role") + @NamedQuery( + name = "RoleMembership.findById", + query + = "SELECT m FROM RoleMembership m WHERE m.membershipId = :membershipId" + ), + @NamedQuery( + name = "RoleMembership.findByUuid", + query = "SELECT m FROM RoleMembership m WHERE m.uuid = :uuid" + ), + @NamedQuery( + name = "RoleMembership.findByRoleAndMember", + query + = "SELECT m FROM RoleMembership m WHERE m.member = :member AND m.role = :role" + ) }) @XmlRootElement(name = "role-membership", namespace = CORE_XML_NS) @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, diff --git a/ccm-core/src/main/java/org/libreccm/security/RoleMembershipImExporter.java b/ccm-core/src/main/java/org/libreccm/security/RoleMembershipImExporter.java index a397299ce..c3679a1d8 100644 --- a/ccm-core/src/main/java/org/libreccm/security/RoleMembershipImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/security/RoleMembershipImExporter.java @@ -23,15 +23,17 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.NoResultException; import javax.transaction.Transactional; /** * Exporter/Importer for {@link RoleMembership}s. - * + * * @author Jens Pelzetter */ @Processes(RoleMembership.class) @@ -42,27 +44,45 @@ public class RoleMembershipImExporter private EntityManager entityManager; @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return RoleMembership.class; } @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final RoleMembership entity) { - entityManager.persist(entity); } @Override protected Set> getRequiredEntities() { - final Set> classes = new HashSet<>(); classes.add(User.class); classes.add(Group.class); classes.add(Role.class); - + return classes; } + @Override + protected RoleMembership reloadEntity(final RoleMembership entity) { + try { + return entityManager + .createNamedQuery( + "RoleMembership.findById", RoleMembership.class + ) + .setParameter( + "membershipId", + Objects.requireNonNull(entity).getMembershipId() + ).getSingleResult(); + } catch (NoResultException ex) { + throw new IllegalArgumentException( + String.format( + "RoleMembeship entity %s not found in database.", + Objects.toString(entity) + ) + ); + } + } + } diff --git a/ccm-core/src/main/java/org/libreccm/security/User.java b/ccm-core/src/main/java/org/libreccm/security/User.java index ce71aa92c..56f4857e2 100644 --- a/ccm-core/src/main/java/org/libreccm/security/User.java +++ b/ccm-core/src/main/java/org/libreccm/security/User.java @@ -59,6 +59,7 @@ import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.NamedSubgraph; import javax.persistence.OneToMany; +import javax.persistence.OrderBy; import javax.persistence.Table; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -186,6 +187,7 @@ public class User extends Party implements Serializable, Exportable { * Additional email addresses of the user. */ @ElementCollection(fetch = FetchType.EAGER) + @OrderBy("address") @CollectionTable(name = "USER_EMAIL_ADDRESSES", schema = DB_SCHEMA, joinColumns = { @@ -363,36 +365,37 @@ public class User extends Party implements Serializable, Exportable { @Override public JsonObjectBuilder buildJson() { - final JsonArrayBuilder emailAddressesArrayBuilder = Json.createArrayBuilder(); - + final JsonArrayBuilder emailAddressesArrayBuilder = Json + .createArrayBuilder(); + emailAddresses .stream() .map(EmailAddress::buildJson) .forEach(emailAddressesArrayBuilder::add); - + return super .buildJson() .add("givenName", givenName) .add("familyName", familyName) .add("primaryEmailAddress", primaryEmailAddress.buildJson()) - .add( - "emailAddresses", - emailAddresses - .stream() - .map(EmailAddress::buildJson) - .map(JsonObjectBuilder::build) - .collect(new JsonArrayCollector()) - ) - .add("banned", banned) - .add("passwordResetRequired", passwordResetRequired) - .add( - "groupMemberships", - groupMemberships - .stream() - .map(GroupMembership::buildJson) - .map(JsonObjectBuilder::build) - .collect(new JsonArrayCollector()) - ); + .add( + "emailAddresses", + emailAddresses + .stream() + .map(EmailAddress::buildJson) + .map(JsonObjectBuilder::build) + .collect(new JsonArrayCollector()) + ) + .add("banned", banned) + .add("passwordResetRequired", passwordResetRequired) + .add( + "groupMemberships", + groupMemberships + .stream() + .map(GroupMembership::buildJson) + .map(JsonObjectBuilder::build) + .collect(new JsonArrayCollector()) + ); } @Override diff --git a/ccm-core/src/main/java/org/libreccm/security/UserImExporter.java b/ccm-core/src/main/java/org/libreccm/security/UserImExporter.java index bf34502ee..9abe63a00 100644 --- a/ccm-core/src/main/java/org/libreccm/security/UserImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/security/UserImExporter.java @@ -23,6 +23,7 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; @@ -32,7 +33,7 @@ import javax.transaction.Transactional; /** * Exporter/Importer for users. - * + * * @author Jens Pelzetter */ @RequestScoped @@ -41,19 +42,18 @@ public class UserImExporter extends AbstractEntityImExporter { @Inject private EntityManager entityManager; - + @Inject private UserRepository userRepository; @Override - protected Class getEntityClass() { + public Class getEntityClass() { return User.class; } @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final User entity) { - // Reset partyId. entity.setPartyId(0); // userRepository.save(entity); @@ -62,8 +62,21 @@ public class UserImExporter extends AbstractEntityImExporter { @Override protected Set> getRequiredEntities() { - return Collections.emptySet(); } + @Override + protected User reloadEntity(final User entity) { + return userRepository + .findById(Objects.requireNonNull(entity).getPartyId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "User entity %s was not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/sites/Site.java b/ccm-core/src/main/java/org/libreccm/sites/Site.java index e0559bdd1..7799af838 100644 --- a/ccm-core/src/main/java/org/libreccm/sites/Site.java +++ b/ccm-core/src/main/java/org/libreccm/sites/Site.java @@ -44,6 +44,10 @@ import javax.persistence.Table; @Entity @Table(name = "SITES", schema = DB_SCHEMA) @NamedQueries({ + @NamedQuery( + name = "Site.findByUuid", + query = "SELECT s FROM Site s WHERE s.uuid = :uuid" + ), @NamedQuery( name = "Site.findByDomain", query = "SELECT s FROM Site s " diff --git a/ccm-core/src/main/java/org/libreccm/sites/SiteAwareApplication.java b/ccm-core/src/main/java/org/libreccm/sites/SiteAwareApplication.java index 0cb9e6f6c..d91544d4d 100644 --- a/ccm-core/src/main/java/org/libreccm/sites/SiteAwareApplication.java +++ b/ccm-core/src/main/java/org/libreccm/sites/SiteAwareApplication.java @@ -67,7 +67,7 @@ public class SiteAwareApplication extends CcmApplication { @Override public int hashCode() { - int hash = 3; + int hash = super.hashCode(); if (site != null) { hash = 59 * hash + Objects.hashCode(site.getDomainOfSite()); hash = 59 * hash + Objects.hashCode(site.isDefaultSite()); @@ -84,6 +84,9 @@ public class SiteAwareApplication extends CcmApplication { if (obj == null) { return false; } + if (!super.equals(obj)) { + return false; + } if (!(obj instanceof SiteAwareApplication)) { return false; } diff --git a/ccm-core/src/main/java/org/libreccm/sites/SiteRepository.java b/ccm-core/src/main/java/org/libreccm/sites/SiteRepository.java index 81389faa3..fb646c3e5 100644 --- a/ccm-core/src/main/java/org/libreccm/sites/SiteRepository.java +++ b/ccm-core/src/main/java/org/libreccm/sites/SiteRepository.java @@ -41,6 +41,28 @@ public class SiteRepository extends AbstractEntityRepository { private static final long serialVersionUID = 3120528987720524155L; + /** + * Retrieve a {@link Site} by its UUID. + * + * @param uuid The UUID of the site. + * + * @return An {@link Optional} containing the {@link Site} if a site for the + * provided UUID exists. + */ + @Transactional(Transactional.TxType.REQUIRED) + public Optional findByUuid(final String uuid) { + try { + return Optional.of( + getEntityManager() + .createNamedQuery("Site.findByUuid", Site.class) + .setParameter("uuid", uuid) + .getSingleResult() + ); + } catch (NoResultException ex) { + return Optional.empty(); + } + } + /** * Retrieve the {@link Site} for a specific domain. * @@ -127,7 +149,7 @@ public class SiteRepository extends AbstractEntityRepository { public String getIdAttributeName() { return "objectId"; } - + @Override public Long getIdOfEntity(final Site entity) { return entity.getObjectId(); diff --git a/ccm-core/src/main/java/org/libreccm/theming/FileSystemThemeProvider.java b/ccm-core/src/main/java/org/libreccm/theming/FileSystemThemeProvider.java index b66210d5f..00dde8161 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/FileSystemThemeProvider.java +++ b/ccm-core/src/main/java/org/libreccm/theming/FileSystemThemeProvider.java @@ -18,8 +18,11 @@ */ package org.libreccm.theming; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.libreccm.core.UnexpectedErrorException; import org.libreccm.files.CcmFiles; +import org.libreccm.files.CcmFilesNotConfiguredException; import org.libreccm.files.DirectoryNotEmptyException; import org.libreccm.files.FileAccessException; import org.libreccm.files.FileAlreadyExistsException; @@ -53,6 +56,10 @@ import javax.inject.Inject; public class FileSystemThemeProvider implements ThemeProvider { private static final long serialVersionUID = 1L; + + private static final Logger LOGGER = LogManager.getLogger( + FileSystemThemeProvider.class + ); private static final String BASE_PATH = "/themes"; private static final String DRAFT_THEMES_PATH = BASE_PATH + "/draft"; @@ -70,11 +77,15 @@ public class FileSystemThemeProvider implements ThemeProvider { @Inject private ThemeFileInfoUtil themeFileInfoUtil; + @Override + public String getName() { + return "FileSystemThemeProvider"; + } + @Override public List getThemes() { try { - if (!ccmFiles.isDirectory(BASE_PATH) || !ccmFiles.isDirectory(DRAFT_THEMES_PATH)) { @@ -92,8 +103,10 @@ public class FileSystemThemeProvider implements ThemeProvider { } catch (FileAccessException | FileDoesNotExistException | InsufficientPermissionsException ex) { - throw new UnexpectedErrorException(ex); + } catch(CcmFilesNotConfiguredException ex) { + LOGGER.warn(ex); + return Collections.emptyList(); } } @@ -119,6 +132,9 @@ public class FileSystemThemeProvider implements ThemeProvider { | InsufficientPermissionsException ex) { throw new UnexpectedErrorException(ex); + } catch(CcmFilesNotConfiguredException ex) { + LOGGER.warn(ex); + return Collections.emptyList(); } } diff --git a/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java b/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java index 3a8836b7e..ed89e5afd 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java +++ b/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java @@ -80,6 +80,11 @@ public class StaticThemeProvider implements ThemeProvider { @Inject private ThemeFileInfoUtil themeFileInfoUtil; + @Override + public String getName() { + return "StaticThemeProvider"; + } + @Override public List getThemes() { diff --git a/ccm-core/src/main/java/org/libreccm/theming/ThemeProvider.java b/ccm-core/src/main/java/org/libreccm/theming/ThemeProvider.java index 451e4b683..885424340 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/ThemeProvider.java +++ b/ccm-core/src/main/java/org/libreccm/theming/ThemeProvider.java @@ -36,6 +36,12 @@ import javax.enterprise.context.RequestScoped; */ public interface ThemeProvider extends Serializable { + /** + * A human readable name for the {@code ThemeProvider} implementation. + * @return + */ + String getName(); + /** * Provides a list of all themes provided by this theme provider. The list * should be ordered by the name of the theme. diff --git a/ccm-core/src/main/java/org/libreccm/theming/Themes.java b/ccm-core/src/main/java/org/libreccm/theming/Themes.java index fcf2a4eae..8601065d7 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/Themes.java +++ b/ccm-core/src/main/java/org/libreccm/theming/Themes.java @@ -28,7 +28,9 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Any; @@ -53,6 +55,7 @@ public class Themes implements Serializable { @Any private Instance providers; // + @Inject private ThemeProcessors themeProcessors; @@ -108,6 +111,100 @@ public class Themes implements Serializable { return Optional.empty(); } + public List getThemeProviders() { + return providers + .stream() + .collect(Collectors.toList()); + } + + public Optional findThemeProviderInstance( + final Class ofClazz + ) { + final Instance instance = providers + .select(ofClazz); + if (instance.isResolvable()) { + return Optional.of(instance.get()); + } else { + return Optional.empty(); + } + } + + public ThemeInfo createTheme( + final String themeName, final String providerName + ) { + final Class providerClass = getThemeProviderClass( + providerName + ); + + return createTheme( + themeName, + findThemeProviderInstance(providerClass).orElseThrow( + () -> new IllegalArgumentException( + String.format( + "No instance of ThemeProvider implementation %s available.", + providerName + ) + ) + ) + ); + } + + public ThemeInfo createTheme( + final String themeName, final ThemeProvider themeProvider + ) { + return themeProvider.createTheme(themeName); + } + + public void deleteTheme(final String themeName) { + final Optional provider = findProviderOfTheme( + Objects.requireNonNull( + themeName, + "Can't delete theme null." + ) + ); + + if (provider.isPresent()) { + provider.get().deleteTheme(themeName); + } else { + throw new IllegalArgumentException( + String.format( + "No provider providing a theme named %s found.", + themeName + ) + ); + } + } + + public void publishTheme(final String themeName) { + final Optional provider = findProviderOfTheme(themeName); + + if (provider.isPresent()) { + provider.get().publishTheme(themeName); + } else { + throw new IllegalArgumentException( + String.format( + "No provider providing a theme named %s found.", + themeName + ) + ); + } + } + + public void unpublishTheme(final String themeName) { + final Optional provider = findProviderOfTheme(themeName); + + if (provider.isPresent()) { + provider.get().unpublishTheme(themeName); + } else { + throw new IllegalArgumentException( + String.format( + "No provider providing a theme named %s found.", + themeName + ) + ); + } + } + /** * Creates HTML from the result of rendering a {@link PageModel}. * @@ -240,7 +337,37 @@ public class Themes implements Serializable { final ThemeProvider provider = forTheme.get(); provider.deleteThemeFile(theme.getName(), path); + } + @SuppressWarnings("unchecked") + private Class getThemeProviderClass( + final String providerName + ) { + try { + return (Class) Class.forName(providerName); + } catch (ClassNotFoundException ex) { + throw new IllegalArgumentException( + String.format( + "No ThemeProvider implementation %s available.", + providerName + ) + ); + } + } + + private Optional findProviderOfTheme( + final String themeName + ) { + final List providersList = new ArrayList<>(); + providers.forEach(provider -> providersList.add(provider)); + + return providersList + .stream() + .filter( + current -> current.providesTheme( + themeName, ThemeVersion.DRAFT + ) + ).findAny(); } } diff --git a/ccm-core/src/main/java/org/libreccm/theming/db/DatabaseThemeProvider.java b/ccm-core/src/main/java/org/libreccm/theming/db/DatabaseThemeProvider.java index 7f6bf54c0..bceb2da3c 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/db/DatabaseThemeProvider.java +++ b/ccm-core/src/main/java/org/libreccm/theming/db/DatabaseThemeProvider.java @@ -71,6 +71,11 @@ public class DatabaseThemeProvider implements ThemeProvider { @Inject private ThemeRepository themeRepository; + @Override + public String getName() { + return "DatabaseThemeProvider"; + } + @Override @Transactional(Transactional.TxType.REQUIRED) public List getThemes() { @@ -193,7 +198,7 @@ public class DatabaseThemeProvider implements ThemeProvider { final Optional themeFile = fileRepository .findByPath(theme, path, version); - + if (themeFile.isPresent()) { return Optional.of(createThemeFileInfo(themeFile.get())); } else { @@ -419,6 +424,7 @@ public class DatabaseThemeProvider implements ThemeProvider { private class DataFileOutputStream extends OutputStream { private final DataFile dataFile; + private final ByteArrayOutputStream outputStream; private DataFileOutputStream(final DataFile dataFile) { diff --git a/ccm-core/src/main/java/org/libreccm/ui/IsAuthenticatedFilter.java b/ccm-core/src/main/java/org/libreccm/ui/IsAuthenticatedFilter.java new file mode 100644 index 000000000..dea89b408 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/IsAuthenticatedFilter.java @@ -0,0 +1,71 @@ +/* + * 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; + +import org.libreccm.security.Shiro; + +import java.io.IOException; +import java.net.URI; + +import javax.inject.Inject; +import javax.servlet.ServletContext; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.core.Response; + +/** + * Filter for securing EE MVC UIs. Checks if the current user is authenticated. + * If not the user is redirected to the login application. + * + * @author Jens Pelzetter + */ +@PreMatching +public class IsAuthenticatedFilter implements ContainerRequestFilter { + + @Inject + private ServletContext servletContext; + + @Inject + private Shiro shiro; + + @Override + public void filter(final ContainerRequestContext requestContext) + throws IOException { + if (!shiro.getSubject().isAuthenticated()) { + final String contextPath = servletContext.getContextPath(); + final String returnUrl = requestContext + .getUriInfo() + .getRequestUri() + .getPath(); + requestContext.abortWith( + Response.temporaryRedirect( + URI.create( + String.format( + "/%s/ccm/register?return_url=%s", + contextPath, + returnUrl + ) + ) + ).build() + ); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/Message.java b/ccm-core/src/main/java/org/libreccm/ui/Message.java new file mode 100644 index 000000000..dee8912d5 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/Message.java @@ -0,0 +1,55 @@ +/* + * 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; + +/** + * Stores a message to be displayed in the UI. + * + * @author Jens Pelzetter + */ +public class Message { + + /** + * The message (or the translation key for the message). + */ + private final String message; + + /** + * The type of the message. + */ + private final MessageType messageType; + + public Message(String message, MessageType messageType) { + this.message = message; + this.messageType = messageType; + } + + public String getMessage() { + return message; + } + + public MessageType getMessageType() { + return messageType; + } + + public String getMessageTypeClass() { + return messageType.toString().toLowerCase(); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/MessageType.java b/ccm-core/src/main/java/org/libreccm/ui/MessageType.java new file mode 100644 index 000000000..740f78bdd --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/MessageType.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * Possible message types. The types are equivalent to the contextual classes + * of the Bootstrap framework. + * + * @author Jens Pelzetter + */ +public enum MessageType { + + PRIMARY, + SECONDARY, + SUCCESS, + DANGER, + WARNING, + INFO, + LIGHT, + DARK, + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/MvcLocaleResolver.java b/ccm-core/src/main/java/org/libreccm/ui/MvcLocaleResolver.java new file mode 100644 index 000000000..23960329d --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/MvcLocaleResolver.java @@ -0,0 +1,47 @@ +/* + * 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; + +import org.libreccm.l10n.GlobalizationHelper; + +import java.util.Locale; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.mvc.locale.LocaleResolver; +import javax.mvc.locale.LocaleResolverContext; + +/** + * A locale resolver implementation that simply passes the locale negoiated by + * LibreCCM to Jakarta EE MVC. + * + * @author Jens Pelzetter + */ +@RequestScoped +public class MvcLocaleResolver implements LocaleResolver { + + @Inject + private GlobalizationHelper globalizationHelper; + + @Override + public Locale resolveLocale(final LocaleResolverContext context) { + return globalizationHelper.getNegotiatedLocale(); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/AdminApplication.java b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminApplication.java new file mode 100644 index 000000000..3afdaeb16 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminApplication.java @@ -0,0 +1,75 @@ +/* + * 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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.libreccm.ui.IsAuthenticatedFilter; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.enterprise.inject.Instance; +import javax.inject.Inject; +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Collects the controllers for the admin application and registers them with + * JAX-RS. + * + * @author Jens Pelzetter + */ +@ApplicationPath("/@admin") +public class AdminApplication extends Application { + + private static final Logger LOGGER = LogManager.getLogger( + AdminApplication.class + ); + + /** + * Injection point for the admin pages. + */ + @Inject + private Instance adminPages; + + @Override + public Set> getClasses() { + final Set> classes = new HashSet<>(); + + classes.add(IsAuthenticatedFilter.class); + + classes.addAll( + adminPages + .stream() + .map(AdminPage::getControllerClasses) + .flatMap(controllers -> controllers.stream()) + .collect(Collectors.toSet()) + ); + + LOGGER.debug( + "Adding classes to AdminApplication: {}", Objects.toString(classes) + ); + + return classes; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/AdminConstants.java b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminConstants.java new file mode 100644 index 000000000..6b21d404c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminConstants.java @@ -0,0 +1,38 @@ +/* + * 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; + +/** + * Some constants for the admin application + * + * @author Jens Pelzetter + */ +public class AdminConstants { + + private AdminConstants() { + // Nothing + } + + /** + * Bundle that provides the translations for the admin application. + */ + public static final String ADMIN_BUNDLE = "org.libreccm.ui.AdminBundle"; + + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/AdminMessages.java b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminMessages.java new file mode 100644 index 000000000..998fe8ae4 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminMessages.java @@ -0,0 +1,145 @@ +/* + * 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.text.MessageFormat; +import java.util.AbstractMap; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Provides simple access to the messages in the admin bundle. The make it as + * easy as possible to access the messages this class is implemented as a map a + * made available as named bean. For simple messages, {@code AdminMesssages} can + * be used like a map in a facelets template: + * + *
+ * #{AdminMessages['some.message.key'])
+ * 
+ * + * Messages with placeholders can be retrieved using + * {@link #getMessage(java.lang.String, java.util.List)} or + * {@link #getMessage(java.lang.String, java.lang.Object[])}. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("AdminMessages") +public class AdminMessages extends AbstractMap { + + /** + * Provides access to the locale negoiated by LibreCCM. + */ + @Inject + private GlobalizationHelper globalizationHelper; + + /** + * The {@link ResourceBundle} to use. + */ + private ResourceBundle messages; + + /** + * Loads the resource bundle. + */ + @PostConstruct + private void init() { + messages = ResourceBundle.getBundle( + AdminConstants.ADMIN_BUNDLE, + globalizationHelper.getNegotiatedLocale() + ); + } + + /** + * Retrieves a message from the resource bundle. + * + * @param key The key of the message. + * @return The translated message or {@code ???message???} if the the key is + * not found in the resource bundle (message is replaced with the key). + */ + public String getMessage(final String key) { + if (messages.containsKey(key)) { + return messages.getString(key); + } else { + return String.format("???%s???", key); + } + } + + /** + * Retrieves a message with placeholders. + * + * @param key The key of the message. + * @param parameters The parameters for the placeholders. + * @return The translated message or {@code ???message???} if the the key is + * not found in the resource bundle (message is replaced with the key). + */ + public String getMessage( + final String key, final List parameters + ) { + return getMessage(key, parameters.toArray()); + } + + /** + * The translated message or {@code ???message???} if the the key is + * not found in the resource bundle (message is replaced with the key). + * + @param key The key of the message. + * @param parameters The parameters for the placeholders. + * @return The translated message or {@code ???message???} if the the key is + * not found in the resource bundle (message is replaced with the key). + */ + public String getMessage( + final String key, final Object[] parameters + ) { + if (messages.containsKey(key)) { + return MessageFormat.format(messages.getString(key), parameters); + } else { + return String.format("???%s???", key); + } + } + + @Override + public String get(final Object key) { + return get((String) key); + } + + public String get(final String key) { + return getMessage(key); + } + + @Override + public Set> entrySet() { + return messages + .keySet() + .stream() + .collect( + Collectors.toMap(key -> key, key -> messages.getString(key)) + ) + .entrySet(); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPage.java new file mode 100644 index 000000000..88ed5dbe2 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/AdminPage.java @@ -0,0 +1,95 @@ +/* + * 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.Set; + +import javax.mvc.MvcContext; + +/** + * Implementations of this interface provide the controllers etc. for an admin + * page. + * + * @author Jens Pelzetter + */ +public interface AdminPage { + + /** + * Classes implementing the controllers of the page. + * + * @return A set of controllers to be added to the {@link AdminApplication}. + */ + Set> 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 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 getImportTasks() { + return Collections.unmodifiableSortedSet(importTasks); + } + + /** + * Export all entities of the selected entity types. + * + * @param exportTypes The entity types to export. + * @param exportName Name of the export archive. + * + */ + @Transactional(Transactional.TxType.REQUIRED) + public void exportEntities( + final Set> exportTypes, + final String exportName + ) { + final Set entities = new HashSet<>(); + for (final Class type : exportTypes) { + @SuppressWarnings("unchecked") + final Set entitiesOfType = collectEntities( + (Class) type + ); + entities.addAll(entitiesOfType); + } + + final ExportTaskStatus taskStatus = new ExportTaskStatus(); + taskStatus.setName(exportName); + taskStatus.setStarted(LocalDateTime.now()); + exportTaskSender.fireAsync( + new ExportTask(exportName, LocalDate.now(), entities, taskStatus) + ).handle((task, ex) -> handleExportTaskResult(task, ex, taskStatus)); + + taskStatus.setStatus(ImExportTaskStatus.RUNNING); + exportTasks.add(taskStatus); + } + + public void importEntities(final String importName) { + final ImportTaskStatus taskStatus = new ImportTaskStatus(); + taskStatus.setStarted(LocalDateTime.now()); + importTaskSender.fireAsync( + new ImportTask(importName, LocalDate.now(), taskStatus) + ).handle((task, ex) -> handleImportTaskResult(task, ex, taskStatus)); + + taskStatus.setStatus(ImExportTaskStatus.RUNNING); + importTasks.add(taskStatus); + } + + /** + * Called every 5 minutes to remove finished tasks from {@link #exportTasks} + * and and {@link #importTasks}. + */ + @Schedule(hour = "*", minute = "*/5", persistent = false) + protected void removeFinishedTasks() { + exportTasks.removeIf( + taskStatus -> taskStatus.getStatus() == ImExportTaskStatus.FINISHED + ); + importTasks.removeIf( + taskStatus -> taskStatus.getStatus() == ImExportTaskStatus.FINISHED + ); + } + + /** + * Collects the entities of the provided types for exporting. + * + * @param ofType The entity type. + * + * @return All entities of the provided type. + */ + private Set collectEntities( + final Class ofType + ) { + final CriteriaBuilder builder = entityManager.getCriteriaBuilder(); + final CriteriaQuery query = builder.createQuery(ofType); + final Root from = query.from(ofType); + + return new HashSet<>( + entityManager.createQuery( + query.select(from) + ).getResultList() + ); + } + + /** + * Handler function for processing the result of an export tasks. + * + * @param task The task. + * @param ex Exception thrown during the export process. If the export + * process run without errors, this parameter will be + * {@code null}. + * @param status The status of the task. + * + * @return The task. + * + * @see CompletionStage#handle(java.util.function.BiFunction) + */ + private Object handleExportTaskResult( + final ExportTask task, final Throwable ex, final ExportTaskStatus status + ) { + if (ex == null) { + status.setStatus(ImExportTaskStatus.FINISHED); + } else { + status.setStatus(ImExportTaskStatus.ERROR); + status.setException(ex); + LOGGER.error("Export Task {} failed ", task); + LOGGER.error("with exception:", ex); + } + return task; + } + + /** + * Handler function for processing the result of an import tasks. + * + * @param task The task. + * @param ex Exception thrown during the import process. If the import + * process run without errors, this parameter will be + * {@code null}. + * @param status The status of the task. + * + * @return The task. + * + * @see CompletionStage#handle(java.util.function.BiFunction) + */ + private Object handleImportTaskResult( + final ImportTask task, final Throwable ex, final ImportTaskStatus status + ) { + if (ex == null) { + status.setStatus(ImExportTaskStatus.FINISHED); + } else { + status.setStatus(ImExportTaskStatus.ERROR); + status.setException(ex); + LOGGER.error("Import Task {} failed", task); + LOGGER.error("with exception: ", ex); + } + return task; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportOption.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportOption.java new file mode 100644 index 000000000..a62f73a74 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportOption.java @@ -0,0 +1,66 @@ +/* + * 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.ImportManifest; + +import java.util.Objects; + +/** + * Provides a preprocessed {@link ImportManifest} for easier use in a template. + * + * @author Jens Pelzetter + */ +public class ImportOption implements Comparable { + + /** + * Name of the import. + */ + private final String importName; + + /** + * Label of the import, includes the relevant data from the + * {@link ImportManifest}. + */ + private final String label; + + public ImportOption( + final String importName, + final String label + ) { + this.importName = importName; + this.label = label; + } + + public String getImportName() { + return importName; + } + + public String getLabel() { + return label; + } + + @Override + public int compareTo(final ImportOption other) { + return importName.compareTo( + Objects.requireNonNull(other).getImportName() + ); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportTask.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportTask.java new file mode 100644 index 000000000..d3a9d217f --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportTask.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.ui.admin.imexport; + +import java.time.LocalDate; + +/** + * Data for an import task. + * + * @author Jens Pelzetter + */ +public class ImportTask { + + /** + * Name of the import archive. + */ + private final String name; + + /** + * When was the import task started? + */ + private final LocalDate started; + + /** + * The status of the import task. + */ + private final ImportTaskStatus status; + + public ImportTask( + final String name, + final LocalDate started, + final ImportTaskStatus status + ) { + this.name = name; + this.started = started; + this.status = status; + } + + public String getName() { + return name; + } + + public LocalDate getStarted() { + return started; + } + + public ImportTaskStatus getStatus() { + return status; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportTaskStatus.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportTaskStatus.java new file mode 100644 index 000000000..db9509f6c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/ImportTaskStatus.java @@ -0,0 +1,150 @@ +/* + * 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 import task. + * + * @author Jens Pelzetter + */ +public class ImportTaskStatus implements Comparable { + + /** + * Name of the import archive. + */ + private String name; + + /** + * When was the task started? + */ + private LocalDateTime started; + + /** + * Status of the import 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 = 53 * hash + Objects.hashCode(name); + hash = 53 * hash + Objects.hashCode(started); + hash = 53 * hash + Objects.hashCode(status); + return hash; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ExportTaskStatus)) { + return false; + } + final ImportTaskStatus other = (ImportTaskStatus) obj; + if (!other.canEqual(this)) { + return false; + } + if (!Objects.equals(name, other.getName())) { + return false; + } + if (!Objects.equals(started, other.getStarted())) { + return false; + } + return status == other.getStatus(); + } + + public boolean canEqual(final Object obj) { + return obj instanceof ImportTaskStatus; + } + + @Override + public int compareTo(final ImportTaskStatus other) { + return Comparator + .nullsFirst(Comparator + .comparing(ImportTaskStatus::getName) + .thenComparing(ImportTaskStatus::getStarted) + ) + .compare(this, other); + } + + @Override + public String toString() { + return String.format( + "%s{ " + + "name = %s, " + + "started = %s, " + + "status = %s" + + " }", + super.toString(), + name, + DateTimeFormatter.ISO_DATE_TIME.withZone( + ZoneId.systemDefault() + ).format(started), + Objects.toString(status) + ); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/package-info.java new file mode 100644 index 000000000..011755e60 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/imexport/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 importing and exporting entities. + */ +package org.libreccm.ui.admin.imexport; diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/package-info.java new file mode 100644 index 000000000..c60a8dc83 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/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 + */ +/** + * Base classes for the admin application. + */ +package org.libreccm.ui.admin; diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteDetailsModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteDetailsModel.java new file mode 100644 index 000000000..c3c3041c6 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteDetailsModel.java @@ -0,0 +1,127 @@ +/* + * 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.sites; + +import org.libreccm.sites.Site; +import org.libreccm.theming.ThemeInfo; +import org.libreccm.ui.Message; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Named; + +/** + * Model providing the properties of a site for the UI. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("SiteDetailsModel") +public class SiteDetailsModel { + + private long siteId; + + private String uuid; + + private String identifier; + + private String domain; + + private boolean defaultSite; + + private String defaultTheme; + + private Map availableThemes; + + private List messages; + + public long getSiteId() { + return siteId; + } + + public String getUuid() { + return uuid; + } + + public String getIdentifier() { + return identifier; + } + + public boolean isNew() { + return siteId == 0; + } + + public String getDomain() { + return domain; + } + + public boolean isDefaultSite() { + return defaultSite; + } + + public String getDefaultTheme() { + return defaultTheme; + } + + public Map getAvailableThemes() { + return Collections.unmodifiableMap(availableThemes); + } + + public List getMessages() { + return Collections.unmodifiableList(messages); + } + + protected void addMessage(final Message message) { + messages.add(message); + } + + /** + * Set the properties of this model to the values of the properties of the + * provided site. + * + * @param site The site. + */ + protected void setSite(final Site site) { + Objects.requireNonNull(site); + + siteId = site.getObjectId(); + uuid = site.getUuid(); + identifier = String.format("ID-%d", siteId); + domain = site.getDomainOfSite(); + defaultSite = site.isDefaultSite(); + defaultTheme = site.getDefaultTheme(); + } + + protected void setAvailableThemes(final List availableThemes) { + this.availableThemes = availableThemes + .stream() + .collect( + Collectors.toMap( + themeInfo -> themeInfo.getName(), + themeInfo -> themeInfo.getName() + ) + ); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteFormController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteFormController.java new file mode 100644 index 000000000..9343f8c45 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteFormController.java @@ -0,0 +1,176 @@ +/* + * 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.sites; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.sites.Site; +import org.libreccm.sites.SiteRepository; +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 for processing the {@code POST} requests from the form for + * creating and editing sites. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/sites") +public class SiteFormController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private SiteDetailsModel siteDetailsModel; + + @Inject + private SiteRepository siteRepository; + + @Inject + private IdentifierParser identifierParser; + + @FormParam("domain") + private String domainOfSite; + + @FormParam("defaultSite") + private String defaultSite; + + @FormParam("defaultTheme") + private String defaultTheme; + + /** + * Create a new site. + * + * @return Redirect to the sites overview. + */ + @POST + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String createSite() { + final Site site = new Site(); + site.setDomainOfSite(domainOfSite); + if (defaultSite != null) { + resetDefaultSite(); + site.setDefaultSite(true); + } + site.setDefaultTheme(defaultTheme); + + siteRepository.save(site); + + return "redirect:sites"; + } + + /** + * Update a site with the data from the form. + * + * @param siteIdentifierParam The identifier of the site to update. + * @return Redirect to the details page of the site. + */ + @POST + @Path("/{siteIdentifier}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateSite( + @PathParam("siteIdentifier") final String siteIdentifierParam + ) { + final Identifier siteIdentifier = identifierParser.parseIdentifier( + siteIdentifierParam + ); + + final Optional result; + switch (siteIdentifier.getType()) { + case ID: + result = siteRepository.findById( + Long.parseLong(siteIdentifier.getIdentifier()) + ); + break; + default: + result = siteRepository.findByUuid( + siteIdentifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + final Site site = result.get(); + + site.setDomainOfSite(domainOfSite); + site.setDefaultTheme(defaultTheme); + + final boolean isDefaultSite = defaultSite != null; + if (isDefaultSite != site.isDefaultSite()) { + resetDefaultSite(); + site.setDefaultSite(isDefaultSite); + } + siteRepository.save(site); + + return String.format( + "redirect:sites/ID-%d/details", site.getObjectId() + ); + } else { + siteDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "sites.not_found_message", + Arrays.asList(siteIdentifierParam) + ), + MessageType.WARNING + ) + ); + + return "org/libreccm/ui/admin/sites/site-not-found.xhtml"; + } + } + + /** + * Helper method for resetting the default site of an installation. + */ + private void resetDefaultSite() { + final Optional result = siteRepository + .findDefaultSite(); + if (result.isPresent()) { + final Site site = result.get(); + site.setDefaultSite(false); + siteRepository.save(site); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteTableRow.java b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteTableRow.java new file mode 100644 index 000000000..98d942603 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SiteTableRow.java @@ -0,0 +1,82 @@ +/* + * 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.sites; + +/** + * Data for a row in the table showing all available sites. + * + * @author Jens Pelzetter + */ +public class SiteTableRow { + + private long siteId; + + private String uuid; + + private String domain; + + private boolean defaultSite; + + private String defaultTheme; + + public long getSiteId() { + return siteId; + } + + protected void setSiteId(final long siteId) { + this.siteId = siteId; + } + + public String getUuid() { + return uuid; + } + + protected void setUuid(final String uuid) { + this.uuid = uuid; + } + + public String getIdentifier() { + return String.format("ID-%d", siteId); + } + + public String getDomain() { + return domain; + } + + protected void setDomain(final String domain) { + this.domain = domain; + } + + public boolean isDefaultSite() { + return defaultSite; + } + + protected void setDefaultSite(final boolean defaultSite) { + this.defaultSite = defaultSite; + } + + public String getDefaultTheme() { + return defaultTheme; + } + + protected void setDefaultTheme(final String defaultTheme) { + this.defaultTheme = defaultTheme; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SitesController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SitesController.java new file mode 100644 index 000000000..06c4e3fee --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SitesController.java @@ -0,0 +1,285 @@ +/* + * 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.sites; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.sites.Site; +import org.libreccm.sites.SiteRepository; +import org.libreccm.theming.Themes; +import org.libreccm.ui.Message; +import org.libreccm.ui.MessageType; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +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; + +/** + * Primary controller for the UI for managing sites. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/sites") +public class SitesController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private SiteDetailsModel siteDetailsModel; + + @Inject + private SiteRepository siteRepository; + + @Inject + private Themes themes; + + /** + * Show all available sites. + * + * @return The template to use. + */ + @GET + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String getSites() { + final List sites = siteRepository.findAll(); + models.put( + "sites", + sites + .stream() + .map(this::buildSiteTableRow) + .collect(Collectors.toList()) + ); + + return "org/libreccm/ui/admin/sites/sites.xhtml"; + } + + /** + * Show the details of a site. + * + * @param siteIdentifierParam Identifier of the site to show. + * + * @return The template to use. + */ + @GET + @Path("/{siteIdentifier}/details") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String getSite( + @PathParam("siteIdentifier") final String siteIdentifierParam + ) { + final Identifier siteIdentifier = identifierParser.parseIdentifier( + siteIdentifierParam + ); + + final Optional result; + switch (siteIdentifier.getType()) { + case ID: + result = siteRepository.findById( + Long.parseLong(siteIdentifier.getIdentifier()) + ); + break; + default: + result = siteRepository.findByUuid( + siteIdentifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + siteDetailsModel.setSite(result.get()); + siteDetailsModel.setAvailableThemes(themes.getAvailableThemes()); + + return "org/libreccm/ui/admin/sites/site-details.xhtml"; + } else { + siteDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "sites.not_found_message", + Arrays.asList(siteIdentifierParam) + ), + MessageType.WARNING + ) + ); + + return "org/libreccm/ui/admin/sites/site-not-found.xhtml"; + } + } + + /** + * Show the form for creating a new site. + * + * @return The template to use. + */ + @GET + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String createNewSite() { + siteDetailsModel.setAvailableThemes(themes.getAvailableThemes()); + return "org/libreccm/ui/admin/sites/site-form.xhtml"; + } + + /** + * Show the form for editing a site. + * + * @param siteIdentifierParam The identifier of the site to edit. + * + * @return The template to use. + */ + @GET + @Path("/{siteIdentifier}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String editSite( + @PathParam("siteIdentifier") final String siteIdentifierParam + ) { + final Identifier siteIdentifier = identifierParser.parseIdentifier( + siteIdentifierParam + ); + + final Optional result; + switch (siteIdentifier.getType()) { + case ID: + result = siteRepository.findById( + Long.parseLong(siteIdentifier.getIdentifier()) + ); + break; + default: + result = siteRepository.findByUuid( + siteIdentifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + siteDetailsModel.setSite(result.get()); + siteDetailsModel.setAvailableThemes(themes.getAvailableThemes()); + + return "org/libreccm/ui/admin/sites/site-form.xhtml"; + } else { + siteDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "sites.not_found_message", + Arrays.asList(siteIdentifierParam) + ), + MessageType.WARNING + ) + ); + + return "org/libreccm/ui/admin/sites/site-not-found.xhtml"; + } + } + + /** + * Delete a site. + * + * @param siteIdentifierParam The identifier of the site to delete. + * @param confirmed Was the deletion confirmed by the user? + * + * @return Redirect to the list of all available sites. + */ + @POST + @Path("/{identifier}/delete") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String deleteSite( + @PathParam("identifier") final String siteIdentifierParam, + @FormParam("confirmed") final String confirmed + ) { + if ("true".equals(confirmed)) { + final Identifier siteIdentifier = identifierParser.parseIdentifier( + siteIdentifierParam + ); + + final Optional result; + switch (siteIdentifier.getType()) { + case ID: + result = siteRepository.findById( + Long.parseLong(siteIdentifier.getIdentifier()) + ); + break; + default: + result = siteRepository.findByUuid( + siteIdentifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + siteRepository.delete(result.get()); + } + } + + return "redirect:sites"; + } + + /** + * Helper method for building a + * {@link org.libreccm.ui.admin.sites.SiteTableRow} instance for a + * {@link Site}. + * + * @param site The site. + * + * @return A {@link SiteTableRow} instance for the site. + */ + private SiteTableRow buildSiteTableRow(final Site site) { + final SiteTableRow row = new SiteTableRow(); + row.setSiteId(site.getObjectId()); + row.setUuid(site.getUuid()); + row.setDomain(site.getDomainOfSite()); + row.setDefaultSite(site.isDefaultSite()); + row.setDefaultTheme(site.getDefaultTheme()); + + return row; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SitesPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SitesPage.java new file mode 100644 index 000000000..d9f85c4c9 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/SitesPage.java @@ -0,0 +1,80 @@ +/* + * 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.sites; + +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 sites. + * + * @author Jens Pelzetter + */ +@ApplicationScoped +public class SitesPage implements AdminPage { + @Override + public Set> getControllerClasses() { + final Set> classes = new HashSet<>(); + classes.add(SitesController.class); + classes.add(SiteFormController.class); + return classes; + } + + @Override + public String getUriIdentifier() { + return String.format( + "%s#getSites", SitesController.class.getSimpleName() + ); + } + + @Override + public String getLabelBundle() { + return AdminConstants.ADMIN_BUNDLE; + } + + @Override + public String getLabelKey() { + return "sites.label"; + } + + @Override + public String getDescriptionBundle() { + return AdminConstants.ADMIN_BUNDLE; + } + + @Override + public String getDescriptionKey() { + return "sites.description"; + } + + @Override + public String getIcon() { + return "collection"; + } + + @Override + public int getPosition() { + return 50; + } +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/sites/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/package-info.java new file mode 100644 index 000000000..2326c0214 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/sites/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 sites. + */ +package org.libreccm.ui.admin.sites; diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationController.java new file mode 100644 index 000000000..97e038520 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationController.java @@ -0,0 +1,55 @@ +/* + * 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.systeminformation; + +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.mvc.Controller; +import javax.mvc.MvcContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +/** + * Controller for the systeminformations page. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/systeminformation") +public class SystemInformationController { + + /** + * Show the system information page. + * + * @return The template to use. + */ + @GET + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String getSystemInformation() { + return "org/libreccm/ui/admin/systeminformation.xhtml"; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationModel.java new file mode 100644 index 000000000..146a31ea1 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationModel.java @@ -0,0 +1,90 @@ +/* + * 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.systeminformation; + +import com.arsdigita.util.UncheckedWrapperException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Named; + +/** + * Model providing the date for the system information page. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("SystemInformationModel") +public class SystemInformationModel { + + /** + * Get some data about this LibreCCM installation, eg. version, application + * name, and homepage. + * + * @return The information about this CCM installation. + */ + public Map getCcmSystemInformation() { + final Properties properties = new Properties(); + try { + final InputStream stream = getClass().getResourceAsStream( + "systeminformation.properties"); + if (stream == null) { + properties.put("version", ""); + properties.put("appname", "LibreCCM"); + properties.put("apphomepage", "http://www.libreccm.org"); + } else { + properties.load(stream); + } +// properties.load(getClass().getResourceAsStream( +// "WEB-INF/systeminformation.properties")); + } catch (IOException ex) { + throw new UncheckedWrapperException(ex); + } + + final Map sysInfo = new HashMap<>(); + + for (String key : properties.stringPropertyNames()) { + sysInfo.put(key, properties.getProperty(key)); + } + + return sysInfo; + } + + /** + * Get the Java System Properties from the runtime environment. + * + * @return The Java System Properties of the runtime environment. + */ + public Map getJavaSystemProperties() { + final Properties systemProperties = System.getProperties(); + final Map result = new HashMap<>(); + for (final Object key : systemProperties.keySet()) { + result.put( + (String) key, systemProperties.getProperty((String) key) + ); + } + return result; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationPage.java new file mode 100644 index 000000000..151aceff4 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/SystemInformationPage.java @@ -0,0 +1,82 @@ +/* + * 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.systeminformation; + +import java.util.HashSet; +import java.util.Set; + +import org.libreccm.ui.admin.AdminConstants; +import org.libreccm.ui.admin.AdminPage; + +import javax.enterprise.context.ApplicationScoped; + +/** + * {@link AdminPage} implementaton for the system information page. + * + * @author Jens Pelzetter + */ +@ApplicationScoped +public class SystemInformationPage implements AdminPage { + + @Override + public Set> getControllerClasses() { + final Set> classes = new HashSet<>(); + classes.add(SystemInformationController.class); + return classes; + } + + @Override + public String getUriIdentifier() { + return String.format( + "%s#getSystemInformation", + SystemInformationController.class.getSimpleName() + ); + } + + @Override + public String getLabelBundle() { + return AdminConstants.ADMIN_BUNDLE; + } + + @Override + public String getLabelKey() { + return "systeminformation.label"; + } + + @Override + public String getDescriptionBundle() { + return AdminConstants.ADMIN_BUNDLE; + } + + @Override + public String getDescriptionKey() { + return "systeminformation.description"; + } + + @Override + public String getIcon() { + return "info-circle-fill"; + } + + @Override + public int getPosition() { + return 80; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/package-info.java new file mode 100644 index 000000000..a78260d3b --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/systeminformation/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 inspecting some information about LibreCCM and the runtime environment. + */ +package org.libreccm.ui.admin.systeminformation; diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesController.java new file mode 100644 index 000000000..8036e6c5c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesController.java @@ -0,0 +1,143 @@ +/* + * 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.themes; + +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.theming.Themes; + +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; + +/** + * Primary controller for the UI for managing themes. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/themes") +public class ThemesController { + + @Inject + private Themes themes; + + @Inject + private Models models; + + /** + * Show all available themes. + * + * @return The template to use. + */ + @GET + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String getThemes() { + return "org/libreccm/ui/admin/themes/themes.xhtml"; + } + + /** + * Create a new theme. + * + * @param themeName The name of the new theme. + * @param providerName The provider of the new theme. + * + * @return Redirect to the list of available themes. + */ + @POST + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String createTheme( + @FormParam("themeName") final String themeName, + @FormParam("providerName") final String providerName + ) { + themes.createTheme(themeName, providerName); + + return "redirect:themes/"; + } + + /** + * (Re-)Publish a theme. + * + * @param themeName The theme to (re-)publish. + * + * @return Redirect to the list of themes. + */ + @POST + @Path("/{themeName}/publish") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String publishTheme(final String themeName) { + themes.publishTheme(themeName); + + return "redirect:themes/"; + } + + /** + * Unpublish a theme. + * + * @param themeName The theme to unpublish. + * + * @return Redirect to the list of themes. + */ + @POST + @Path("/{themeName}/unpublish") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String unpublishTheme(final String themeName) { + themes.unpublishTheme(themeName); + + return "redirect:themes/"; + } + + /** + * Delete a theme. + * + * @param themeName The theme to delete. + * + * @return Redirect to the list of themes. + */ + @POST + @Path("/{themeName}/delete") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String deleteTheme(@PathParam("themeName") final String themeName) { + themes.deleteTheme(themeName); + + return "redirect:themes/"; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesModel.java new file mode 100644 index 000000000..ffa55f47c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesModel.java @@ -0,0 +1,143 @@ +/* + * 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.themes; + +import org.libreccm.l10n.GlobalizationHelper; +import org.libreccm.l10n.LocalizedTextsUtil; +import org.libreccm.theming.ThemeInfo; +import org.libreccm.theming.ThemeProvider; +import org.libreccm.theming.ThemeVersion; +import org.libreccm.theming.Themes; +import org.libreccm.ui.admin.AdminConstants; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; + +/** + * Model for the themes page. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("Themes") +public class ThemesModel { + + @Inject + private GlobalizationHelper globalizationHelper; + + @Inject + private Themes themes; + + /** + * Get all available themes. + * + * @return A list of all available themes. + */ + public List getThemes() { + return themes + .getAvailableThemes() + .stream() + .map(this::mapThemeInfo) + .collect(Collectors.toList()); + } + + /** + * Get all available {@link org.libreccm.theming.ThemeProvider}s which + * support changes and draft themes. + * + * @return + */ + public Map getProviderOptions() { + return themes + .getThemeProviders() + .stream() + .filter(ThemeProvider::supportsChanges) + .filter(ThemeProvider::supportsDraftThemes) + .collect( + Collectors.toMap( + provider -> provider.getClass().getName(), + provider -> provider.getName() + ) + ); + } + + /** + * Helper function for mapping a {@link ThemeInfo} instance to a + * {@link ThemesTableRow} instance. + * + * @param themeInfo The {@link ThemeInfo} instance to map. + * + * @return A {@link ThemesTableRow} instance for the theme. + */ + private ThemesTableRow mapThemeInfo(final ThemeInfo themeInfo) { + final LocalizedTextsUtil textsUtil = globalizationHelper + .getLocalizedTextsUtil(AdminConstants.ADMIN_BUNDLE); + + final ThemesTableRow row = new ThemesTableRow(); + row.setDescription( + Optional + .ofNullable(themeInfo.getManifest().getDescription()) + .map(ls -> globalizationHelper.getValueFromLocalizedString(ls)) + .orElse("") + ); + row.setName(themeInfo.getName()); + row.setProvider(themeInfo.getProvider().getName()); + row.setTitle( + Optional + .ofNullable(themeInfo.getManifest().getTitle()) + .map(ls -> globalizationHelper.getValueFromLocalizedString(ls)) + .orElse("") + ); + row.setType(themeInfo.getType()); + row.setVersion( + textsUtil.getText( + String.format( + "themes.versions.%s", + Objects.toString( + themeInfo.getVersion()).toLowerCase(Locale.ROOT) + ) + ) + ); + + row.setPublished(themeInfo.getVersion() == ThemeVersion.LIVE); + + final Optional themeProviderResult = themes + .findThemeProviderInstance(themeInfo.getProvider()); + if (themeProviderResult.isPresent()) { + final ThemeProvider themeProvider = themeProviderResult.get(); + + row.setEditable(themeProvider.supportsChanges()); + row.setPublishable(themeProvider.supportsDraftThemes()); + } else { + row.setEditable(false); + row.setPublishable(false); + } + + return row; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesPage.java new file mode 100644 index 000000000..bcee4b564 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesPage.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.themes; + +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 themes. + * + * @author Jens Pelzetter + */ +@ApplicationScoped +public class ThemesPage implements AdminPage { + + @Override + public Set> getControllerClasses() { + final Set> classes = new HashSet<>(); + classes.add(ThemesController.class); + return classes; + } + + @Override + public String getUriIdentifier() { + return String.format( + "%s#getThemes", ThemesController.class.getSimpleName() + ); + } + + @Override + public String getLabelBundle() { + return AdminConstants.ADMIN_BUNDLE; + } + + @Override + public String getLabelKey() { + return "themes.label"; + } + + @Override + public String getDescriptionBundle() { + return AdminConstants.ADMIN_BUNDLE; + } + + @Override + public String getDescriptionKey() { + return "themes.description"; + } + + @Override + public String getIcon() { + return "brush-fill"; + } + + @Override + public int getPosition() { + return 70; + } + + + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesTableRow.java b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesTableRow.java new file mode 100644 index 000000000..3796bbd68 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/ThemesTableRow.java @@ -0,0 +1,149 @@ +/* + * 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.themes; + +import org.libreccm.theming.ThemeVersion; + +import java.io.Serializable; +import java.util.Comparator; + +/** + * Data of theme for displaying in the table of available themes. + * + * @author Jens Pelzetter + */ +public class ThemesTableRow implements Comparable, Serializable { + + private static final long serialVersionUID = 1L; + + private String name; + + private String type; + + private ThemeVersion themeVersion; + + private String version; + + private String provider; + + private String title; + + private String description; + + private boolean editable; + + private boolean publishable; + + private boolean published; + + public String getName() { + return name; + } + + protected void setName(final String name) { + this.name = name; + } + + public ThemeVersion getThemeVersion() { + return themeVersion; + } + + protected void setThemeVersion(final ThemeVersion themeVersion) { + this.themeVersion = themeVersion; + } + + public String getType() { + return type; + } + + protected void setType(final String type) { + this.type = type; + } + + public String getVersion() { + return version; + } + + protected void setVersion(final String version) { + this.version = version; + } + + public String getProvider() { + return provider; + } + + protected void setProvider(final String provider) { + this.provider = provider; + } + + 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 isEditable() { + return editable; + } + + protected void setEditable(final boolean editable) { + this.editable = editable; + } + + public boolean isPublishable() { + return publishable; + } + + protected void setPublishable(final boolean publishable) { + this.publishable = publishable; + } + + public boolean isPublished() { + return published; + } + + protected void setPublished(final boolean published) { + this.published = published; + } + + @Override + public int compareTo(final ThemesTableRow other) { + return Comparator.nullsFirst(Comparator + .comparing(ThemesTableRow::getTitle) + .thenComparing(ThemesTableRow::getName) + .thenComparing(ThemesTableRow::getVersion) + .thenComparing(ThemesTableRow::getType) + .thenComparing(ThemesTableRow::getProvider) + .thenComparing(ThemesTableRow::isPublishable) + .thenComparing(ThemesTableRow::isPublished) + .thenComparing(ThemesTableRow::isEditable) + ).compare(this, other); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/themes/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/package-info.java new file mode 100644 index 000000000..27d9497d3 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/themes/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 themes. + */ +package org.libreccm.ui.admin.themes; diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/EmailFormController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/EmailFormController.java new file mode 100644 index 000000000..1aec4519c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/EmailFormController.java @@ -0,0 +1,353 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.core.EmailAddress; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.User; +import org.libreccm.security.UserRepository; +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.mvc.Models; +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 managing the post requests from the email edit form. + * + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path( + "/users-groups-roles/users/{userIdentifier}/email-addresses/{emailIdentifier}/save" +) +public class EmailFormController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private EmailFormModel emailFormModel; + + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private UserDetailsModel userDetailsModel; + + @Inject + private UserRepository userRepository; + + // MvcBinding does not work with Krazo 1.1.0-M1 +// @MvcBinding + @FormParam("address") +// @NotBlank +// @Email + private String address; + + @FormParam("bouncing") + private String bouncingParam; + + @FormParam("verified") + private String verifiedParam; + + @POST + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String saveEmailAddress( + @PathParam("userIdentifier") final String userIdentifierParam, + @PathParam("emailIdentifier") final String emailIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + final User user = result.get(); + + // MvcBinding does not work with Krazo 1.1.0-M1 +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// emailFormModel.setUserIdentifier(userIdentifierParam); +// emailFormModel.setAddress(address); +// emailFormModel.setBouncing(bouncing); +// emailFormModel.setVerified(verified); +// +// return "org/libreccm/ui/admin/users-groups-roles/email-form.xhtml"; +// } + if (address == null || address.matches("\\s*")) { + emailFormModel.addMessage( + new Message( + "usergroupsroles.users.user_details.email_addresses.errors.address_empty", + MessageType.DANGER) + ); + emailFormModel.setUserIdentifier(userIdentifierParam); + emailFormModel.setAddress(address); + emailFormModel.setBouncing(bouncingParam != null); + emailFormModel.setVerified(verifiedParam != null); + + return "org/libreccm/ui/admin/users-groups-roles/email-form.xhtml"; + } + + if ("new".equals(emailIdentifierParam)) { + return addEmailAddress(user); + } else { + return updateEmailAddress( + userIdentifierParam, + user, + Integer.parseInt(emailIdentifierParam) + ); + } + + } else { + userDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + + private String addEmailAddress(final User user) { + final EmailAddress emailAddress = new EmailAddress(); + emailAddress.setAddress(address); + emailAddress.setBouncing(bouncingParam != null); + emailAddress.setVerified(verifiedParam != null); + user.addEmailAddress(emailAddress); + + userRepository.save(user); + + return String.format( + "redirect:/users-groups-roles/users/%s/details", + user.getName() + ); + } + + private String updateEmailAddress( + final String userIdentifierParam, + final User user, + final int emailId + ) { + if (user.getEmailAddresses().size() <= emailId) { + models.put("error.userIdentifier", userIdentifierParam); + models.put("error.emailId", emailId); + return "org/libreccm/ui/admin/users-groups-roles/email-not-found.xhtml"; + } else { + final EmailAddress emailAddress = user + .getEmailAddresses() + .get(emailId); + + emailAddress.setAddress(address); + emailAddress.setBouncing(bouncingParam != null); + emailAddress.setVerified(verifiedParam != null); + + userRepository.save(user); + + return String.format( + "redirect:/users-groups-roles/users/%s/details", + user.getName() + ); + } + } + +// @POST +// @Path("/users-groups-roles/users/{userIdentifier}/email-addresses/new") +// @AuthorizationRequired +// @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) +// @Transactional(Transactional.TxType.REQUIRED) +// public String addNewEmailAddress( +// @PathParam("userIdentifier") final String userIdentifierParam +// ) { +// final Identifier identifier = identifierParser.parseIdentifier( +// userIdentifierParam +// ); +// final Optional result; +// switch (identifier.getType()) { +// case ID: +// result = userRepository.findById( +// Long.parseLong(identifier.getIdentifier()) +// ); +// break; +// case UUID: +// result = userRepository.findByUuid( +// identifier.getIdentifier() +// ); +// break; +// default: +// result = userRepository.findByName(identifier.getIdentifier()); +// break; +// } +// +// if (result.isPresent()) { +// final User user = result.get(); +// +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// emailFormModel.setUserIdentifier(userIdentifierParam); +// emailFormModel.setAddress(address); +// emailFormModel.setBouncing(bouncing); +// emailFormModel.setVerified(verified); +// +// return "org/libreccm/ui/admin/users-groups-roles/email-form.xhtml"; +// } +// +// final EmailAddress emailAddress = new EmailAddress(); +// emailAddress.setAddress(address); +// emailAddress.setBouncing(bouncing); +// emailAddress.setVerified(verified); +// user.addEmailAddress(emailAddress); +// +// userRepository.save(user); +// +// return String.format( +// "redirect:/users-groups-roles/users/%s/details", +// user.getName() +// ); +// } else { +// userDetailsModel.addMessage( +// new Message( +// adminMessages.getMessage( +// "usersgroupsroles.users.not_found.message", +// Arrays.asList(userIdentifierParam) +// ), +// MessageType.WARNING +// ) +// ); +// return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; +// } +// } +// +// @POST +// @Path("/users-groups-roles/users/{userIdentifier}/email-addresses/{emailId}") +// @AuthorizationRequired +// @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) +// @Transactional(Transactional.TxType.REQUIRED) +// public String updateEmailAddress( +// @PathParam("userIdentifier") final String userIdentifierParam, +// @PathParam("emailId") final int emailId +// ) { +// final Identifier identifier = identifierParser.parseIdentifier( +// userIdentifierParam +// ); +// final Optional result; +// switch (identifier.getType()) { +// case ID: +// result = userRepository.findById( +// Long.parseLong(identifier.getIdentifier()) +// ); +// break; +// case UUID: +// result = userRepository.findByUuid( +// identifier.getIdentifier() +// ); +// break; +// default: +// result = userRepository.findByName(identifier.getIdentifier()); +// break; +// } +// +// if (result.isPresent()) { +// final User user = result.get(); +// +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// emailFormModel.setUserIdentifier(userIdentifierParam); +// emailFormModel.setEmailId(emailId); +// emailFormModel.setAddress(address); +// emailFormModel.setBouncing(bouncing); +// emailFormModel.setVerified(verified); +// +// return "org/libreccm/ui/admin/users-groups-roles/email-form.xhtml"; +// } +// +// if (user.getEmailAddresses().size() <= emailId) { +// models.put("error.userIdentifier", userIdentifierParam); +// models.put("error.emailId", emailId); +// return "org/libreccm/ui/admin/users-groups-roles/email-not-found.xhtml"; +// } else { +// final EmailAddress emailAddress = user +// .getEmailAddresses() +// .get(emailId); +// +// emailAddress.setAddress(address); +// emailAddress.setBouncing(bouncing); +// emailAddress.setVerified(verified); +// +// userRepository.save(user); +// +// return "org/libreccm/ui/admin/users-groups-roles/email-form.xhtml"; +// } +// } else { +// userDetailsModel.addMessage( +// new Message( +// adminMessages.getMessage( +// "usersgroupsroles.users.not_found.message", +// Arrays.asList(userIdentifierParam) +// ), +// MessageType.WARNING +// ) +// ); +// +// return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; +// } +// } +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/EmailFormModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/EmailFormModel.java new file mode 100644 index 000000000..8b7d6b0eb --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/EmailFormModel.java @@ -0,0 +1,126 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.core.EmailAddress; +import org.libreccm.ui.Message; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Named; + +/** + * Model providing the data for the email edit form. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("EmailFormModel") +public class EmailFormModel { + + private String userIdentifier; + + private int emailId = -1; + + private String address; + + private boolean bouncing; + + private boolean verified; + + private List messages; + + public EmailFormModel() { + this.messages = new ArrayList<>(); + } + + public List getMessages() { + return Collections.unmodifiableList(messages); + } + + public void addMessage(final Message message) { + messages.add(message); + } + + public void setMessages(final List messages) { + this.messages = new ArrayList<>(messages); + } + + public void setEmailAddress( + final String userIdentifier, + final int emailId, + final EmailAddress emailAddress + ) { + this.userIdentifier = userIdentifier; + this.emailId = emailId; + address = emailAddress.getAddress(); + bouncing = emailAddress.isBouncing(); + verified = emailAddress.isVerified(); + } + + public boolean isNew() { + return emailId == -1; + } + + public String getUserIdentifier() { + return userIdentifier; + } + + public void setUserIdentifier(final String userIdentifier) { + this.userIdentifier = userIdentifier; + } + + public int getEmailId() { + return emailId; + } + + public void setEmailId(final int emailId) { + this.emailId = emailId; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public boolean isBouncing() { + return bouncing; + } + + public void setBouncing(boolean bouncing) { + this.bouncing = bouncing; + } + + public boolean isVerified() { + return verified; + } + + public void setVerified(boolean verified) { + this.verified = verified; + } + + + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupDetailsModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupDetailsModel.java new file mode 100644 index 000000000..ba2255ae9 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupDetailsModel.java @@ -0,0 +1,177 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.security.Group; +import org.libreccm.security.GroupMembership; +import org.libreccm.security.Role; +import org.libreccm.security.RoleMembership; +import org.libreccm.security.RoleRepository; +import org.libreccm.security.User; +import org.libreccm.security.UserRepository; +import org.libreccm.ui.Message; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.transaction.Transactional; + +/** + * Model used by the group details form and the group edit form. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("GroupDetailsModel") +public class GroupDetailsModel { + + @Inject + private RoleRepository roleRepository; + + @Inject + private UserRepository userRepository; + + private long groupId; + + private String uuid; + + private String groupName; + + private List members; + + private List roles; + + private final List messages; + + public GroupDetailsModel() { + messages = new ArrayList<>(); + } + + public List getMessages() { + return Collections.unmodifiableList(messages); + } + + public void addMessage(final Message message) { + messages.add(message); + } + + public long getGroupId() { + return groupId; + } + + public String getUuid() { + return uuid; + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(final String groupName) { + this.groupName = groupName; + } + + public List getMembers() { + return Collections.unmodifiableList(members); + } + + public List getRoles() { + return Collections.unmodifiableList(roles); + } + + public List getGroupMemberFormEntries() { + return userRepository + .findAll() + .stream() + .map(this::buildGroupUserFormEntry) + .collect(Collectors.toList()); + } + + public List getGroupRolesFormEntries() { + return roleRepository + .findAll() + .stream() + .map(this::buildGroupRolesFormEntry) + .collect(Collectors.toList()); + } + + @Transactional(Transactional.TxType.REQUIRED) + protected void setGroup(final Group group) { + Objects.requireNonNull(group); + + groupId = group.getPartyId(); + uuid = group.getUuid(); + groupName = group.getName(); + members = group + .getMemberships() + .stream() + .map(GroupMembership::getMember) + .map(GroupUserMembership::new) + .sorted() + .collect(Collectors.toList()); + roles = group + .getRoleMemberships() + .stream() + .map(RoleMembership::getRole) + .map(PartyRoleMembership::new) + .sorted() + .collect(Collectors.toList()); + } + + public boolean isNewGroup() { + return groupId == 0; + } + + private GroupUserFormEntry buildGroupUserFormEntry(final User user) { + final GroupUserFormEntry entry = new GroupUserFormEntry(); + entry.setUserId(user.getPartyId()); + entry.setUserName(user.getName()); + entry.setUserUuid(user.getUuid()); + entry.setMember( + members + .stream() + .anyMatch( + membership -> membership.getUserUuid().equals(user.getUuid()) + ) + ); + return entry; + } + + private PartyRolesFormEntry buildGroupRolesFormEntry(final Role role) { + final PartyRolesFormEntry entry = new PartyRolesFormEntry(); + entry.setRoleId(role.getRoleId()); + entry.setRoleName(role.getName()); + entry.setRoleUuid(role.getUuid()); + entry.setMember( + roles + .stream() + .anyMatch( + membership -> membership.getRoleUuid().equals(role.getUuid()) + ) + ); + return entry; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupFormController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupFormController.java new file mode 100644 index 000000000..90297e926 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupFormController.java @@ -0,0 +1,193 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.Group; +import org.libreccm.security.GroupRepository; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +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.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +/** + * Controller for processing the POST requests from the group form. Depending + * on the value returned by {@link GroupDetailsModel#isNewGroup()} a new group + * is created or an existing group is updated. + * + * @author Jens Pelzetter + */ +@Controller +@Path("/users-groups-roles/groups/") +@RequestScoped +public class GroupFormController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private GroupDetailsModel groupDetailsModel; + + // MvcBinding does not work with Krazo 1.1.0-M1 +// @Inject +// private BindingResult bindingResult; + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private GroupRepository groupRepository; + + // MvcBinding does not work with Krazo 1.1.0-M1 +// @MvcBinding + @FormParam("groupName") +// @NotBlank + private String groupName; + + @POST + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String createGroup() { + // MvcBinding does not work with Krazo 1.1.0-M1 +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// return "org/libreccm/ui/admin/users-groups-roles/group-form.xhtml"; +// } + final List errors = new ArrayList<>(); + if (groupName == null || groupName.matches("\\s*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.groups.form.errors.name_not_empty" + ) + ); + } + if (!groupName.matches("[a-zA-Z0-9_-]*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.groups.form.errors.name_invalid" + ) + ); + } + if (!errors.isEmpty()) { + models.put("errors", errors); + groupDetailsModel.setGroupName(groupName); + return "org/libreccm/ui/admin/users-groups-roles/group-form.xhtml"; + } + + final Group group = new Group(); + group.setName(groupName); + groupRepository.save(group); + + return "redirect:users-groups-roles/groups"; + } + + @POST + @Path("{groupIdentifier}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateGroup( + @PathParam("groupIdentifier") final String groupIdentifierParam + ) { + + final Identifier identifier = identifierParser.parseIdentifier( + groupIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = groupRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = groupRepository.findByUuid(identifier.getIdentifier()); + break; + default: + result = groupRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + final Group group = result.get(); + + // MvcBinding does not work with Krazo 1.1.0-M1 +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// return "org/libreccm/ui/admin/users-groups-roles/group-form.xhtml"; +// } + final List errors = new ArrayList<>(); + if (groupName == null || groupName.matches("\\s*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.groups.form.errors.name_not_empty" + ) + ); + } + if (!groupName.matches("[a-zA-Z_-]*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.groups.form.errors.name_invalid" + ) + ); + } + if (!errors.isEmpty()) { + models.put("errors", errors); + groupDetailsModel.setGroup(group); + return "org/libreccm/ui/admin/users-groups-roles/group-form.xhtml"; + } + + group.setName(groupName); + + groupRepository.save(group); + return "redirect:users-groups-roles/groups"; + } else { + models.put( + "errors", Arrays.asList( + adminMessages.getMessage( + "usersgroupsroles.groups.not_found.message", + Arrays.asList(groupIdentifierParam) + ) + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml"; + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupMembersRolesController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupMembersRolesController.java new file mode 100644 index 000000000..a7bd8978c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupMembersRolesController.java @@ -0,0 +1,288 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.Group; +import org.libreccm.security.GroupManager; +import org.libreccm.security.GroupRepository; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.Role; +import org.libreccm.security.RoleManager; +import org.libreccm.security.RoleRepository; +import org.libreccm.security.User; +import org.libreccm.security.UserRepository; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +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.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +/** + * Controller for adding members to a group or removing members from a group + * based on the selections in the member form (dialog in group-details). + * + * @author Jens Pelzetter + */ +@Controller +@Path("/users-groups-roles/groups/") +@RequestScoped +public class GroupMembersRolesController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private IdentifierParser identifierParser; + + @Inject + private GroupManager groupManager; + + @Inject + private GroupRepository groupRepository; + + @Inject + private Models models; + + @Inject + private RoleManager roleManager; + + @Inject + private RoleRepository roleRepository; + + @Inject + private UserRepository userRepository; + + @POST + @Path("{groupIdentifier}/members") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateGroupMemberships( + @PathParam("groupIdentifier") final String groupIdentifierParam, + @FormParam("groupMembers") final String[] groupMembersParam + ) { + final Identifier groupIdentifier = identifierParser.parseIdentifier( + groupIdentifierParam + ); + final Optional result; + switch (groupIdentifier.getType()) { + case ID: + result = groupRepository.findById( + Long.parseLong(groupIdentifier.getIdentifier()) + ); + break; + case UUID: + result = groupRepository.findByUuid( + groupIdentifier.getIdentifier() + ); + break; + default: + result = groupRepository.findByName( + groupIdentifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + final Group group = result.get(); + final List memberNames = Arrays.asList(groupMembersParam); + + // Check for new members + final List newMemberNames = memberNames + .stream() + .filter(memberName -> !hasMember(group, memberName)) + .collect(Collectors.toList()); + + // Check for removed members + final List removedMemberNames = group + .getMemberships() + .stream() + .map(membership -> membership.getMember().getName()) + .filter(memberName -> !memberNames.contains(memberName)) + .collect(Collectors.toList()); + + for (final String newMemberName : newMemberNames) { + addNewMember(group, newMemberName); + } + + for (final String removedMemberName : removedMemberNames) { + removeMember(group, removedMemberName); + } + + return String.format( + "redirect:/users-groups-roles/groups/%s/details", + groupIdentifierParam + ); + } else { + models.put( + "errors", Arrays.asList( + adminMessages.getMessage( + "usersgroupsroles.groups.not_found.message", + Arrays.asList(groupIdentifierParam) + ) + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml"; + } + } + + @POST + @Path("{groupIdentifier}/roles") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateRoleMemberships( + @PathParam("groupIdentifier") final String groupIdentifierParam, + @FormParam("groupRoles") final String[] groupRoles + ) { + final Identifier groupIdentifier = identifierParser.parseIdentifier( + groupIdentifierParam + ); + final Optional result; + switch (groupIdentifier.getType()) { + case ID: + result = groupRepository.findById( + Long.parseLong(groupIdentifier.getIdentifier()) + ); + break; + case UUID: + result = groupRepository.findByUuid( + groupIdentifier.getIdentifier() + ); + break; + default: + result = groupRepository.findByName( + groupIdentifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + final Group group = result.get(); + final List roleNames = Arrays.asList(groupRoles); + + // Check for new roles + final List newRoleNames = roleNames + .stream() + .filter(roleName -> !hasRole(group, roleName)) + .collect(Collectors.toList()); + + // Check for removed roles + final List removedRoleNames = group + .getRoleMemberships() + .stream() + .map(membership -> membership.getRole().getName()) + .filter(roleName -> !roleNames.contains(roleName)) + .collect(Collectors.toList()); + + for (final String newRoleName : newRoleNames) { + addNewRole(group, newRoleName); + } + + for (final String removedRoleName : removedRoleNames) { + removeRole(group, removedRoleName); + } + + return String.format( + "redirect:/users-groups-roles/groups/%s/details", + groupIdentifierParam + ); + } else { + models.put( + "errors", Arrays.asList( + adminMessages.getMessage( + "usersgroupsroles.groups.not_found.message", + Arrays.asList(groupIdentifierParam) + ) + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml"; + } + } + + private boolean hasMember(final Group group, final String memberName) { + return group + .getMemberships() + .stream() + .map(membership -> membership.getMember().getName()) + .anyMatch(name -> name.equals(memberName)); + } + + private void addNewMember(final Group group, final String newMemberName) { + final Optional result = userRepository.findByName(newMemberName); + if (result.isPresent()) { + final User user = result.get(); + groupManager.addMemberToGroup(user, group); + } + } + + private void removeMember( + final Group group, final String removedMemberName + ) { + final Optional result = userRepository.findByName( + removedMemberName + ); + if (result.isPresent()) { + final User user = result.get(); + groupManager.removeMemberFromGroup(user, group); + } + } + + private boolean hasRole(final Group group, final String roleName) { + return group + .getRoleMemberships() + .stream() + .map(membership -> membership.getMember().getName()) + .anyMatch(name -> name.equals(roleName)); + } + + private void addNewRole(final Group group, final String newRoleName) { + final Optional result = roleRepository.findByName(newRoleName); + if (result.isPresent()) { + final Role role = result.get(); + roleManager.assignRoleToParty(role, group); + } + } + + private void removeRole(final Group group, final String removedRoleName) { + final Optional result = roleRepository.findByName( + removedRoleName + ); + if (result.isPresent()) { + final Role role = result.get(); + roleManager.removeRoleFromParty(role, group); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupUserFormEntry.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupUserFormEntry.java new file mode 100644 index 000000000..24b051d9f --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupUserFormEntry.java @@ -0,0 +1,68 @@ +/* + * 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.usersgroupsroles; + +/** + * Model of an entry in the form for managing the members of group. + * + * @author Jens Pelzetter + */ +public class GroupUserFormEntry { + + private long userId; + + private String userUuid; + + private String userName; + + private boolean member; + + public long getUserId() { + return userId; + } + + public void setUserId(final long userId) { + this.userId = userId; + } + + public String getUserUuid() { + return userUuid; + } + + public void setUserUuid(final String userUuid) { + this.userUuid = userUuid; + } + + public String getUserName() { + return userName; + } + + public void setUserName(final String userName) { + this.userName = userName; + } + + public boolean isMember() { + return member; + } + + public void setMember(final boolean member) { + this.member = member; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupUserMembership.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupUserMembership.java new file mode 100644 index 000000000..3dcd0865a --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupUserMembership.java @@ -0,0 +1,117 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.security.User; + +import java.util.Objects; + +/** + * Model friendly representation of a member of a group. + * + * @author Jens Pelzetter + */ +public class GroupUserMembership implements Comparable { + + private long userId; + + private String userUuid; + + private String userName; + + private String primaryEmailAddress; + + private String givenName; + + private String familyName; + + public GroupUserMembership() { + // Nothing + } + + public GroupUserMembership(final User user) { + userId = user.getPartyId(); + userUuid = user.getUuid(); + userName = user.getName(); + primaryEmailAddress = user.getPrimaryEmailAddress().getAddress(); + givenName = user.getGivenName(); + familyName = user.getFamilyName(); + } + + public long getUserId() { + return userId; + } + + public void setUserId(final long userId) { + this.userId = userId; + } + + public String getUserUuid() { + return userUuid; + } + + public void setUserUuid(final String userUuid) { + this.userUuid = userUuid; + } + + public String getUserName() { + return userName; + } + + public void setUserName(final String userName) { + this.userName = userName; + } + + public String getPrimaryEmailAddress() { + return primaryEmailAddress; + } + + public void setPrimaryEmailAddress(final String primaryEmailAddress) { + this.primaryEmailAddress = primaryEmailAddress; + } + + public String getGivenName() { + return givenName; + } + + public void setGivenName(final String givenName) { + this.givenName = givenName; + } + + public String getFamilyName() { + return familyName; + } + + public void setFamilyName(final String familyName) { + this.familyName = familyName; + } + + @Override + public int compareTo(final GroupUserMembership other) { + int result = userName.compareTo( + Objects.requireNonNull(other.getUserName()) + ); + if (result == 0) { + return primaryEmailAddress.compareTo(other.getPrimaryEmailAddress()); + } else { + return result; + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupsController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupsController.java new file mode 100644 index 000000000..4f21c91d6 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/GroupsController.java @@ -0,0 +1,253 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.Group; +import org.libreccm.security.GroupManager; +import org.libreccm.security.GroupMembership; +import org.libreccm.security.GroupRepository; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.RoleManager; +import org.libreccm.security.RoleMembership; +import org.libreccm.ui.Message; +import org.libreccm.ui.MessageType; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.Arrays; +import java.util.List; +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; + +/** + * Primary controller for managing groups. Retrieves data for the views and + * shows them. Processing of POST requests from the forms is done in other + * controllers. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/users-groups-roles/groups") +public class GroupsController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private GroupDetailsModel groupDetailsModel; + + @Inject + private GroupManager groupManager; + + @Inject + private GroupRepository groupRepository; + + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private RoleManager roleManager; + + @GET + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String getGroups() { + final List groups = groupRepository.findAll(); + models.put("groups", groups); + + return "org/libreccm/ui/admin/users-groups-roles/groups.xhtml"; + } + + @GET + @Path("/{groupIdentifier}/details") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String getGroupDetails( + @PathParam("groupIdentifier") final String groupIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + groupIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = groupRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = groupRepository.findByUuid(identifier.getIdentifier()); + break; + default: + result = groupRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + groupDetailsModel.setGroup(result.get()); + return "org/libreccm/ui/admin/users-groups-roles/group-details.xhtml"; + } else { + groupDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.groups.not_found.message", + Arrays.asList(groupIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml"; + } + } + + @GET + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String newGroup() { + return "org/libreccm/ui/admin/users-groups-roles/group-form.xhtml"; + } + + @GET + @Path("/{groupIdentifier}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String editGroup( + @PathParam("groupIdentifier") final String groupIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + groupIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = groupRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = groupRepository.findByUuid(identifier.getIdentifier()); + break; + default: + result = groupRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + groupDetailsModel.setGroup(result.get()); + return "org/libreccm/ui/admin/users-groups-roles/group-form.xhtml"; + } else { + groupDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.groups.not_found.message", + Arrays.asList(groupIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml"; + } + } + + @POST + @Path("/{groupIdentifier}/delete") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String deleteGroup( + @PathParam("groupIdentifier") final String groupIdentifierParam, + @FormParam("confirmed") final String confirmed + ) { + if ("true".equals(confirmed)) { + final Identifier identifier = identifierParser.parseIdentifier( + groupIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = groupRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = groupRepository.findByUuid(identifier + .getIdentifier()); + break; + default: + result = groupRepository.findByName(identifier + .getIdentifier()); + break; + } + + if (result.isPresent()) { + final Group group = result.get(); + for (final RoleMembership roleMembership : group + .getRoleMemberships()) { + roleManager.removeRoleFromParty( + roleMembership.getRole(), group + ); + } + + for (final GroupMembership groupMembership : group + .getMemberships()) { + groupManager.removeMemberFromGroup( + groupMembership.getMember(), group + ); + } + + groupRepository.delete(result.get()); + } else { + groupDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.groups.not_found.message", + Arrays.asList(groupIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml"; + } + } + + return "redirect:users-groups-roles/groups"; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/OverviewModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/OverviewModel.java new file mode 100644 index 000000000..9ddf66253 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/OverviewModel.java @@ -0,0 +1,91 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.GroupRepository; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.RoleRepository; +import org.libreccm.security.UserRepository; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.transaction.Transactional; + +/** + * Model for the overview page of the users/groups/roles admin module. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("UsersGroupsRolesOverviewModel") +public class OverviewModel { + + @Inject + private GroupRepository groupRepository; + + @Inject + private RoleRepository roleRepository; + + @Inject + private UserRepository userRepository; + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional + public long getActiveUsersCount() { + return userRepository + .findAll() + .stream() + .filter(user -> !user.isBanned()) + .count(); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional + public long getDisabledUsersCount() { + return userRepository + .findAll() + .stream() + .filter(user -> user.isBanned()) + .count(); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional + public long getGroupsCount() { + return groupRepository + .findAll() + .size(); + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional + public long getRolesCount() { + return roleRepository + .findAll() + .size(); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/PartyRoleMembership.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/PartyRoleMembership.java new file mode 100644 index 000000000..5e8ec5058 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/PartyRoleMembership.java @@ -0,0 +1,75 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.security.Role; + +/** + * Model friendly representation of the role membership of a {@link Party}. + * + * @author Jens Pelzetter + */ +public class PartyRoleMembership implements Comparable{ + + private long roleId; + + private String roleUuid; + + private String roleName; + + public PartyRoleMembership() { + // Nothing + } + + public PartyRoleMembership(final Role role) { + roleId = role.getRoleId(); + roleUuid = role.getUuid(); + roleName = role.getName(); + } + + public long getRoleId() { + return roleId; + } + + public void setRoleId(long roleId) { + this.roleId = roleId; + } + + public String getRoleUuid() { + return roleUuid; + } + + public void setRoleUuid(String roleUuid) { + this.roleUuid = roleUuid; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + @Override + public int compareTo(final PartyRoleMembership other) { + return roleName.compareTo(other.getRoleName()); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/PartyRolesFormEntry.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/PartyRolesFormEntry.java new file mode 100644 index 000000000..82b658ef1 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/PartyRolesFormEntry.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.usersgroupsroles; + +/** + * Model for an entry of a selectable role in the user roles form. + * + * @author Jens Pelzetter + */ +public class PartyRolesFormEntry { + + private long roleId; + + private String roleUuid; + + private String roleName; + + private boolean member; + + public long getRoleId() { + return roleId; + } + + public void setRoleId(final long roleId) { + this.roleId = roleId; + } + + public String getRoleUuid() { + return roleUuid; + } + + public void setRoleUuid(final String roleUuid) { + this.roleUuid = roleUuid; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(final String roleName) { + this.roleName = roleName; + } + + public boolean isMember() { + return member; + } + + public void setMember(final boolean member) { + this.member = member; + } + + + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleDetailsModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleDetailsModel.java new file mode 100644 index 000000000..2b822ed1d --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleDetailsModel.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.usersgroupsroles; + +import org.libreccm.security.Party; +import org.libreccm.security.PartyRepository; +import org.libreccm.security.Role; +import org.libreccm.security.RoleMembership; +import org.libreccm.ui.Message; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.transaction.Transactional; + +/** + * Provides the data for the details view of a role and the role edit form. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("RoleDetailsModel") +public class RoleDetailsModel { + + @Inject + private PartyRepository partyRepository; + + private long roleId; + + private String uuid; + + private String roleName; + + private List members; + + private List permissions; + + private final List messages; + + public RoleDetailsModel() { + this.messages = new ArrayList<>(); + } + + public List getMessages() { + return Collections.unmodifiableList(messages); + } + + public void addMessage(final Message message) { + messages.add(message); + } + + public long getRoleId() { + return roleId; + } + + public String getUuid() { + return uuid; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(final String roleName) { + this.roleName = roleName; + } + + public List getMembers() { + return Collections.unmodifiableList(members); + } + + public List getPermissions() { + return Collections.unmodifiableList(permissions); + } + + public List getRolePartyFormEnties() { + return partyRepository + .findAll() + .stream() + .map(this::buildRolePartyFormEntry) + .collect(Collectors.toList()); + } + + @Transactional(Transactional.TxType.REQUIRED) + protected void setRole(final Role role) { + Objects.requireNonNull(role); + + roleId = role.getRoleId(); + uuid = role.getUuid(); + roleName = role.getName(); + members = role + .getMemberships() + .stream() + .map(RoleMembership::getMember) + .map(RolePartyMembership::new) + .sorted() + .collect(Collectors.toList()); + } + + public boolean isNewRole() { + return roleId == 0; + } + + private RolePartyFormEntry buildRolePartyFormEntry(final Party party) { + final RolePartyFormEntry entry = new RolePartyFormEntry(); + entry.setPartyId(party.getPartyId()); + entry.setPartyUuid(party.getUuid()); + entry.setPartyName(party.getName()); + entry.setMember( + members + .stream() + .anyMatch( + membership -> membership.getPartyUuid().equals(party + .getUuid()) + ) + ); + return entry; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleFormController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleFormController.java new file mode 100644 index 000000000..6f9abd5fe --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleFormController.java @@ -0,0 +1,190 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.Role; +import org.libreccm.security.RoleRepository; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +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.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +/** + * Processes the POST requests from the role edit form. Depending on the value + * returned by {@link RoleDetailsModel#isNewRole()} a new role is created or + * an existing role updated. + * + * @author Jens Pelzetter + */ +@Controller +@Path("/users-groups-roles/roles/") +@RequestScoped +public class RoleFormController { + + @Inject + private AdminMessages adminMessages; + + // MvcBinding does not work with Krazo 1.1.0-M1 +// @Inject +// private BindingResult bindingResult; + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private RoleDetailsModel roleDetailsModel; + + @Inject + private RoleRepository roleRepository; + + // MvcBinding does not work with Krazo 1.1.0-M1 +// @MvcBinding + @FormParam("roleName") +// @NotBlank + private String roleName; + + @POST + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String createRole() { + // MvcBinding does not work with Krazo 1.1.0-M1 +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// return "org/libreccm/ui/admin/users-groups-roles/role-form.xhtml"; +// } + final List errors = new ArrayList<>(); + if (roleName == null || roleName.matches("\\s*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.roles.form.errors.name_not_empty" + ) + ); + } + if (!roleName.matches("[a-zA-Z0-9_-]")) { + errors.add( + adminMessages.get( + "usersgroupsroles.roles.form.errors.name_invalid" + ) + ); + } + if (!errors.isEmpty()) { + models.put("errors", errors); + roleDetailsModel.setRoleName(roleName); + return "org/libreccm/ui/admin/users-groups-roles/role-form.xhtml"; + } + + final Role role = new Role(); + role.setName(roleName); + roleRepository.save(role); + + return "redirect:users-groups-roles/roles"; + } + + @POST + @Path("{roleIdentifier}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateRole( + @PathParam("roleIdentifier") final String roleIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + roleIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = roleRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = roleRepository.findByUuid(identifier.getIdentifier()); + break; + default: + result = roleRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + final Role role = result.get(); + + // MvcBinding does not work with Krazo 1.1.0-M1 +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// return "org/libreccm/ui/admin/users-groups-roles/role-form.xhtml"; +// } + final List errors = new ArrayList<>(); + if (roleName == null || roleName.matches("\\s*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.roles.form.errors.name_not_empty" + ) + ); + } + if (!roleName.matches("[a-zA-Z0-9_-]")) { + errors.add( + adminMessages.get( + "usersgroupsroles.roles.form.errors.name_invalid" + ) + ); + } + if (!errors.isEmpty()) { + models.put("errors", errors); + roleDetailsModel.setRole(role); + return "org/libreccm/ui/admin/users-groups-roles/role-form.xhtml"; + } + + role.setName(roleName); + return "redirect:users-groups-roles/roles"; + } else { + models.put( + "errors", Arrays.asList( + adminMessages.getMessage( + "usersgroupsroles.roles.not_found.message", + Arrays.asList(roleIdentifierParam) + ) + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/role-not-found.xhtml"; + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleMembersController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleMembersController.java new file mode 100644 index 000000000..340caa9b9 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RoleMembersController.java @@ -0,0 +1,180 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.Party; +import org.libreccm.security.PartyRepository; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.Role; +import org.libreccm.security.RoleManager; +import org.libreccm.security.RoleRepository; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +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.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +/** + * Controller for adding members to a role and removing members from a role + * depending on the selections in the role members management form (dialog in + * role details). + * + * @author Jens Pelzetter + */ +@Controller +@Path("/users-groups-roles/roles/") +@RequestScoped + +public class RoleMembersController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private PartyRepository partyRepository; + + @Inject + private RoleManager roleManager; + + @Inject + private RoleRepository roleRepository; + + @POST + @Path("{roleIdentifier}/members") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateRoleMemberships( + @PathParam("roleIdentifier") final String roleIdentifierParam, + @FormParam("roleMembers") final String[] roleMembersParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + roleIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = roleRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = roleRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = roleRepository.findByName( + identifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + final Role role = result.get(); + final List memberNames = Arrays.asList(roleMembersParam); + + // Check for new members + final List newMemberNames = memberNames + .stream() + .filter(memberName -> !hasMember(role, memberName)) + .collect(Collectors.toList()); + + // Check for removed members + final List removedMemberNames = role + .getMemberships() + .stream() + .map(membership -> membership.getMember().getName()) + .filter(memberName -> !memberNames.contains(memberName)) + .collect(Collectors.toList()); + + for (final String newMemberName : newMemberNames) { + addNewMember(role, newMemberName); + } + + for (final String removedMemberName : removedMemberNames) { + removeMember(role, removedMemberName); + } + + return String.format( + "redirect:/users-groups-roles/roles/%s/details", + roleIdentifierParam + ); + } else { + models.put( + "errors", Arrays.asList( + adminMessages.getMessage( + "usersgroupsroles.roles.not_found.message", + Arrays.asList(roleIdentifierParam) + ) + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/role-not-found.xhtml"; + } + } + + private boolean hasMember(final Role role, final String memberName) { + return role + .getMemberships() + .stream() + .map(membership -> membership.getMember().getName()) + .anyMatch(name -> name.equals(memberName)); + } + + private void addNewMember(final Role role, final String newMemberName) { + final Optional result = partyRepository.findByName( + newMemberName + ); + if (result.isPresent()) { + final Party party = result.get(); + roleManager.assignRoleToParty(role, party); + } + } + + private void removeMember(final Role role, final String removedMemberName) { + final Optional result = partyRepository.findByName( + removedMemberName + ); + if (result.isPresent()) { + final Party party = result.get(); + roleManager.removeRoleFromParty(role, party); + } + } +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePartyFormEntry.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePartyFormEntry.java new file mode 100644 index 000000000..0b1c2acf2 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePartyFormEntry.java @@ -0,0 +1,68 @@ +/* + * 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.usersgroupsroles; + +/** + * Model for an entry in the role members form. + * + * @author Jens Pelzetter + */ +public class RolePartyFormEntry { + + private long partyId; + + private String partyUuid; + + private String partyName; + + private boolean member; + + public long getPartyId() { + return partyId; + } + + public void setPartyId(final long partyId) { + this.partyId = partyId; + } + + public String getPartyUuid() { + return partyUuid; + } + + public void setPartyUuid(final String partyUuid) { + this.partyUuid = partyUuid; + } + + public String getPartyName() { + return partyName; + } + + public void setPartyName(final String partyName) { + this.partyName = partyName; + } + + public boolean isMember() { + return member; + } + + public void setMember(final boolean member) { + this.member = member; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePartyMembership.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePartyMembership.java new file mode 100644 index 000000000..e82ab66fc --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePartyMembership.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.usersgroupsroles; + +import org.libreccm.security.Party; + +import java.util.Objects; + +/** + * Model friendly representation of a member of a role. + * + * @author Jens Pelzetter + */ +public class RolePartyMembership implements Comparable{ + + private long partyId; + + private String partyUuid; + + private String partyName; + + public RolePartyMembership() { + // Nothing + } + + public RolePartyMembership(final Party party) { + partyId = party.getPartyId(); + partyUuid = party.getUuid(); + partyName = party.getName(); + } + + public long getPartyId() { + return partyId; + } + + public void setPartyId(final long partyId) { + this.partyId = partyId; + } + + public String getPartyUuid() { + return partyUuid; + } + + public void setPartyUuid(final String partyUuid) { + this.partyUuid = partyUuid; + } + + public String getPartyName() { + return partyName; + } + + public void setPartyName(final String partyName) { + this.partyName = partyName; + } + + @Override + public int compareTo(final RolePartyMembership other) { + return partyName.compareTo( + Objects.requireNonNull(other).getPartyName() + ); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePermission.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePermission.java new file mode 100644 index 000000000..0c34c5a0e --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolePermission.java @@ -0,0 +1,114 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.security.Permission; + +import java.util.Objects; + +/** + * Model friendly representation of a permission granted to a role. + * + * @author Jens Pelzetter + */ +public class RolePermission implements Comparable { + + private long permissionId; + + private String permissionUuid; + + private String grantedPrivilege; + + private String objectName; + + private boolean objectPermission; + + public RolePermission() { + // Nothing + } + + public RolePermission(final Permission permission) { + permissionId = permission.getPermissionId(); + permissionUuid = permission.getUuid(); + grantedPrivilege = permission.getGrantedPrivilege(); + objectPermission = permission.getObject() != null; + if (objectPermission) { + objectName = permission.getObject().getDisplayName(); + } + } + + public long getPermissionId() { + return permissionId; + } + + public void setPermissionId(final long permissionId) { + this.permissionId = permissionId; + } + + public String getPermissionUuid() { + return permissionUuid; + } + + public void setPermissionUuid(final String permissionUuid) { + this.permissionUuid = permissionUuid; + } + + public String getGrantedPrivilege() { + return grantedPrivilege; + } + + public void setGrantedPrivilege(final String grantedPrivilege) { + this.grantedPrivilege = grantedPrivilege; + } + + public String getObjectName() { + return objectName; + } + + public void setObjectName(final String objectName) { + this.objectName = objectName; + } + + public boolean isObjectPermission() { + return objectPermission; + } + + public void setObjectPermission(final boolean objectPermission) { + this.objectPermission = objectPermission; + } + + @Override + public int compareTo(final RolePermission other) { + int result = Objects.compare( + grantedPrivilege, + Objects.requireNonNull(other).getGrantedPrivilege(), + (privilege1, privilege2) -> privilege1.compareTo(privilege2) + ); + if (result == 0 && isObjectPermission()) { + return Objects.compare( + objectName, + Objects.requireNonNull(other).getObjectName(), + (name1, name2) -> name1.compareTo(name2) + ); + } else { + return result; + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolesController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolesController.java new file mode 100644 index 000000000..b03aac08c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/RolesController.java @@ -0,0 +1,238 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.Role; +import org.libreccm.security.RoleRepository; +import org.libreccm.ui.Message; +import org.libreccm.ui.MessageType; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.Arrays; +import java.util.List; +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; + +/** + * Primary controller for managing roles. Retrieves the data for the views for + * manageing roles. POST requests from the forms a processed by other + * controllers. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/users-groups-roles/roles") +public class RolesController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private RoleDetailsModel rolesDetailsModel; + + @Inject + private RoleRepository roleRepository; + + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @GET + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String getRoles() { + final List roles = roleRepository.findAll(); + models.put("roles", roles); + + return "org/libreccm/ui/admin/users-groups-roles/roles.xhtml"; + } + + @GET + @Path("/{roleIdentifier}/details") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String getRoleDetails( + @PathParam("roleIdentifier") final String roleIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + roleIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = roleRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = roleRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = roleRepository.findByName( + identifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + rolesDetailsModel.setRole(result.get()); + return "org/libreccm/ui/admin/users-groups-roles/role-details.xhtml"; + } else { + rolesDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.roles.not_found_message", + Arrays.asList(roleIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/role-not-found.xhtml"; + } + } + + @GET + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String newRole() { + return "org/libreccm/ui/admin/users-groups-roles/role-form.xhtml"; + } + + @GET + @Path("/{roleIdentifier}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String editRole( + @PathParam("roleIdentifier") final String roleIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + roleIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = roleRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = roleRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = roleRepository.findByName( + identifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + rolesDetailsModel.setRole(result.get()); + return "org/libreccm/ui/admin/users-groups-roles/role-form.xhtml"; + } else { + rolesDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.roles.not_found_message", + Arrays.asList(roleIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/role-not-found.xhtml"; + } + } + + @POST + @Path("/{roleIdentifier}/delete") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String deleteRole( + @PathParam("roleIdentifier") final String roleIdentifierParam, + @FormParam("confirmed") final String confirmed + ) { + if ("true".equals(confirmed)) { + final Identifier identifier = identifierParser.parseIdentifier( + roleIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = roleRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = roleRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = roleRepository.findByName( + identifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + roleRepository.delete(result.get()); + } else { + rolesDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.roles.not_found_message", + Arrays.asList(roleIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/role-not-found.xhtml"; + } + } + + return "redirect:users-groups-roles/roles"; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserDetailsModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserDetailsModel.java new file mode 100644 index 000000000..5a9b208d3 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserDetailsModel.java @@ -0,0 +1,241 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.core.EmailAddress; +import org.libreccm.security.Group; +import org.libreccm.security.GroupMembership; +import org.libreccm.security.GroupRepository; +import org.libreccm.security.Role; +import org.libreccm.security.RoleMembership; +import org.libreccm.security.RoleRepository; +import org.libreccm.security.User; +import org.libreccm.ui.Message; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.transaction.Transactional; + +/** + * Model used by the user details view and the user edit form. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("UserDetailsModel") +public class UserDetailsModel { + + @Inject + private GroupRepository groupRepository; + + @Inject + private RoleRepository roleRepository; + + private long userId; + + private String uuid; + + private String name; + + private String givenName; + + private String familyName; + + private EmailAddress primaryEmailAddress; + + private List emailAddresses; + + private boolean banned; + + private boolean passwordResetRequired = true; + + private List groupMemberships; + + private List roles; + + private final List messages; + + public UserDetailsModel() { + messages = new ArrayList<>(); + } + + @Transactional(Transactional.TxType.REQUIRED) + protected void setUser(final User user) { + Objects.requireNonNull(user); + + userId = user.getPartyId(); + uuid = user.getUuid(); + name = user.getName(); + givenName = user.getGivenName(); + familyName = user.getFamilyName(); + primaryEmailAddress = user.getPrimaryEmailAddress(); + // Ensure that we don't get a lazyily initalized list. + emailAddresses = user + .getEmailAddresses() + .stream() + .collect(Collectors.toList()); + banned = user.isBanned(); + passwordResetRequired = user.isPasswordResetRequired(); + groupMemberships = user + .getGroupMemberships() + .stream() + .sorted() + .map(GroupMembership::getGroup) + .map(UserGroupMembership::new) + .collect(Collectors.toList()); + roles = user + .getRoleMemberships() + .stream() + .map(RoleMembership::getRole) + .map(PartyRoleMembership::new) + .sorted() + .collect(Collectors.toList()); + } + + public List getMessages() { + return Collections.unmodifiableList(messages); + } + + public void addMessage(final Message message) { + messages.add(message); + } + + public long getUserId() { + return userId; + } + + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getGivenName() { + return givenName; + } + + public void setGivenName(final String givenName) { + this.givenName = givenName; + } + + public String getFamilyName() { + return familyName; + } + + public void setFamilyName(final String familyName) { + this.familyName = familyName; + } + + public EmailAddress getPrimaryEmailAddress() { + return primaryEmailAddress; + } + + public List getEmailAddresses() { + return Collections.unmodifiableList(emailAddresses); + } + + public boolean isBanned() { + return banned; + } + + public void setBanned(final boolean banned) { + this.banned = banned; + } + + public boolean isPasswordResetRequired() { + return passwordResetRequired; + } + + public void setPasswordResetRequired( + final boolean passwordResetRequired + ) { + this.passwordResetRequired = passwordResetRequired; + } + + public List getGroupMemberships() { + return Collections.unmodifiableList(groupMemberships); + } + + public List getUserGroupsFormEntries() { + return groupRepository + .findAll() + .stream() + .map(this::buildUserGroupsFormEntry) + .collect(Collectors.toList()); + } + + public List getRoles() { + return Collections.unmodifiableList(roles); + } + + public List getUserRolesFormEntries() { + return roleRepository + .findAll() + .stream() + .map(this::buildUserRolesFormEntry) + .collect(Collectors.toList()); + } + + public boolean isNewUser() { + return userId == 0; + } + + private UserGroupsFormEntry buildUserGroupsFormEntry(final Group group) { + final UserGroupsFormEntry entry = new UserGroupsFormEntry(); + entry.setGroupId(group.getPartyId()); + entry.setGroupName(group.getName()); + entry.setGroupUuid(group.getUuid()); + entry.setMember( + groupMemberships + .stream() + .anyMatch( + membership -> membership.getGroupUuid().equals(group.getUuid()) + ) + ); + return entry; + } + + private PartyRolesFormEntry buildUserRolesFormEntry(final Role role) { + final PartyRolesFormEntry entry = new PartyRolesFormEntry(); + entry.setRoleId(role.getRoleId()); + entry.setRoleName(role.getName()); + entry.setRoleUuid(role.getUuid()); + entry.setMember( + roles + .stream() + .anyMatch( + membership -> membership.getRoleUuid().equals(role.getUuid()) + ) + ); + return entry; + } +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserFormController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserFormController.java new file mode 100644 index 000000000..d7ffba95b --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserFormController.java @@ -0,0 +1,310 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.User; +import org.libreccm.security.UserManager; +import org.libreccm.security.UserRepository; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +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.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +/** + * Controller managing the user post requests from the user edit form. + * + * @author Jens Pelzetter + */ +@Controller +@Path("/users-groups-roles/users/") +@RequestScoped +public class UserFormController { + + @Inject + private AdminMessages adminMessages; + + // MvcBinding does not work with Krazo 1.1.0-M1 +// @Inject +// private BindingResult bindingResult; + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private UserDetailsModel userDetailsModel; + + @Inject + private UserManager userManager; + + @Inject + private UserRepository userRepository; + + // MvcBinding does not work with Krazo 1.1.0-M1 +// @MvcBinding + @FormParam("userName") +// @NotBlank + private String userName; + + @FormParam("givenName") + private String givenName; + + @FormParam("familyName") + private String familyName; + + // MvcBinding does not work with Krazo 1.1.0-M1 +// @MvcBinding + @FormParam("primaryEmailAddress") +// @NotBlank +// @Email + private String primaryEmailAddress; + + @FormParam("primaryEmailAddressBouncing") + private boolean primaryEmailAddressBouncing; + + @FormParam("primaryEmailAddressVerified") + private boolean primaryEmailAddressVerified; + + @FormParam("banned") + private boolean banned; + + @FormParam("passwordResetRequired") + private boolean passwordResetRequired; + + @FormParam("password") + private String password; + + @FormParam("passwordConfirmation") + private String passwordConfirmation; + + @POST + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String createUser() { + // MvcBinding does not work with Krazo 1.1.0-M1 +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// return "org/libreccm/ui/admin/users-groups-roles/user-form.xhtml"; +// } + final List errors = new ArrayList<>(); + if (userName == null || userName.matches("\\s*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.form.errors.username.empty" + ) + ); + } + if (!userName.matches("[a-zA-Z0-9_-]*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.form.errors.username.invalid" + ) + ); + } + + if (primaryEmailAddress == null || primaryEmailAddress.matches("\\s*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.form.errors.primary_email.empty" + ) + ); + } + if (!primaryEmailAddress.matches( + "^[a-zA-Z0-9\\._-]*@[a-zA-Z0-9\\._-]*$" + )) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.form.errors.primary_email.invalid" + ) + ); + } + + if (password == null || password.isEmpty()) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.new.errors.password.empty" + ) + ); + } + + if (!Objects.equals(password, passwordConfirmation)) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.new.errors.password.no_match" + ) + ); + } + if (!errors.isEmpty()) { + models.put("errors", errors); + userDetailsModel.setName(userName); + userDetailsModel.setGivenName(givenName); + userDetailsModel.setFamilyName(familyName); + userDetailsModel + .getPrimaryEmailAddress() + .setAddress(primaryEmailAddress); + userDetailsModel + .getPrimaryEmailAddress() + .setBouncing(primaryEmailAddressBouncing); + userDetailsModel + .getPrimaryEmailAddress() + .setVerified(primaryEmailAddressVerified); + userDetailsModel.setBanned(banned); + userDetailsModel.setPasswordResetRequired(passwordResetRequired); + return "org/libreccm/ui/admin/users-groups-roles/user-form.xhtml"; + } + + userManager.createUser( + givenName, familyName, userName, primaryEmailAddress, password + ); + + return "redirect:users-groups-roles/users"; + } + + @POST + @Path("{userIdentifier}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateUser( + @PathParam("userIdentifier") final String userIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + final User user = result.get(); + + // MvcBinding does not work with Krazo 1.1.0-M1 +// if (bindingResult.isFailed()) { +// models.put("errors", bindingResult.getAllMessages()); +// return "org/libreccm/ui/admin/users-groups-roles/user-form.xhtml"; +// } + final List errors = new ArrayList<>(); + if (userName == null || userName.matches("\\s*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.form.errors.username.empty" + ) + ); + } + if (!userName.matches("[a-zA-Z0-9_-]*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.form.errors.username.invalid" + ) + ); + } + + if (primaryEmailAddress == null || primaryEmailAddress.matches( + "\\s*")) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.form.errors.primary_email.empty" + ) + ); + } + if (!primaryEmailAddress.matches( + "^[a-zA-Z0-9\\._-]*@[a-zA-Z0-9\\._-]*$" + )) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.form.errors.primary_email.invalid" + ) + ); + } + + if (password != null) { + if (!Objects.equals(password, passwordConfirmation)) { + errors.add( + adminMessages.get( + "usersgroupsroles.users.new.errors.password.no_match" + ) + ); + } + } + if (!errors.isEmpty()) { + userDetailsModel.setUser(user); + return "org/libreccm/ui/admin/users-groups-roles/user-form.xhtml"; + } + + user.setUuid(userName); + user.setGivenName(givenName); + user.setFamilyName(familyName); + user.getPrimaryEmailAddress().setAddress(primaryEmailAddress); + user + .getPrimaryEmailAddress() + .setBouncing(primaryEmailAddressBouncing); + user + .getPrimaryEmailAddress() + .setBouncing(primaryEmailAddressVerified); + user.setBanned(banned); + user.setPasswordResetRequired(passwordResetRequired); + + userRepository.save(user); + return "redirect:users-groups-roles/users"; + } else { + models.put("errors", Arrays.asList( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ) + )); + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupMembership.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupMembership.java new file mode 100644 index 000000000..a96d41d36 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupMembership.java @@ -0,0 +1,75 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.security.Group; + +/** + * Model friendly representation of a group membership of the user. + * + * @author Jens Pelzetter + */ +public class UserGroupMembership implements Comparable { + + private long groupId; + + private String groupUuid; + + private String groupName; + + public UserGroupMembership() { + // Nothing + } + + public UserGroupMembership(final Group group) { + groupId = group.getPartyId(); + groupUuid = group.getUuid(); + groupName = group.getName(); + } + + public long getGroupId() { + return groupId; + } + + public void setGroupId(final long groupId) { + this.groupId = groupId; + } + + public String getGroupUuid() { + return groupUuid; + } + + public void setGroupUuid(final String groupUuid) { + this.groupUuid = groupUuid; + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(final String groupName) { + this.groupName = groupName; + } + + @Override + public int compareTo(final UserGroupMembership other) { + return groupName.compareTo(other.getGroupName()); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupsFormEntry.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupsFormEntry.java new file mode 100644 index 000000000..be0d545a5 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupsFormEntry.java @@ -0,0 +1,69 @@ +/* + * 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.usersgroupsroles; + +/** + * Model for an selectable group in the user groups form. + * + * @author Jens Pelzetter + */ +public class UserGroupsFormEntry { + + private long groupId; + + private String groupUuid; + + private String groupName; + + private boolean member; + + public long getGroupId() { + return groupId; + } + + public void setGroupId(final long groupId) { + this.groupId = groupId; + } + + public String getGroupUuid() { + return groupUuid; + } + + public void setGroupUuid(final String groupUuid) { + this.groupUuid = groupUuid; + } + + public String getGroupName() { + return groupName; + } + + public void setGroupName(final String groupName) { + this.groupName = groupName; + } + + public boolean isMember() { + return member; + } + + public void setMember(final boolean member) { + this.member = member; + } + + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupsRolesController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupsRolesController.java new file mode 100644 index 000000000..bf54ce05c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UserGroupsRolesController.java @@ -0,0 +1,286 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.Group; +import org.libreccm.security.GroupManager; +import org.libreccm.security.GroupRepository; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.Role; +import org.libreccm.security.RoleManager; +import org.libreccm.security.RoleRepository; +import org.libreccm.security.User; +import org.libreccm.security.UserRepository; +import org.libreccm.ui.admin.AdminMessages; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +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.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +/** + * Adds and removes a user from groups and roles depending on the selections + * in the corresponding dialogs in the user details view. + * + * @author Jens Pelzetter + */ +@Controller +@Path("/users-groups-roles/users/") +@RequestScoped +public class UserGroupsRolesController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private GroupManager groupManager; + + @Inject + private GroupRepository groupRepository; + + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private RoleManager roleManager; + + @Inject + private RoleRepository roleRepository; + + @Inject + private UserRepository userRepository; + + @POST + @Path("{userIdentifier}/groups") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateGroupMemberships( + @PathParam("userIdentifier") final String userIdentifierParam, + @FormParam("userGroups") final String[] userGroups + ) { + final Identifier userIdentifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (userIdentifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(userIdentifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + userIdentifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName( + userIdentifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + final User user = result.get(); + final List groupNames = Arrays.asList(userGroups); + + // Check for new groups + final List newGroupNames = groupNames + .stream() + .filter(groupName -> !isMember(user, groupName)) + .collect(Collectors.toList()); + + // Check for removed groups + final List removedGroupNames = user + .getGroupMemberships() + .stream() + .map(membership -> membership.getGroup().getName()) + .filter(groupName -> !groupNames.contains(groupName)) + .collect(Collectors.toList()); + + for (final String newGroupName : newGroupNames) { + addNewGroup(user, newGroupName); + } + + for (final String removedGroupName : removedGroupNames) { + removeGroup(user, removedGroupName); + } + + return String.format( + "redirect:/users-groups-roles/users/%s/details", + userIdentifierParam + ); + } else { + models.put( + "errors", Arrays.asList( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ) + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + + @POST + @Path("{userIdentifier}/roles") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateRoleMemberships( + @PathParam("userIdentifier") final String userIdentifierParam, + @FormParam("userRoles") final String[] userRoles + ) { + final Identifier userIdentifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (userIdentifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(userIdentifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + userIdentifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName( + userIdentifier.getIdentifier() + ); + break; + } + + if (result.isPresent()) { + final User user = result.get(); + final List roleNames = Arrays.asList(userRoles); + + // Check for new roles + final List newRoleNames = roleNames + .stream() + .filter(roleName -> !hasRole(user, roleName)) + .collect(Collectors.toList()); + + // Check for removed roles + final List removedRoleNames = user + .getRoleMemberships() + .stream() + .map(membership -> membership.getRole().getName()) + .filter(roleName -> !roleNames.contains(roleName)) + .collect(Collectors.toList()); + + for (final String newRoleName : newRoleNames) { + addNewRole(user, newRoleName); + } + + for (final String removedRoleName : removedRoleNames) { + removeRole(user, removedRoleName); + } + + return String.format( + "redirect:/users-groups-roles/users/%s/details", + userIdentifierParam + ); + } else { + models.put( + "errors", Arrays.asList( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ) + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + + private boolean isMember(final User user, final String groupName) { + return user + .getGroupMemberships() + .stream() + .map(membership -> membership.getGroup().getName()) + .anyMatch(name -> name.equals(groupName)); + } + + private void addNewGroup(final User user, final String newGroupName) { + final Optional result = groupRepository.findByName(newGroupName); + if (result.isPresent()) { + final Group group = result.get(); + groupManager.addMemberToGroup(user, group); + } + } + + private void removeGroup(final User user, final String removedGroupName) { + final Optional result = groupRepository.findByName( + removedGroupName + ); + if (result.isPresent()) { + final Group group = result.get(); + groupManager.removeMemberFromGroup(user, group); + } + } + + private boolean hasRole(final User user, final String roleName) { + return user + .getRoleMemberships() + .stream() + .map(membership -> membership.getMember().getName()) + .anyMatch(name -> name.equals(roleName)); + } + + private void addNewRole(final User user, final String newRoleName) { + final Optional result = roleRepository.findByName(newRoleName); + if (result.isPresent()) { + final Role role = result.get(); + roleManager.assignRoleToParty(role, user); + } + } + + private void removeRole(final User user, final String removedRoleName) { + final Optional result = roleRepository.findByName( + removedRoleName + ); + if (result.isPresent()) { + final Role role = result.get(); + roleManager.removeRoleFromParty(role, user); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersController.java new file mode 100644 index 000000000..923db04e4 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersController.java @@ -0,0 +1,400 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.api.Identifier; +import org.libreccm.api.IdentifierParser; +import org.libreccm.core.CoreConstants; +import org.libreccm.core.EmailAddress; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.User; +import org.libreccm.security.UserRepository; +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.mvc.Models; +import javax.mvc.MvcContext; +import javax.transaction.Transactional; +import javax.ws.rs.DefaultValue; +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.QueryParam; + +/** + * Controller for the user details view and the {@code GET} requests to the + * user edit form. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/users-groups-roles/users") +public class UsersController { + + @Inject + private AdminMessages adminMessages; + + @Inject + private EmailFormModel emailFormModel; + + @Inject + private IdentifierParser identifierParser; + + @Inject + private Models models; + + @Inject + private MvcContext mvc; + + @Inject + private UserDetailsModel userDetailsModel; + + @Inject + private UserRepository userRepository; + + @Inject + private UsersTableModel usersTableModel; + + @GET + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String getUsers( + @QueryParam("filterterm") @DefaultValue("") final String filterTerm + ) { + usersTableModel.setFilterTerm(filterTerm); + return "org/libreccm/ui/admin/users-groups-roles/users.xhtml"; + } + + @GET + @Path("/{userIdentifier}/details") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String getUserDetails( + @PathParam("userIdentifier") final String userIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + userDetailsModel.setUser(result.get()); + return "org/libreccm/ui/admin/users-groups-roles/user-details.xhtml"; + } else { + userDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + + @GET + @Path("/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String newUser() { + return "org/libreccm/ui/admin/users-groups-roles/user-form.xhtml"; + } + + @GET + @Path("/{userIdentifier}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String editUser( + @PathParam("userIdentifier") final String userIdentifierParam + ) { + final Identifier identifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + userDetailsModel.setUser(result.get()); + return "org/libreccm/ui/admin/users-groups-roles/user-form.xhtml"; + } else { + userDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + + @POST + @Path("/{userIdentifier}/disable") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String disableUser( + @PathParam("userIdentifier") final String userIdentifierParam, + @FormParam("confirmed") final boolean confirmed + ) { + final Identifier identifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + final User user = result.get(); + user.setBanned(true); + userRepository.save(user); + return String.format( + //"redirect:%s", mvc.uri("UsersController#getUsers") + "redirect:users-groups-roles/users" + ); + } else { + userDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + + @GET + @Path("/{userIdentifier}/email-addresses/new") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String getNewEmailAddressForm( + @PathParam("userIdentifier") final String userIdentifierParam + ) { + emailFormModel.setUserIdentifier(userIdentifierParam); + return "org/libreccm/ui/admin/users-groups-roles/email-form.xhtml"; + } + + @GET + @Path("/{userIdentifier}/email-addresses/{emailId}") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String getEditEmailAddressForm( + @PathParam("userIdentifier") final String userIdentifierParam, + @PathParam("emailId") final int emailId + ) { + final Identifier identifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + final User user = result.get(); + + if (user.getEmailAddresses().size() <= emailId) { + models.put("errorUserIdentifier", userIdentifierParam); + models.put("errorEmailId", emailId); + return "org/libreccm/ui/admin/users-groups-roles/email-not-found.xhtml"; + } else { + final EmailAddress emailAddress = user + .getEmailAddresses() + .get(emailId); + emailFormModel.setUserIdentifier(userIdentifierParam); + emailFormModel.setEmailId(emailId); + emailFormModel.setAddress(emailAddress.getAddress()); + emailFormModel.setBouncing(emailAddress.isBouncing()); + emailFormModel.setVerified(emailAddress.isVerified()); + return "org/libreccm/ui/admin/users-groups-roles/email-form.xhtml"; + } + } else { + userDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ), + MessageType.WARNING + ) + ); + + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + + @POST + @Path("/{userIdentifier}/email-addresses/{emailId}/remove") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String removeEmailAddress( + @PathParam("userIdentifier") final String userIdentifierParam, + @PathParam("emailId") final int emailId, + @FormParam("confirmed") final boolean confirmed + ) { + if (!confirmed) { + return String.format( + "redirect:%s", + mvc.uri( + String.format( + "UsersController#getUserDetails", + "{ userIdentifier: %s}", + userIdentifierParam + ) + ) + ); + } + + final Identifier identifier = identifierParser.parseIdentifier( + userIdentifierParam + ); + final Optional result; + switch (identifier.getType()) { + case ID: + result = userRepository.findById( + Long.parseLong(identifier.getIdentifier()) + ); + break; + case UUID: + result = userRepository.findByUuid( + identifier.getIdentifier() + ); + break; + default: + result = userRepository.findByName(identifier.getIdentifier()); + break; + } + + if (result.isPresent()) { + final User user = result.get(); + if (user.getEmailAddresses().size() <= emailId) { + return String.format( + "redirect:%s", + mvc.uri( + String.format( + "UsersController#getUserDetails", + "{ userIdentifier: %s}", + userIdentifierParam + ) + ) + ); + } + user.getEmailAddresses().remove(emailId); + userRepository.save(user); + return String.format( + "redirect:%s", + mvc.uri( + String.format( + "UsersController#getUserDetails", + "{ userIdentifier: %s}", + userIdentifierParam + ) + ) + ); + } else { + userDetailsModel.addMessage( + new Message( + adminMessages.getMessage( + "usersgroupsroles.users.not_found.message", + Arrays.asList(userIdentifierParam) + ), + MessageType.WARNING + ) + ); + return "org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml"; + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersGroupsRolesController.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersGroupsRolesController.java new file mode 100644 index 000000000..c724cb047 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersGroupsRolesController.java @@ -0,0 +1,48 @@ +/* + * 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.usersgroupsroles; + +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 overview page of the users/groups/roles admin section. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/users-groups-roles") +public class UsersGroupsRolesController { + + @GET + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + public String getOverview() { + return "org/libreccm/ui/admin/users-groups-roles/overview.xhtml"; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersGroupsRolesPage.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersGroupsRolesPage.java new file mode 100644 index 000000000..0b3e39d9c --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersGroupsRolesPage.java @@ -0,0 +1,93 @@ +/* + * 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.usersgroupsroles; + +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 users/groups/roles section of the + * AdminUI. + * + * @author Jens Pelzetter + */ +@ApplicationScoped +public class UsersGroupsRolesPage implements AdminPage { + + @Override + public Set> getControllerClasses() { + final Set> classes = new HashSet<>(); + classes.add(UsersGroupsRolesController.class); + classes.add(GroupsController.class); + classes.add(GroupFormController.class); + classes.add(GroupMembersRolesController.class); + classes.add(RolesController.class); + classes.add(RoleFormController.class); + classes.add(RoleMembersController.class); + classes.add(UsersController.class); + classes.add(UserFormController.class); + classes.add(UserGroupsRolesController.class); + classes.add(EmailFormController.class); + return classes; + } + + @Override + public String getUriIdentifier() { + return String.format( + "%s#getOverview", + UsersGroupsRolesController.class.getSimpleName() + ); + } + + @Override + public String getLabelBundle() { + return AdminConstants.ADMIN_BUNDLE; + } + + @Override + public String getLabelKey() { + return "usersgroupsroles.label"; + } + + @Override + public String getDescriptionBundle() { + return AdminConstants.ADMIN_BUNDLE; + } + + @Override + public String getDescriptionKey() { + return "usersgroupsroles.description"; + } + + @Override + public String getIcon() { + return "people-fill"; + } + + @Override + public int getPosition() { + return 10; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersTableModel.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersTableModel.java new file mode 100644 index 000000000..26a7250f6 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/UsersTableModel.java @@ -0,0 +1,67 @@ +/* + * 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.usersgroupsroles; + +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.security.User; +import org.libreccm.security.UserRepository; + +import java.util.List; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.transaction.Transactional; + +/** + * Model for the table/list of users. + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("UsersTableModel") +public class UsersTableModel { + + @Inject + private UserRepository userRepository; + + private String filterTerm; + + public String getFilterTerm() { + return filterTerm; + } + + protected void setFilterTerm(final String filterTerm) { + this.filterTerm = filterTerm; + } + + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional + public List getUsers() { + if (filterTerm == null || filterTerm.isEmpty()) { + return userRepository.findAllOrderdByUsername(); + } else { + return userRepository.filtered(filterTerm); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/package-info.java new file mode 100644 index 000000000..1866e2d00 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/admin/usersgroupsroles/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 + */ +/** + * Provides the backend for the user interface for managing users, groups and + * roles. + */ +package org.libreccm.ui.admin.usersgroupsroles; diff --git a/ccm-core/src/main/java/org/libreccm/ui/package-info.java b/ccm-core/src/main/java/org/libreccm/ui/package-info.java new file mode 100644 index 000000000..5c8f1eeb2 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/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 + */ +/** + * Utility classes for Jakarta EE MVC based user interfaces. + */ +package org.libreccm.ui; diff --git a/ccm-core/src/main/java/org/libreccm/web/ApplicationImExporter.java b/ccm-core/src/main/java/org/libreccm/web/ApplicationImExporter.java index a32fdf5b4..cb2a296cd 100644 --- a/ccm-core/src/main/java/org/libreccm/web/ApplicationImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/web/ApplicationImExporter.java @@ -23,6 +23,7 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; @@ -43,22 +44,33 @@ public class ApplicationImExporter private ApplicationRepository applicationRepository; @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return CcmApplication.class; } @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final CcmApplication entity) { - applicationRepository.save(entity); } @Override protected Set> getRequiredEntities() { - return Collections.emptySet(); } + @Override + protected CcmApplication reloadEntity(final CcmApplication entity) { + return applicationRepository + .findById(Objects.requireNonNull(entity).getObjectId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "CcmApplication entity %s not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/web/ApplicationType.java b/ccm-core/src/main/java/org/libreccm/web/ApplicationType.java index 5c5019960..a66cd3a61 100644 --- a/ccm-core/src/main/java/org/libreccm/web/ApplicationType.java +++ b/ccm-core/src/main/java/org/libreccm/web/ApplicationType.java @@ -23,8 +23,12 @@ import com.arsdigita.ui.admin.applications.AbstractAppSettingsPane; import com.arsdigita.ui.admin.applications.DefaultApplicationInstanceForm; import com.arsdigita.ui.admin.applications.DefaultApplicationSettingsPane; +import org.libreccm.ui.admin.applications.ApplicationController; +import org.libreccm.ui.admin.applications.DefaultApplicationController; + import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -111,10 +115,13 @@ public @interface ApplicationType { * * @return */ + @SuppressWarnings("rawtypes") // Can't specify type here, otherwise problems in using classes. Class creator(); Class instanceForm() default DefaultApplicationInstanceForm.class; - + Class settingsPane() default DefaultApplicationSettingsPane.class; + + Class applicationController() default DefaultApplicationController.class; } diff --git a/ccm-core/src/main/java/org/libreccm/workflow/AssignableTaskImExporter.java b/ccm-core/src/main/java/org/libreccm/workflow/AssignableTaskImExporter.java index 287b63237..a75311637 100644 --- a/ccm-core/src/main/java/org/libreccm/workflow/AssignableTaskImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/workflow/AssignableTaskImExporter.java @@ -23,6 +23,7 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; @@ -31,7 +32,7 @@ import javax.transaction.Transactional; /** * Exporter/Importer for {@link AssignableTask}s. - * + * * @author Tobias Osmers * @author Jens Pelzetter * @@ -45,13 +46,12 @@ public class AssignableTaskImExporter private AssignableTaskRepository assignableTaskRepository; @Override - protected Class getEntityClass() { + public Class getEntityClass() { return AssignableTask.class; } @Override protected Set> getRequiredEntities() { - final Set> entities = new HashSet<>(); entities.add(Workflow.class); @@ -61,8 +61,21 @@ public class AssignableTaskImExporter @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final AssignableTask entity) { - assignableTaskRepository.save(entity); } + @Override + protected AssignableTask reloadEntity(final AssignableTask entity) { + return assignableTaskRepository + .findById(Objects.requireNonNull(entity).getTaskId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "AssignableTask entity %s not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/java/org/libreccm/workflow/TaskAssignment.java b/ccm-core/src/main/java/org/libreccm/workflow/TaskAssignment.java index 2e4092f45..16cb3f651 100644 --- a/ccm-core/src/main/java/org/libreccm/workflow/TaskAssignment.java +++ b/ccm-core/src/main/java/org/libreccm/workflow/TaskAssignment.java @@ -37,17 +37,39 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; import javax.persistence.Table; /** * Represents the assignment of a {@link AssignableTask} to a {@link Role}. - * + * * @author Jens Pelzetter */ @Entity @Table(name = "WORKFLOW_TASK_ASSIGNMENTS", schema = DB_SCHEMA) -@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, - property = "customAssignId") +@NamedQueries({ + @NamedQuery( + name = "TaskAssignment.findById", + query = "SELECT t FROM TaskAssignment t WHERE t.taskAssignmentId = :assignmentId" + ), + @NamedQuery( + name = "TaskAssignment.findByUuid", + query = "SELECT t FROM TaskAssignment t WHERE t.uuid = :uuid" + ), + @NamedQuery( + name = "TaskAssignment.findByTask", + query = "SELECT t FROM TaskAssignment t WHERE t.task = :task" + ), + @NamedQuery( + name = "TaskAssignment.findByRole", + query = "SELECT t FROM TaskAssignment t WHERE t.role = :role" + ) +}) +@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "customAssignId" +) public class TaskAssignment implements Serializable, Exportable { private static final long serialVersionUID = -4427537363301565707L; @@ -62,7 +84,7 @@ public class TaskAssignment implements Serializable, Exportable { @Column(name = "UUID", unique = true, nullable = false) private String uuid; - + /** * The task. */ @@ -86,7 +108,7 @@ public class TaskAssignment implements Serializable, Exportable { protected void setTaskAssignmentId(final long taskAssignmentId) { this.taskAssignmentId = taskAssignmentId; } - + @Override public String getUuid() { return uuid; @@ -95,7 +117,6 @@ public class TaskAssignment implements Serializable, Exportable { public void setUuid(final String uuid) { this.uuid = uuid; } - public AssignableTask getTask() { return task; diff --git a/ccm-core/src/main/java/org/libreccm/workflow/TaskAssignmentImExporter.java b/ccm-core/src/main/java/org/libreccm/workflow/TaskAssignmentImExporter.java index bb8fb2933..7b685ed79 100644 --- a/ccm-core/src/main/java/org/libreccm/workflow/TaskAssignmentImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/workflow/TaskAssignmentImExporter.java @@ -23,47 +23,69 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.NoResultException; import javax.transaction.Transactional; /** * Exporter/Importer for {@link TaskAssignment}s. - * + * * @author Tobias Osmers * @author Jens Pelzetter */ @RequestScoped @Processes(TaskAssignment.class) -public class TaskAssignmentImExporter +public class TaskAssignmentImExporter extends AbstractEntityImExporter { @Inject private EntityManager entityManager; @Override - protected Class getEntityClass() { - + public Class getEntityClass() { return TaskAssignment.class; } @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final TaskAssignment entity) { - entityManager.persist(entity); - + } @Override protected Set> getRequiredEntities() { - + final Set> classes = new HashSet<>(); classes.add(AssignableTask.class); - + return classes; } + + @Override + protected TaskAssignment reloadEntity(final TaskAssignment entity) { + try { + return entityManager + .createNamedQuery( + "TaskAssignment.findById", TaskAssignment.class + ) + .setParameter( + "assignmentId", + Objects.requireNonNull(entity).getTaskAssignmentId() + ).getSingleResult(); + } catch (NoResultException ex) { + throw new IllegalArgumentException( + String.format( + "TaskAssignment entity %s not found in database.", + Objects.toString(entity) + ) + ); + } + } + } diff --git a/ccm-core/src/main/java/org/libreccm/workflow/TaskCommentImExporter.java b/ccm-core/src/main/java/org/libreccm/workflow/TaskCommentImExporter.java index adec8de19..1a7d6d1d2 100644 --- a/ccm-core/src/main/java/org/libreccm/workflow/TaskCommentImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/workflow/TaskCommentImExporter.java @@ -23,6 +23,7 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; @@ -43,7 +44,7 @@ public class TaskCommentImExporter extends AbstractEntityImExporter private TaskCommentRepository taskCommentRepository; @Override - protected Class getEntityClass() { + public Class getEntityClass() { return TaskComment.class; } @@ -55,12 +56,25 @@ public class TaskCommentImExporter extends AbstractEntityImExporter @Override protected Set> getRequiredEntities() { - final Set> classes = new HashSet<>(); classes.add(AssignableTask.class); return classes; } + + @Override + protected TaskComment reloadEntity(final TaskComment entity) { + return taskCommentRepository + .findById(Objects.requireNonNull(entity).getCommentId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "TaskComment entity %s not found in database.", + Objects.toString(entity) + ) + ) + ); + } } diff --git a/ccm-core/src/main/java/org/libreccm/workflow/TaskDependency.java b/ccm-core/src/main/java/org/libreccm/workflow/TaskDependency.java index 2de740200..f5ac216b2 100644 --- a/ccm-core/src/main/java/org/libreccm/workflow/TaskDependency.java +++ b/ccm-core/src/main/java/org/libreccm/workflow/TaskDependency.java @@ -34,6 +34,8 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; import javax.persistence.Table; /** @@ -43,8 +45,28 @@ import javax.persistence.Table; */ @Entity @Table(name = "WORKFLOW_TASK_DEPENDENCIES", schema = CoreConstants.DB_SCHEMA) -@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, - property = "uuid") +@NamedQueries({ + @NamedQuery( + name = "TaskDependency.findById", + query = "SELECT d FROM TaskDependency d WHERE d.taskDependencyId = :dependencyId" + ), + @NamedQuery( + name = "TaskDependency.findByUuid", + query = "SELECT d FROM TaskDependency d WHERE d.uuid = :uuid" + ), + @NamedQuery( + name = "TaskDependency.findByBlockedTask", + query = "SELECT d FROM TaskDependency d WHERE d.blockedTask = :task" + ), + @NamedQuery( + name = "TaskDependency.findByBlockingTask", + query = "SELECT d FROM TaskDependency d WHERE d.blockingTask = :task" + ) +}) +@JsonIdentityInfo( + generator = ObjectIdGenerators.PropertyGenerator.class, + property = "uuid" +) public class TaskDependency implements Serializable, Exportable { private static final long serialVersionUID = -4383255770131633943L; diff --git a/ccm-core/src/main/java/org/libreccm/workflow/TaskDependencyImExporter.java b/ccm-core/src/main/java/org/libreccm/workflow/TaskDependencyImExporter.java index e66aac96b..128dce652 100644 --- a/ccm-core/src/main/java/org/libreccm/workflow/TaskDependencyImExporter.java +++ b/ccm-core/src/main/java/org/libreccm/workflow/TaskDependencyImExporter.java @@ -23,48 +23,67 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.NoResultException; import javax.transaction.Transactional; /** * Exporter/Importer for {@link TaskDependency} entities. - * + * * @author Jens Pelzetter */ @RequestScoped @@ -42,7 +43,7 @@ public class WorkflowImExporter extends AbstractEntityImExporter { private WorkflowRepository workflowRepository; @Override - protected Class getEntityClass() { + public Class getEntityClass() { return Workflow.class; } @@ -50,14 +51,26 @@ public class WorkflowImExporter extends AbstractEntityImExporter { @Override @Transactional(Transactional.TxType.REQUIRED) protected void saveImportedEntity(final Workflow entity) { - workflowRepository.save(entity); } @Override protected Set> getRequiredEntities() { - return Collections.emptySet(); } + @Override + protected Workflow reloadEntity(final Workflow entity) { + return workflowRepository + .findById(Objects.requireNonNull(entity).getWorkflowId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Workflow entity %s not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formCheck.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formCheck.xhtml new file mode 100644 index 000000000..5c1d349b3 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formCheck.xhtml @@ -0,0 +1,44 @@ + + + + + + + + + + + +
+ + +
+
+ \ No newline at end of file diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupChecks.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupChecks.xhtml new file mode 100644 index 000000000..6828dce8c --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupChecks.xhtml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + +
+
+ #{cc.attrs.label} +
+ +
+ + +
+
+
+
+ + #{cc.attrs.help} + +
+
+ diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupColor.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupColor.xhtml new file mode 100644 index 000000000..d03a66e42 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupColor.xhtml @@ -0,0 +1,48 @@ + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupDate.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupDate.xhtml new file mode 100644 index 000000000..d4c920c1d --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupDate.xhtml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupEmail.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupEmail.xhtml new file mode 100644 index 000000000..00b0d5b92 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupEmail.xhtml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + + + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupFile.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupFile.xhtml new file mode 100644 index 000000000..f0984a644 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupFile.xhtml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupNumber.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupNumber.xhtml new file mode 100644 index 000000000..693717010 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupNumber.xhtml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupPassword.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupPassword.xhtml new file mode 100644 index 000000000..ca9e387f8 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupPassword.xhtml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupRadio.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupRadio.xhtml new file mode 100644 index 000000000..0b3717a1b --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupRadio.xhtml @@ -0,0 +1,62 @@ + + + + + + + + + + + + +
+
+ #{cc.attrs.label} +
+ +
+ + +
+
+
+
+ + #{cc.attrs.help} + +
+
+ + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupRange.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupRange.xhtml new file mode 100644 index 000000000..aec116750 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupRange.xhtml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupSearch.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupSearch.xhtml new file mode 100644 index 000000000..447e953bd --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupSearch.xhtml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupSelect.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupSelect.xhtml new file mode 100644 index 000000000..92c5736b9 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupSelect.xhtml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTel.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTel.xhtml new file mode 100644 index 000000000..21c89fc3c --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTel.xhtml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + + + + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupText.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupText.xhtml new file mode 100644 index 000000000..1d9664069 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupText.xhtml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTextarea.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTextarea.xhtml new file mode 100644 index 000000000..5df333d2f --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTextarea.xhtml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTime.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTime.xhtml new file mode 100644 index 000000000..7c7907051 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupTime.xhtml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupUrl.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupUrl.xhtml new file mode 100644 index 000000000..8a5cbc55a --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/formGroupUrl.xhtml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + +
+ + + + #{cc.attrs.help} + +
+
+ + + + + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/modalForm.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/modalForm.xhtml new file mode 100644 index 000000000..e62917ab6 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/modalForm.xhtml @@ -0,0 +1,80 @@ +]> + + + + + + + + + + + + + + +
+ +
+ +
+ diff --git a/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/svgIcon.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/svgIcon.xhtml new file mode 100644 index 000000000..c8751f49e --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/bootstrap/svgIcon.xhtml @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/libreccm/deleteDialog.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/libreccm/deleteDialog.xhtml new file mode 100644 index 000000000..b70493b87 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/libreccm/deleteDialog.xhtml @@ -0,0 +1,124 @@ +]> + + + + + + + + + + + + + +
+ +
+ +
+ + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/libreccm/localizedStringEditor.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/libreccm/localizedStringEditor.xhtml new file mode 100644 index 000000000..f68a2c444 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/libreccm/localizedStringEditor.xhtml @@ -0,0 +1,510 @@ +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +

#{cc.attrs.title}

+
+ +

#{cc.attrs.title}

+
+ +

#{cc.attrs.title}

+
+ +

#{cc.attrs.title}

+
+ +
#{cc.attrs.title}
+
+ +
#{cc.attrs.title}
+
+ +
#{cc.attrs.title}
+
+
+ +
+
+ +
+ +
+
+ + +

+ #{cc.attrs.emptyText} +

+
+ + + + + + + + + + + + + + + + + + + +
#{cc.attrs.tableLocaleHeading}#{cc.attrs.tableValueHeading}#{cc.attrs.tableActionsHeading}
#{entry.key}#{entry.value} +
+ +
+ +
+
+ +
+ +
+
+
+
+
+ + diff --git a/ccm-core/src/main/resources/META-INF/resources/components/libreccm/messages.xhtml b/ccm-core/src/main/resources/META-INF/resources/components/libreccm/messages.xhtml new file mode 100644 index 000000000..fcf8899f0 --- /dev/null +++ b/ccm-core/src/main/resources/META-INF/resources/components/libreccm/messages.xhtml @@ -0,0 +1,21 @@ +]> + + + + + +
+ + + +
+
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/applications/applicationtypes.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/applications/applicationtypes.xhtml new file mode 100644 index 000000000..45721134f --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/applications/applicationtypes.xhtml @@ -0,0 +1,59 @@ + + + + + + + + + + + + +
+

#{AdminMessages['applications.label']}

+ +
    + +
  • +
    +

    + + + #{type.title} + + + #{type.title} + + +

    + + + + #{AdminMessages['applications.types.singleton']} + + + #{AdminMessages.getMessage('applications.number_of_instances_one', [type.numberOfInstances])} + + + #{AdminMessages.getMessage('applications.number_of_instances', [type.numberOfInstances])} + + + +
    +

    + ${type.description} +

    +
  • +
    +
+ +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/application-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/application-not-found.xhtml new file mode 100644 index 000000000..ce9fb3c87 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/application-not-found.xhtml @@ -0,0 +1,25 @@ + + + + + + + + + + + + +
+

#{AdminMessages['categorymanager.label']}

+

ToDo

+
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-details.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-details.xhtml new file mode 100644 index 000000000..aa3dafd87 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-details.xhtml @@ -0,0 +1,308 @@ + + + + + + + + + + + + + + +
+

#{AdminMessages.getMessage('categories.details.title', [CategoryDetailsModel.path])}

+ + + +
+
+
#{AdminMessages['categories.details.id']}
+
#{CategoryDetailsModel.categoryId}
+
+
+
#{AdminMessages['categories.details.uuid']}
+
#{CategoryDetailsModel.uuid}
+
+
+
#{AdminMessages['categories.details.uniqueId']}
+
#{CategoryDetailsModel.uniqueId}
+
+
+
#{AdminMessages['categories.details.name']}
+
#{CategoryDetailsModel.name}
+
+
+
#{AdminMessages['categories.details.path']}
+
+ + + #{CategoryDetailsModel.categoryPath.domain.domainKey} + + + + / + + #{category.name} + + +
+
+
+
#{AdminMessages['categories.details.enabled']}
+
+ + + #{AdminMessages['categories.details.enabled.yes']} + + + #{AdminMessages['categories.details.enabled.no']} + + +
+
+
+
#{AdminMessages['categories.details.visible']}
+
+ + + #{AdminMessages['categories.details.visible.yes']} + + + #{AdminMessages['categories.details.visible.no']} + + +
+
+
+
#{AdminMessages['categories.details.abstract_category']}
+
+ + + #{AdminMessages['categories.details.abstract_category.yes']} + + + #{AdminMessages['categories.details.abstract_category.no']} + + +
+
+
+ + + + + + + +

#{AdminMessages['categories.details.subcategories.heading']}

+ + + +

#{AdminMessages['categories.details.subcategories.none']}

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ #{AdminMessages['categories.details.subcategories.table.headings.name']} + + #{AdminMessages['categories.details.subcategories.table.headings.enabled']} + + #{AdminMessages['categories.details.subcategories.table.headings.visible']} + + #{AdminMessages['categories.details.subcategories.table.headings.abstract']} + + #{AdminMessages['categories.details.subcategories.table.headings.actions']} +
+ + #{category.name} + + + + + #{AdminMessages['categories.details.enabled.yes']} + + + #{AdminMessages['categories.details.enabled.no']} + + + + + + #{AdminMessages['categories.details.visible.yes']} + + + #{AdminMessages['categories.details.visible.no']} + + + + + + #{AdminMessages['categories.details.abstract_category.yes']} + + + #{AdminMessages['categories.details.abstract_category.no']} + + + + +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+
+ +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-form.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-form.xhtml new file mode 100644 index 000000000..66532865c --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-form.xhtml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + +
+

+ + + #{AdminMessage.getMessage('categories.new.label', [ CategoryDetailsModel.parent.path ])} + + + #{AdminMessage.getMessage('categories.edit.label', [ CategoryDetailsModel.path ])} + + + #{AdminMessages['categories.label']} +

+ +
+ + + + + + + #{AdminMessages['categories.form.buttons.cancel']} + + + +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-not-found.xhtml new file mode 100644 index 000000000..464da85f7 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/category-not-found.xhtml @@ -0,0 +1,29 @@ + + + + + + + + + + + + +
+

#{AdminMessages['categories.not_found.title']}

+ + + +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-details.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-details.xhtml new file mode 100644 index 000000000..afcdba4fb --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-details.xhtml @@ -0,0 +1,435 @@ +]> + + + + + + + + + + + + +
+

#{AdminMessages.getMessage('categorysystems.details.title', [CategorySystemDetailsModel.domainKey])}

+ + + +
+
+
+ #{AdminMessages['categorysystems.details.id']} +
+
+ #{CategorySystemDetailsModel.categorySystemId} +
+
+
+
+ #{AdminMessages['categorysystems.details.uuid']} +
+
+ #{CategorySystemDetailsModel.uuid} +
+
+
+
+ #{AdminMessages['categorysystems.details.domainKey']} +
+
+ #{CategorySystemDetailsModel.domainKey} +
+
+
+
+ #{AdminMessages['categorysystems.details.uri']} +
+
+ #{CategorySystemDetailsModel.uri} +
+
+
+
+ #{AdminMessages['categorysystems.details.version']} +
+
+ #{CategorySystemDetailsModel.version} +
+
+
+
+ #{AdminMessages['categorysystems.details.released']} +
+
+ #{CategorySystemDetailsModel.released} +
+
+
+ + + + + + +

+ #{AdminMessages['categorysystems.details.owners.heading']} +

+
+ + +

#{AdminMessages['categorysstems.details.owner.add.dialog.title']}

+
+ +
+ + + + #{AdminMessages['categorysystems.details.owner.add.dialog.application.help']} + +
+
+ + + + #{AdminMessages['categorysystems.details.owner.add.dialog.context.help']} + +
+
+ + + + +
+
+ + +

+ #{AdminMessages['categorysystems.details.owners.none']} +

+
+ + + + + + + + + + + + + + + + + + +
+ #{AdminMessages['categorysystems.details.owners.applicationname']} + + #{AdminMessages['categorysystems.details.owners.context']} + + #{AdminMessages['categorysystems.details.owners.actions']} +
+ #{owner.ownerAppName} + + #{owner.context} + +
+ +
+ +
+
+
+ +

#{AdminMessages['categorysystems.details.categories.heading']}

+ + + +

+ #{AdminMessages['categorysystems.details.categories.none']} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ #{AdminMessages['categorysystems.details.categories.table.headings.name']} + + #{AdminMessages['categorysystems.details.categories.table.headings.enabled']} + + #{AdminMessages['categorysystems.details.categories.table.headings.visible']} + + #{AdminMessages['categorysystems.details.categories.table.headings.abstract']} + + #{AdminMessages['categorysystems.details.categories.table.headings.actions']} +
+ + #{category.name} + + + + + #{AdminMessages['categorysystems.details.categories.table.headings.enabled.true']} + + + #{AdminMessages['categorysystems.details.categories.table.headings.enabled.false']} + + + + + + #{AdminMessages['categorysystems.details.categories.table.headings.visible.true']} + + + #{AdminMessages['categorysystems.details.categories.table.headings.visible.false']} + + + + + + #{AdminMessages['categorysystems.details.categories.table.headings.abstract.true']} + + + #{AdminMessages['categorysystems.details.categories.table.headings.abstract.false']} + + + + +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+
+
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-form.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-form.xhtml new file mode 100644 index 000000000..ac09993f9 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-form.xhtml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + +
+

#{CategorySystemDetailsModel.new ? AdminMessages['categorysystems.new.label'] : AdminMessages.getMessage('categorysystems.edit.label', [CategorySystemDetailsModel.domainKey])}

+ + + + + +
+
+ + + + #{AdminMessages['categorysystems.form.domainKey.help']} + +
+
+ + + + #{AdminMessages['categorysystems.form.uri.help']} + +
+
+ + + + #{AdminMessages['categorysystems.form.version.help']} + +
+
+ + + + #{AdminMessages['categorysystems.form.relased.help']} + +
+ + #{AdminMessages['categorysystems.form.buttons.cancel']} + + +
+ +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml new file mode 100644 index 000000000..bd26588e4 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystem-not-found.xhtml @@ -0,0 +1,31 @@ + + + + + + + + + + + + +
+

#{AdminMessages['categorysystems.not_found.title']}

+ + + + + +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystems.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystems.xhtml new file mode 100644 index 000000000..779cbfa94 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/categories/categorysystems.xhtml @@ -0,0 +1,118 @@ +]> + + + + + + + + + + + + + +
+

#{AdminMessages['categorysystems.label']}

+ + + + + + + + + + + + + + + + + + + + + + + + +
#{AdminMessages['categorysystems.table.headers.domainKey']}#{AdminMessages['categorysystems.table.headers.uri']}#{AdminMessages['categorysystems.table.headers.version']}#{AdminMessages['categorysystems.table.headers.released']} + #{AdminMessages['categorysystems.table.headers.actions']} +
#{categorySystem.domainKey}#{categorySystem.uri}#{categorySystem.version}#{categorySystem.released} + + + #{AdminMessages['categorysystems.table.actions.edit']} + + + + +
+ +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/ccm-admin.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/ccm-admin.xhtml new file mode 100644 index 000000000..705e1dc97 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/ccm-admin.xhtml @@ -0,0 +1,63 @@ + + + + #{title} - LibreCCM Admin + + + +
+ + +
+
+ +
+ + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/configuration-class-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/configuration-class-not-found.xhtml new file mode 100644 index 000000000..3beb3feca --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/configuration-class-not-found.xhtml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/configuration.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/configuration.xhtml new file mode 100644 index 000000000..cfffb9180 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/configuration.xhtml @@ -0,0 +1,38 @@ + + + + + + + + + + + + +
+

#{AdminMessages['configuration.label']}

+ +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/failed-to-update-setting.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/failed-to-update-setting.xhtml new file mode 100644 index 000000000..fbceb64aa --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/failed-to-update-setting.xhtml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/invalid-setting-value.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/invalid-setting-value.xhtml new file mode 100644 index 000000000..797eb5374 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/invalid-setting-value.xhtml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/setting-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/setting-not-found.xhtml new file mode 100644 index 000000000..ee11042a2 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/setting-not-found.xhtml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/settings.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/settings.xhtml new file mode 100644 index 000000000..be78558ae --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/settings.xhtml @@ -0,0 +1,403 @@ +]> + + + + + + + + + + + + + +
+

#{confLabel}

+

+ #{confDescription} +

+ + + + + + + + + + + + + + + + + + + + + +
+ #{AdminMessages['configuration.settings.table.headings.label']} + + #{AdminMessages['configuration.settings.table.headings.value']} + + #{AdminMessages['configuration.settings.table.headings.defaultValue']} + + #{AdminMessages['configuration.settings.table.headings.actions']} +
#{setting.label} + + +
#{setting.defaultValue}
+
+ +
#{setting.value}
+
+
+
+
#{setting.defaultValue}
+
+ + + + + + + + + + +
+
+
+ +
+ + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/unsupported-setting-type.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/unsupported-setting-type.xhtml new file mode 100644 index 000000000..a75b13749 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/configuration/unsupported-setting-type.xhtml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/dashboard.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/dashboard.xhtml new file mode 100644 index 000000000..3412d81c1 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/dashboard.xhtml @@ -0,0 +1,52 @@ + + + + + + + + + + + +
+

#{AdminMessages['dashboard.label']}

+
+ +
+
+ +
+

+ + #{page.label} + +

+

+ #{page.description} +

+
+
+
+
+
+
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/export.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/export.xhtml new file mode 100644 index 000000000..d10a2a48f --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/export.xhtml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + +
+

#{AdminMessages['imexport.export.label']}

+ +
+

#{AdminMessages['imexport.export.help']}

+ + + + + + + + + #{AdminMessages['imexport.export.cancel']} + + + +
+
+ +
+ + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/imexport.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/imexport.xhtml new file mode 100644 index 000000000..f5f43b9af --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/imexport.xhtml @@ -0,0 +1,161 @@ + + + + + + + + + + + + +
+

#{AdminMessages['imexport.label']}

+
+
+
+ +
+

+ + #{AdminMessages['import.label']} + +

+

+ #{import.description} +

+
+
+
+ +
+
+ +
+

+ + #{AdminMessages['export.label']} + +

+

+ #{export.description} +

+
+
+
+
+ +
+

#{AdminMessages['imexport.activeexports.heading']}

+ + + +

+ #{AdminMessages['imexport.activeexports.none']} +

+
+ + + + + + + + + + + + + + + + + + + + +
#{AdminMessages['imexport.activeexports.table.columns.name.heading']}#{AdminMessages['imexport.activeexports.table.columns.started.heading']}#{AdminMessages['imexport.activeexports.table.columns.status.heading']}#{AdminMessages['imexport.activeexports.table.columns.actions.heading']}
#{task.name}#{task.started}#{task.status} + + #{AdminMessages['imexport.activeexports.table.columns.actions.button_label']} + +
+
+
+
+ +
+

#{AdminMessages['imexport.activeimports.heading']}

+ + + +

+ #{AdminMessages['imexport.activeimports.none']} +

+
+ + + + + + + + + + + + + + + + + + + + +
#{AdminMessages['imexport.activeimports.table.columns.name.heading']}#{AdminMessages['imexport.activeimports.table.columns.started.heading']}#{AdminMessages['imexport.activeimports.table.columns.status.heading']}#{AdminMessages['imexport.activeimports.table.columns.actions.heading']}
#{task.name}#{task.started} + + + #{AdminMessages['imexport.activeimports.table.columns.status.finished']} + + + #{AdminMessages['imexport.activeimports.table.columns.status.running']} + + + + + #{AdminMessages['imexport.activeimports.table.columns.actions.button_label']} + +
+
+
+
+
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/import.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/import.xhtml new file mode 100644 index 000000000..af59ec114 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/imexport/import.xhtml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + +
+

#{AdminMessages['imexport.import.label']}

+ +
+

#{AdminMessages['imexport.import.help']}

+ + + + #{AdminMessages['imexport.import.cancel']} + + + +
+
+ +
+ \ No newline at end of file diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-details.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-details.xhtml new file mode 100644 index 000000000..d5e12392e --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-details.xhtml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + +
+

#{AdminMessages.getMessage('site.details.title', [SiteDetailsModel.domain])}

+ + +
+
+
#{AdminMessages['sites.details.domain']}
+
#{SiteDetailsModel.domain}
+
+
+
#{AdminMessages['sites.details.defaultSite']}
+
+ + + #{AdminMessages['sites.details.defaultSite.yes']} + + + #{AdminMessages['sites.details.defaultSite.no']} + + +
+
+
+
#{AdminMessages['sites.details.defaultTheme']}
+
#{SiteDetailsModel.defaultTheme}
+
+
+ + + +
+
+ +
+ + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-form.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-form.xhtml new file mode 100644 index 000000000..1d06f31e8 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-form.xhtml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + +
+

+ + + #{AdminMessages['sites.create.label']} + + + #{AdminMessages.getMessage('sites.edit.label', [SiteDetailsModel.domain])} + + +

+ +
+ + + + + #{AdminMessages['sites.form.buttons.cancel']} + + + +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-not-found.xhtml new file mode 100644 index 000000000..e0516e546 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/site-not-found.xhtml @@ -0,0 +1,30 @@ + + + + + + + + + + + + +
+

#{AdminMessages['site.not_found.title']}

+ + + +
+
+ +
+ + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/sites.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/sites.xhtml new file mode 100644 index 000000000..201cb5c55 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/sites/sites.xhtml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + +
+

#{AdminMessages['sites.label']}

+ + + + + + + + + + + + + + + + + + + + + + +
#{AdminMessages['sites.table.heading.domain']}#{AdminMessages['sites.table.heading.defaultSite']}#{AdminMessages['sites.table.heading.defaultTheme']}#{AdminMessages['sites.table.heading.actions']}
#{site.domain} + + + #{AdminMessages['sites.table.defaultSite.yes']} + + + #{AdminMessages['sites.table.defaultSite.no']} + + + + #{site.defaultTheme} + + + + + #{AdminMessages['sites.table.edit']} + + + + +
+
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/systeminformation.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/systeminformation.xhtml new file mode 100644 index 000000000..e4fd53b3d --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/systeminformation.xhtml @@ -0,0 +1,86 @@ + + + + + + + + + + + + +
+

#{AdminMessages['systeminformation.label']}

+ + +
+
+

AdminMessages['systeminformation.tabs.libreccm.label']

+
+ +
+
#{prop.key}
+
#{prop.value}
+
+
+
+
+
+

#{AdminMessages['systeminformation.tabs.java.label']}

+
+ +
+
#{prop.key}
+
#{prop.value}
+
+
+
+
+
+
+
+
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/themes/themes.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/themes/themes.xhtml new file mode 100644 index 000000000..8cd0203c7 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/themes/themes.xhtml @@ -0,0 +1,168 @@ +]> + + + + + + + + + + + +
+

#{AdminMessages['themes.label']}

+ + + +

#{AdminMessages['themes.dialog.new_theme.title']}

+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
#{AdminMessages['themes.table.headers.name']}#{AdminMessages['themes.table.headers.title']}#{AdminMessages['themes.table.headers.version']}#{AdminMessages['themes.table.headers.type']}#{AdminMessages['themes.table.headers.provider']}#{AdminMessages['themes.table.headers.actions']}
#{theme.name}#{theme.title}#{theme.version}#{theme.type}#{theme.provider} + + + + + + + +
+ +
+
+ +
+ +
+
+
+
+ + + + + + + +
+
+
+ +
+ \ No newline at end of file diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles.xhtml new file mode 100644 index 000000000..b9c5d8f28 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles.xhtml @@ -0,0 +1,48 @@ + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/email-form.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/email-form.xhtml new file mode 100644 index 000000000..a8a5d164b --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/email-form.xhtml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + #{AdminMessages['usersgroupsroles.users.email.form.address.help']} + +
+
+ + +
+
+ + +
+ +
+
+ +
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/email-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/email-not-found.xhtml new file mode 100644 index 000000000..37aff6e5d --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/email-not-found.xhtml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-details.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-details.xhtml new file mode 100644 index 000000000..232969eea --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-details.xhtml @@ -0,0 +1,237 @@ +]> + + + + + + + + + + + + + +
+
+
#{AdminMessages['usersgroupsroles.groups.group_details.groupId']}
+
#{GroupDetailsModel.groupId}
+
+
+
#{AdminMessages['usersgroupsroles.groups.group_details.uuid']}
+
#{GroupDetailsModel.uuid}
+
+
+
#{AdminMessages['usersgroupsroles.groups.group_details.groupName']}
+
#{GroupDetailsModel.groupName}
+
+
+ + + + + + #{AdminMessages['usersgroupsroles.groups.group_details.edit_group']} + + + +
+

+ #{AdminMessages['usersgroupsroles.groups.group_details.members.heading']} +

+ + +
+ + + + + + + + + +
+

+ #{AdminMessages['usersgroupsroles.groups.groups_details.roles.heading']} +

+ + +
+ + + + + + + + + + +
+
+ diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-form.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-form.xhtml new file mode 100644 index 000000000..389b7ec73 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-form.xhtml @@ -0,0 +1,77 @@ + + + + + + + + + + + +
  • + #{GroupDetailsModel.newGroup ? AdminMessages['usersgroupsroles.groups.breadcrumb.new'] : AdminMessages['usersgroupsroles.groups.breadcrumb.edit']} +
  • +
    + + + + + + +
    +
    + + + + #{AdminMessages['usersgroupsroles.groups.form.username.help']} + +
    + + #{AdminMessages['usersgroupsroles.groups.form.buttons.cancel']} + + +
    +
    + +
    + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml new file mode 100644 index 000000000..23cbab073 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/group-not-found.xhtml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/groups.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/groups.xhtml new file mode 100644 index 000000000..de27f6318 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/groups.xhtml @@ -0,0 +1,128 @@ +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #{AdminMessages['usersgroupsroles.groups.table.headers.groupname']}#{AdminMessages['usersgroupsroles.groups.table.headers.actions']}
    #{group.name} + + + + + + #{AdminMessages['usersgroupsroles.groups.detailslink.label']} + + + + + +
    +
    +
    + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/overview.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/overview.xhtml new file mode 100644 index 000000000..1a3df4cff --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/overview.xhtml @@ -0,0 +1,24 @@ + + + + + + + + + + + + +

    #{UsersGroupsRolesOverviewModel.activeUsersCount} #{AdminMessages['usersgroupsroles.active_users_count.label']}

    +

    #{UsersGroupsRolesOverviewModel.disabledUsersCount} #{AdminMessages['usersgroupsroles.disabled_users_count.label']}

    +

    #{UsersGroupsRolesOverviewModel.groupsCount} #{AdminMessages['usersgroupsroles.groups_count.label']}

    +

    #{UsersGroupsRolesOverviewModel.rolesCount} #{AdminMessages['usersgroupsroles.roles_count.label']}

    +
    +
    + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-details.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-details.xhtml new file mode 100644 index 000000000..b532cd5aa --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-details.xhtml @@ -0,0 +1,149 @@ +]> + + + + + + + + + + + + + + +
    +
    +
    #{AdminMessages['usersgroupsroles.roles.role_details.roleId']}
    +
    #{RoleDetailsModel.roleName}
    +
    +
    +
    #{AdminMessages['usersgroupsroles.roles.role_details.uuid']}
    +
    #{RoleDetailsModel.uuid}
    +
    +
    +
    #{AdminMessages['usersgroupsroles.roles.role_details.name']}
    +
    #{RoleDetailsModel.roleName}
    +
    +
    + + + + + + #{AdminMessages['usersgroupsroles.roles.role_details.edit_role']} + + + +
    +

    + #{AdminMessages['usersgroupsroles.roles.role_details.members.heading']} +

    + + +
    + + +
      + +
    • + #{member.partyName} +
    • +
      +
    +
    + + + +
    +
    +
    + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-form.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-form.xhtml new file mode 100644 index 000000000..018da950f --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-form.xhtml @@ -0,0 +1,77 @@ + + + + + + + + + + + +
  • + #{RoleDetailsModel.newRole ? AdminMessages['usersgroupsroles.roles.breadcrumb.new'] : AdminMessages['usersgroupsroles.roles.breadcrumb.edit']} +
  • +
    + + + + + + +
    +
    + + + + #{AdminMessages['usersgroupsroles.roles.form.rolename.help']} + +
    + + #{AdminMessages['usersgroupsroles.roles.form.buttons.cancel']} + + +
    +
    + +
    + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-not-found.xhtml new file mode 100644 index 000000000..25f4f3a0d --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/role-not-found.xhtml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/roles.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/roles.xhtml new file mode 100644 index 000000000..5b0b7a27f --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/roles.xhtml @@ -0,0 +1,129 @@ +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    #{AdminMessages['usersgroupsroles.roles.table.headers.rolename']}#{AdminMessages['usersgroupsroles.roles.table.headers.actions']}
    #{role.name} + + + + + + #{AdminMessages['usersgroupsroles.roles.detailslink.label']} + + + + + +
    +
    +
    + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-details.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-details.xhtml new file mode 100644 index 000000000..573c25805 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-details.xhtml @@ -0,0 +1,458 @@ +]> + + + + + + + + + + + + + +
    +
    +
    #{AdminMessages['usersgroupsroles.users.user_details.id']}
    +
    #{UserDetailsModel.userId}
    +
    +
    +
    #{AdminMessages['usersgroupsroles.users.user_details.uuid']}
    +
    #{UserDetailsModel.uuid}
    +
    +
    +
    #{AdminMessages['usersgroupsroles.users.user_details.name']}
    +
    #{UserDetailsModel.name}
    +
    +
    +
    #{AdminMessages['usersgroupsroles.users.user_details.given_name']}
    +
    #{UserDetailsModel.givenName}
    +
    +
    +
    #{AdminMessages['usersgroupsroles.users.user_details.family_name']}
    +
    #{UserDetailsModel.familyName}
    +
    +
    +
    + #{AdminMessages['usersgroupsroles.users.user_details.primary_email_address']} +
    +
    +
    +
    #{AdminMessages['usersgroupsroles.users.user_details.additional_email_addresses.cols.address']}
    +
    + #{UserDetailsModel.primaryEmailAddress.address} +
    +
    + #{AdminMessages['usersgroupsroles.users.user_details.additional_email_addresses.cols.boucing']} +
    +
    + + + #{AdminMessages['usersgroupsroles.users.user_details.email_address.bouncing.yes']} + + + #{AdminMessages['usersgroupsroles.users.user_details.email_address.bouncing.no']} + + +
    +
    + #{AdminMessages['usersgroupsroles.users.user_details.additional_email_addresses.cols.verified']} +
    +
    + + + #{AdminMessages['usersgroupsroles.users.user_details.email_address.verified.yes']} + + + #{AdminMessages['usersgroupsroles.users.user_details.email_address.verified.no']} + + +
    +
    +
    +
    +
    +
    + #{AdminMessages['usersgroupsroles.users.user_details.disabled']} +
    +
    + + + #{AdminMessages['usersgroupsroles.users.user_details.disabled.yes']} + + + #{AdminMessages['usersgroupsroles.users.user_details.disabled.no']} + + +
    +
    +
    +
    + #{AdminMessages['usersgroupsroles.users.user_details.password_reset_required']} +
    +
    + + + #{AdminMessages['usersgroupsroles.users.user_details.password_reset_required.yes']} + + + #{AdminMessages['usersgroupsroles.users.user_details.password_reset_required.no']} + + +
    +
    +
    + + + + + + #{AdminMessages['usersgroupsroles.users.user_details.edit_user']} + + + +
    +

    + #{AdminMessages['usersgroupsroles.users.user_details.additional_email_addresses.heading']} +

    + +
    + + + + + + + + + + + + + + + + + + + + +
    + #{AdminMessages['usersgroupsroles.users.user_details.additional_email_addresses.cols.address']} + + #{AdminMessages['usersgroupsroles.users.user_details.additional_email_addresses.cols.boucing']} + + #{AdminMessages['usersgroupsroles.users.user_details.additional_email_addresses.cols.verified']} + + #{AdminMessages['usersgroupsroles.users.user_details.additional_email_addresses.cols.actions']} +
    + #{address.address} + + + + #{AdminMessages['usersgroupsroles.users.user_details.email_address.bouncing.yes']} + + + #{AdminMessages['usersgroupsroles.users.user_details.email_address.bouncing.no']} + + + + + + #{AdminMessages['usersgroupsroles.users.user_details.email_address.verified.yes']} + + + #{AdminMessages['usersgroupsroles.users.user_details.email_address.verified.no']} + + + + + + + + + #{AdminMessages['usersgroupsroles.users.user_details.email_addresses.edit']} + + + + + +
    +
    + + + +
    + +
    +

    + #{AdminMessages['usersgroupsroles.users.user_details.groups.heading']} +

    + + +
    + + + + + + + + + +
    +

    + #{AdminMessages['usersgroupsroles.users.user_details.roles.heading']} +

    + + +
    + + + + + + + + + + +
    + +
    + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-form.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-form.xhtml new file mode 100644 index 000000000..d19034fdc --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-form.xhtml @@ -0,0 +1,197 @@ + + + + + + + + + + + +
  • + + #{UserDetailsModel.newUser ? AdminMessages['usersgroupsroles.users.breadcrumb.new'] : AdminMessages['usersgroupsroles.users.breadcrumb.edit']} +
  • +
    + + + + + +
    +
    + + + + #{AdminMessages['usersgroupsroles.users.form.username.help']} + +
    +
    + + + + #{AdminMessages['usersgroupsroles.users.form.givenname.help']} + +
    +
    + + + + #{AdminMessages['usersgroupsroles.users.form.familyname.help']} + +
    +
    + + + + #{AdminMessages['usersgroupsroles.users.form.primaryemailaddress.help']} + +
    +
    + + +
    +
    + + +
    + +
    + + + + #{AdminMessages['usersgroupsroles.users.form.password.help']} + +
    +
    + + + + #{AdminMessages['usersgroupsroles.users.form.passwordconfirmation.help']} + +
    +
    +
    + + +
    +
    + + +
    + + #{AdminMessages['usersgroupsroles.users.form.buttons.cancel']} + + +
    +
    +
    + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml new file mode 100644 index 000000000..4f4079020 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/user-not-found.xhtml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/users.xhtml b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/users.xhtml new file mode 100644 index 000000000..7cf4fa1e9 --- /dev/null +++ b/ccm-core/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/users-groups-roles/users.xhtml @@ -0,0 +1,158 @@ +]> + + + + + + + + + + + + +
    +
    +
    +
    + + +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    #{AdminMessages['usersgroupsroles.users.table.headers.username']}#{AdminMessages['usersgroupsroles.users.table.headers.givenname']}#{AdminMessages['usersgroupsroles.users.table.headers.familyname']}#{AdminMessages['usersgroupsroles.users.table.headers.email']}#{AdminMessages['usersgroupsroles.users.table.headers.disabled']}#{AdminMessages['usersgroupsroles.users.table.headers.actions']}
    #{user.name}#{user.givenName}#{user.familyName} + #{user.primaryEmailAddress.address} + + #{user.banned ? AdminMessages['usersgroupsroles.users.table.headers.disabled.true'] : AdminMessages['usersgroupsroles.users.table.headers.disabled.false']} + + + + + + + #{AdminMessages['usersgroupsroles.users.detailslink.label']} + + + + + + + +
    +
    +
    + + diff --git a/ccm-core/src/main/resources/assets/bootstrap/bootstrap-icons.svg b/ccm-core/src/main/resources/assets/bootstrap/bootstrap-icons.svg new file mode 100644 index 000000000..f7731a14b --- /dev/null +++ b/ccm-core/src/main/resources/assets/bootstrap/bootstrap-icons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ccm-core/src/main/resources/com/arsdigita/bebop/BebopConfig.properties b/ccm-core/src/main/resources/com/arsdigita/bebop/BebopConfig.properties new file mode 100644 index 000000000..773a769f4 --- /dev/null +++ b/ccm-core/src/main/resources/com/arsdigita/bebop/BebopConfig.properties @@ -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 + +description=Settings for legacy UI framework used by CCM +title=Bebop Configuration +presenterClassName.label=Presenter Class name +presenterClassName.desc=Fully qualified class name of the presenter implementation to use +basePageClassName.desc=Fully qualified class name of the basic page component +basePageClassName.label=Base Page Class Name +tidyConfigFile.desc= +tidyConfigFile.label=Tidy Configuration File +fancyErrors.desc= +fancyErrors.label=Fancy Errors? +dcpOnButtons.desc= +dcpOnButtons.label=Enable Double-Click protection on buttons? +dcpOnLinks.desc= +dcpOnLinks.label=Enable Double-Click protecttion on links? +treeSelectEnabled.desc= +treeSelectEnabled.label=Tree Select Enabled +dhtmlEditors.desc= +dhtmlEditors.label=DHTML editors +defaultDhtmlEditor.desc=Default DHTML Editor +defaultDhtmlEditor.label=Default DHTML Editor +dhtmlEditorSrcFile.desc= +dhtmlEditorSrcFile.label=DHTML Editor Main File +showClassName.desc= +showClassName.label=Show Class Name? diff --git a/ccm-core/src/main/resources/com/arsdigita/bebop/BebopConfig_de.properties b/ccm-core/src/main/resources/com/arsdigita/bebop/BebopConfig_de.properties new file mode 100644 index 000000000..773a769f4 --- /dev/null +++ b/ccm-core/src/main/resources/com/arsdigita/bebop/BebopConfig_de.properties @@ -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 + +description=Settings for legacy UI framework used by CCM +title=Bebop Configuration +presenterClassName.label=Presenter Class name +presenterClassName.desc=Fully qualified class name of the presenter implementation to use +basePageClassName.desc=Fully qualified class name of the basic page component +basePageClassName.label=Base Page Class Name +tidyConfigFile.desc= +tidyConfigFile.label=Tidy Configuration File +fancyErrors.desc= +fancyErrors.label=Fancy Errors? +dcpOnButtons.desc= +dcpOnButtons.label=Enable Double-Click protection on buttons? +dcpOnLinks.desc= +dcpOnLinks.label=Enable Double-Click protecttion on links? +treeSelectEnabled.desc= +treeSelectEnabled.label=Tree Select Enabled +dhtmlEditors.desc= +dhtmlEditors.label=DHTML editors +defaultDhtmlEditor.desc=Default DHTML Editor +defaultDhtmlEditor.label=Default DHTML Editor +dhtmlEditorSrcFile.desc= +dhtmlEditorSrcFile.label=DHTML Editor Main File +showClassName.desc= +showClassName.label=Show Class Name? diff --git a/ccm-core/src/main/resources/com/arsdigita/dispatcher/DispatcherConfig.properties b/ccm-core/src/main/resources/com/arsdigita/dispatcher/DispatcherConfig.properties new file mode 100644 index 000000000..2606701a7 --- /dev/null +++ b/ccm-core/src/main/resources/com/arsdigita/dispatcher/DispatcherConfig.properties @@ -0,0 +1,27 @@ +# 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 + +title=Dispatcher Configuration +description=Settings for the legacy dispatcher +cachingActive.desc=Toggle whether or not to use HTTP/1.1 caching +cachingActive.label=Caching Active? +defaultExpiry.desc=Set the default expiration time for HTTP caching +defaultExpiry.label=Default Cache Expiry +statusUrlPrefix.desc=The default page class. A custom installation may provide it's own implementation. Use with care because all pages inherit from this class! +staticUrlPrefix.label=Static URL Prefix +defaultPageClass.desc=Prefix used for serving static files +defaultPageClass.label=Default Page Class diff --git a/ccm-core/src/main/resources/com/arsdigita/dispatcher/DispatcherConfig_de.properties b/ccm-core/src/main/resources/com/arsdigita/dispatcher/DispatcherConfig_de.properties new file mode 100644 index 000000000..49a8a02ca --- /dev/null +++ b/ccm-core/src/main/resources/com/arsdigita/dispatcher/DispatcherConfig_de.properties @@ -0,0 +1,27 @@ +# 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 + +title=Dispatcher Configuration +description= +cachingActive.desc=Toggle whether or not to use HTTP/1.1 caching +cachingActive.label=Caching Active? +defaultExpiry.desc=Set the default expiration time for HTTP caching +defaultExpiry.label=Default Cache Expiry +statusUrlPrefix.desc=The default page class. A custom installation may provide it's own implementation. Use with care because all pages inherit from this class! +staticUrlPrefix.label=Static URL Prefix +defaultPageClass.desc=Prefix used for serving static files +defaultPageClass.label=Default Page Class diff --git a/ccm-core/src/main/resources/com/arsdigita/globalization/GlobalizationConfigDescription.properties b/ccm-core/src/main/resources/com/arsdigita/globalization/GlobalizationConfigDescription.properties index 498ae2705..982cecd08 100644 --- a/ccm-core/src/main/resources/com/arsdigita/globalization/GlobalizationConfigDescription.properties +++ b/ccm-core/src/main/resources/com/arsdigita/globalization/GlobalizationConfigDescription.properties @@ -15,7 +15,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA -description = A configuration record for configuration of the core globalization package +globalization.config.description = A configuration record for configuration of the core globalization package defaultCharset.label = Default Charset defaultCharset.description = Default character set for locales not explicitly listed in the locales parameter. \ No newline at end of file diff --git a/ccm-core/src/main/resources/com/arsdigita/kernel/KernelConfigDescription.properties b/ccm-core/src/main/resources/com/arsdigita/kernel/KernelConfigDescription.properties index 56a265997..1347247da 100644 --- a/ccm-core/src/main/resources/com/arsdigita/kernel/KernelConfigDescription.properties +++ b/ccm-core/src/main/resources/com/arsdigita/kernel/KernelConfigDescription.properties @@ -3,8 +3,8 @@ description = Configure several basic properties for LibreCCM debugEnabled.label = Global debug flag debugEnabled.description = Enables or disables WAF debugging -webdev_supportEnabled.label = Webdev support enabled? -webdev_supportEnabled.description = Enables or disables Webdev support +webdevSupportEnabled.label=Webdev support enabled? +webdevSupportEnabled.description=Enables or disables Webdev support data_permissionCheckEnabled.label = DML permission checking flag data_permissionCheckEnabled.description = Enables or disables permissions checks on database writes @@ -18,8 +18,18 @@ ssoEnabled.description = Enables or disables SSO login rememberLoginEnabled.label = Remember login by default rememberLoginEnabled.description = Determines whether the "remember login" feature is enabled or disabled by default -kernelConfig.secureLoginEnabled.label = Require secure login? -kernelConfig.secureLoginEnabled.description = Accept only credentials presented over secure connection? - supportedLanguages.label = Set the supported languages for categorization supportedLanguages.description = Set the languages supported for this installation (for content creation). Uses the ISO language codes. +title=Kernel Configuration +dataPermissionCheckEnabled.label=Data Permission Check Enabled? +defaultLanguage.label=Default Language +exportPath.label=Export Path +exportPath.description=Where to store export files +importPath.label=Import Path +importPath.description=Where to look for files to import +jwtSecret.label=JWT secret +jwtSecret.description=Secret for generating JSON Web Tokens +secureLoginEnabled.label=Secure Login enabled? +secureLoginEnabled.description=Force HTTPS for login? +systemEmailAddress.label=System Email address +systemEmailAddress.description=Email address used a from address for mail generated by the CCM diff --git a/ccm-core/src/main/resources/com/arsdigita/kernel/KernelConfigDescription_de.properties b/ccm-core/src/main/resources/com/arsdigita/kernel/KernelConfigDescription_de.properties new file mode 100644 index 000000000..1347247da --- /dev/null +++ b/ccm-core/src/main/resources/com/arsdigita/kernel/KernelConfigDescription_de.properties @@ -0,0 +1,35 @@ +description = Configure several basic properties for LibreCCM + +debugEnabled.label = Global debug flag +debugEnabled.description = Enables or disables WAF debugging + +webdevSupportEnabled.label=Webdev support enabled? +webdevSupportEnabled.description=Enables or disables Webdev support + +data_permissionCheckEnabled.label = DML permission checking flag +data_permissionCheckEnabled.description = Enables or disables permissions checks on database writes + +primaryUserIdentifier.label = The primary user identification +primaryUserIdentifier.description = Determines whether email addresses or screen names are used to authenticate users. Valid values: "screen_name" or "email" + +ssoEnabled.label = Enable SSO login? +ssoEnabled.description = Enables or disables SSO login + +rememberLoginEnabled.label = Remember login by default +rememberLoginEnabled.description = Determines whether the "remember login" feature is enabled or disabled by default + +supportedLanguages.label = Set the supported languages for categorization +supportedLanguages.description = Set the languages supported for this installation (for content creation). Uses the ISO language codes. +title=Kernel Configuration +dataPermissionCheckEnabled.label=Data Permission Check Enabled? +defaultLanguage.label=Default Language +exportPath.label=Export Path +exportPath.description=Where to store export files +importPath.label=Import Path +importPath.description=Where to look for files to import +jwtSecret.label=JWT secret +jwtSecret.description=Secret for generating JSON Web Tokens +secureLoginEnabled.label=Secure Login enabled? +secureLoginEnabled.description=Force HTTPS for login? +systemEmailAddress.label=System Email address +systemEmailAddress.description=Email address used a from address for mail generated by the CCM diff --git a/ccm-core/src/main/resources/db/migrations/org/libreccm/ccm_core/h2/V7_0_0_37__change_category_domain_released_to_date.sql b/ccm-core/src/main/resources/db/migrations/org/libreccm/ccm_core/h2/V7_0_0_37__change_category_domain_released_to_date.sql new file mode 100644 index 000000000..3717f43ab --- /dev/null +++ b/ccm-core/src/main/resources/db/migrations/org/libreccm/ccm_core/h2/V7_0_0_37__change_category_domain_released_to_date.sql @@ -0,0 +1 @@ +alter table CCM_CORE.CATEGORY_DOMAINS alter column RELEASED type date; \ No newline at end of file diff --git a/ccm-core/src/main/resources/db/migrations/org/libreccm/ccm_core/pgsql/V7_0_0_37__change_category_domain_released_to_date.sql b/ccm-core/src/main/resources/db/migrations/org/libreccm/ccm_core/pgsql/V7_0_0_37__change_category_domain_released_to_date.sql new file mode 100644 index 000000000..3717f43ab --- /dev/null +++ b/ccm-core/src/main/resources/db/migrations/org/libreccm/ccm_core/pgsql/V7_0_0_37__change_category_domain_released_to_date.sql @@ -0,0 +1 @@ +alter table CCM_CORE.CATEGORY_DOMAINS alter column RELEASED type date; \ No newline at end of file diff --git a/ccm-core/src/main/resources/org/libreccm/ui/AdminBundle.properties b/ccm-core/src/main/resources/org/libreccm/ui/AdminBundle.properties new file mode 100644 index 000000000..bd66256dd --- /dev/null +++ b/ccm-core/src/main/resources/org/libreccm/ui/AdminBundle.properties @@ -0,0 +1,564 @@ +systeminformation.description=Provides several informations about LibreCCM and the environment +systeminformation.label=System Information +applications.label=Applications +applications.description=Manage application instances +imexport.label=Import/Export +categorymanager.label=Categories Manager +categories.description=Manage categories +configuration.label=Configuration +configuration.description=Manage configuration settings +dashboard.label=Dashboard +dashboard.description=Provides access to all applications +imexport.description=Import and export entities +sites.label=Sites +sites.description=Manage sites +usersgroupsroles.label=Users/Groups/Roles +usersgroupsroles.description=Manage users, groups and roles +systeminformation.tabs.libreccm.label=LibreCCM System Information +systeminformation.tabs.java.label=Java System Properties +breadcrumbs.start=LibreCCM Admin +usersgroupsroles.users.label=Users +usersgroupsroles.groups.label=Groups +usersgroupsroles.roles.label=Roles +usersgroupsroles.overview.label=Overview +usersgroupsroles.active_users_count.label=active users +usersgroupsroles.disabled_users_count.label=disabled users +usersgroupsroles.groups_count.label=groups +usersgroupsroles.roles_count.label=roles +usersgroupsroles.users.table.headers.username=User Name +usersgroupsroles.users.table.headers.givenname=Given Name +usersgroupsroles.users.table.headers.familyname=Family Name +usersgroupsroles.users.table.headers.email=Email +usersgroupsroles.users.table.headers.disabled=Disabled? +usersgroupsroles.users.table.headers.actions=Actions +usersgroupsroles.users.table.headers.disabled.true=Yes +usersgroupsroles.users.table.headers.disabled.false=No +usersgroupsroles.users.detailslink.label=Details +usersgroupsroles.users.disablebutton.label=Disable +usersgroupsroles.users.add=Add user +usersgroupsroles.users.filter.label=Filter users +usersgroupsroles.users.filter.submit=Submit +usersgroupsroles.users.not_found.title=User not found +usersgroupsroles.users.user_details.title=User {0} Details +usersgroupsroles.users.user_details.id=User ID +usersgroupsroles.users.user_details.uuid=UUID +usersgroupsroles.users.user_details.name=Name +usersgroupsroles.users.user_details.given_name=Given name +usersgroupsroles.users.user_details.familyname=Familyname +usersgroupsroles.users.user_details.primary_email_address=Primary email address +usersgroupsroles.users.user_details.disabled=Disabled? +usersgroupsroles.users.user_details.disabled.yes=Yes +usersgroupsroles.users.user_details.disabled.no=No +usersgroupsroles.users.user_details.password_reset_required=Password reset required? +usersgroupsroles.users.user_details.password_reset_required.yes=Yes +usersgroupsroles.users.user_details.password_reset_required.no=No +usersgroupsroles.users.user_details.additional_email_addresses.heading=Additional email addresses +'usersgroupsroles.users.user_details.additional_email_addresses.cols.address=Address +usersgroupsroles.users.user_details.additional_email_addresses.cols.boucing=Bouncing? +usersgroupsroles.users.user_details.additional_email_addresses.cols.verified=Verified +usersgroupsroles.users.user_details.additional_email_addresses.cols.actions=Actions +usersgroupsroles.users.user_details.email_address.bouncing.yes=Yes +usersgroupsroles.users.user_details.email_address.bouncing.no=No +usersgroupsroles.users.user_details.email_address.verified.yes=Yes +usersgroupsroles.users.user_details.email_address.verified.no=No +usersgroupsroles.users.user_details.email_addresses.remove=Remove +usersgroupsroles.users.user_details.email_addresses.edit=Edit +usersgroupsroles.users.user_details.groups.heading=Groups Memberships +usersgroupsroles.users.user_details.groups.add=Add +usersgroupsroles.users.user_details.groups.remove=Remove +usersgroupsroles.users.user_details.roles.heading=Roles +usersgroupsroles.users.user_details.roles.add=Add +usersgroupsroles.users.user_details.roles.remove=Remove +usersgroupsroles.users.user_details.edit_user=Edit +usersgroupsroles.users.new.errors.password.empty=Password of a new user can't be empty +usersgroupsroles.users.new.errors.password.no_match=Password and confirmation do not match +usersgroupsroles.users.edit.title=Edit user {0} +usersgroupsroles.users.form.username.label=User name +usersgroupsroles.users.form.username.help=Unique name of the user which can be used to login +usersgroupsroles.users.form.givenname.label=Given Name +usersgroupsroles.users.form.givenname.help=Given name of the user +usersgroupsroles.users.form.familyname.label=Family Name +usersgroupsroles.users.form.familyname.help=Family name of the user +usersgroupsroles.users.form.primaryemailaddress.label=Primary Email address +usersgroupsroles.users.form.primaryemailaddress.help=Primary email address of the user +usersgroupsroles.users.form.primaryemailaddress.boucing.label=Primary email address bouncing? +usersgroupsroles.users.form.primaryemailaddress.verified.label=Primary email address verified? +usersgroupsroles.users.form.password=Password +usersgroupsroles.users.form.password.help=Password of the new user +usersgroupsroles.users.form.passwordconfirmation=Password Confirmation +usersgroupsroles.users.form.passwordconfirmation.help=Confirm the password by repeating it +usersgroupsroles.users.form.banned.label=Disabled? +usersgroupsroles.users.form.passwordresetrequired.label=Password reset required +usersgroupsroles.users.form.buttons.cancel=Cancel +usersgroupsroles.users.form.buttons.create=Create new user +usersgroupsroles.users.form.buttons.save=Save +usersgroupsroles.users.create.title=Create new user +usersgroupsroles.users.email.not_found.title=Email address not found +usersgroupsroles.users.email.not_found.message=User {0} has no email address with ID {1}. +usersgroupsroles.users.breadcrumb.new=New User +usersgroupsroles.users.breadcrumb.edit=Edit user +usersgroupsroles.users.breadcrumbs.email.add=Add Email Address +usersgroupsroles.users.breadcrumbs.email.edit=Edit Email Address +usersgroupsroles.users.email.edit.title=Edit Email Address +usersgroupsroles.users.email.add.title=Add Email Address +usersgroupsroles.users.email.form.address.label=Email Address +usersgroupsroles.users.email.form.address.help=The Email Address +usersgroupsroles.users.email.form.bouncing.label=Bouncing? +usersgroupsroles.users.email.form.verified.label=Verified +usersgroupsroles.users.email.form.buttons.add=Add +usersgroupsroles.users.email.form.buttons.save=Save +usersgroupsroles.users.disable.confirm.title=Confirm to disable user +usersgroupsroles.users.disable.confirm.cancel=Cancel +usersgroupsroles.users.disable.confirm.message=Are you sure to disable user {0} +usersgroupsroles.users.disable.confirm.yes=Disable User +usersgroupsroles.users.user_details.email_addresses.remove.confirm.title=Confirm Removal Of Email Address +usersgroupsroles.users.user_details.email_addresses.remove.confirm.cancel=Cancel +usersgroupsroles.users.user_details.email_addresses.remove.confirm.message=Are you sure to remove email address {0}? +usersgroupsroles.users.user_details.email_addresses.remove.confirm.yes=Remove Email Address +usersgroupsroles.users.user_details.groups.none=This user is not a member of any group +usersgroupsroles.users.user_details.groups.edit=Edit +usersgroupsroles.users.user_details.roles.edit=Edit +usersgroupsroles.users.user_details.roles.none=No roles assigned to this user +usersgroupsroles.users.user_details.email_addresses.none=This user has no additional email addresses +usersgroupsroles.users.user_details.additional_email_addresses.add=Add email address +usersgroupsroles.users.user_details.groups.dialog.title=Edit group memberships +usersgroupsroles.users.user_details.groups.dialog.close=Cancel +usersgroupsroles.users.user_details.groups.dialog.save=Save +usersgroupsroles.users.user_details.roles.dialog.title=Edit role memberships +usersgroupsroles.users.user_details.roles.dialog.close=Cancel +usersgroupsroles.users.user_details.groups.dialog.save=Save +usersgroupsroles.users.user_details.family_name=Family Name +usersgroupsroles.users.user_details.email_addresses.errors.address_empty=Address can't be empty +usersgroupsroles.groups.table.headers.groupname=Group +usersgroupsroles.groups.table.headers.actions=Actions +usersgroupsroles.groups.detailslink.label=Details +usersgroupsroles.groups.delete.button.label=Delete +usersgroupsroles.groups.delete.confirm.title=Delete group +usersgroupsroles.groups.delete.confirm.cancel=Cancel +usersgroupsroles.groups.delete.confirm.yes=Delete Group +usersgroupsroles.groups.delete.confirm.message=Are you sure to delete group {0}? +usersgroupsroles.groups.add=Add group +usersgroupsroles.groups.create.title=Create new group +usersgroupsroles.groups.edit.title=Edit group {0} +usersgroupsroles.groups.breadcrumb.new=New Group +usersgroupsroles.groups.breadcrumb.edit=Edit Group +usersgroupsroles.groups.form.groupname.label=Group Name +usersgroupsroles.groups.form.username.help=Unique name of the group +usersgroupsroles.groups.form.buttons.cancel=Cancel +usersgroupsroles.groups.form.buttons.create=Create new group +usersgroupsroles.groups.form.buttons.save=Save +usersgroupsroles.groups.group_details.title=Details of Group {0} +usersgroupsroles.groups.group_details.id=Group ID +usersgroupsroles.groups.group_details.uuid=Group UUID +usersgroupsroles.groups.group_details.name=Group Name +usersgroupsroles.groups.group_details.edit_group=Edit group +usersgroupsroles.groups.group_details.members.heading=Members +usersgroupsroles.groups.group_details.members.edit=Add/remove members +usersgroupsroles.groups.group_details.members.dialog.title=Add/remove members +usersgroupsroles.groups.group_details.members.dialog.close=Cancel +usersgroupsroles.groups.group_details.members.dialog.save=Apply +usersgroupsroles.groups.group_details.members.none=This group has no members +usersgroupsroles.groups.groups_details.roles.heading=Roles +usersgroupsroles.groups.group_details.roles.edit=Edit +usersgroupsroles.groups.group_details.roles.dialog.title=Edit role memberships +usersgroupsroles.groups.group_details.roles.dialog.close=Cancel +usersgroupsroles.users.user_details.roles.dialog.save=Apply +usersgroupsroles.groups.group_details.roles.dialog.save=Apply +usersgroupsroles.groups.group_details.roles.none=No roles assigned to this group +usersgroupsroles.groups.not_found.title=Group not found +usersgroupsroles.groups.group_details.groupId=Group ID +usersgroupsroles.groups.group_details.groupName=Group Name +usersgroupsroles.users.user_details.additional_email_addresses.cols.address=Address +usersgroupsroles.roles.not_found_message=Role {0} not found +usersgroupsroles.roles.not_found.title=Role not found +usersgroupsroles.roles.role_details.title=Details Role {0} +usersgroupsroles.roles.role_details.roleId=Role ID +usersgroupsroles.roles.role_details.name=Name +usersgroupsroles.roles.role_details.edit_role=Edit role +usersgroupsroles.roles.role_details.members.heading=Members +usersgroupsroles.roles.role_details.members.edit=Add/remove members +usersgroupsroles.roles.role_details.members.dialog.title=Add/remove members +usersgroupsroles.roles.role_details.members.dialog.close=Cancel +usersgroupsroles.roles.role_details.members.dialog.save=Apply +usersgroupsroles.roles.role_details.members.none=This role has no members +usersgroupsroles.roles.create.title=Create new role +usersgroupsroles.roles.edit.title=Edit role {0} +usersgroupsroles.roles.breadcrumb.new=New Role +usersgroupsroles.roles.breadcrumb.edit=Edit Role +usersgroupsroles.roles.form.rolename.label=Name +usersgroupsroles.roles.form.rolename.help=Unique name of the role +usersgroupsroles.roles.form.buttons.create=Create Role +usersgroupsroles.roles.form.buttons.save=Save +usersgroupsroles.roles.add=Add role +usersgroupsroles.roles.table.headers.rolename=Role +usersgroupsroles.roles.table.headers.actions=Actions +usersgroupsroles.roles.detailslink.label=Details +usersgroupsroles.roles.delete.button.label=Delete +usersgroupsroles.roles.delete.confirm.title=Are you sure to delele this role? +usersgroupsroles.roles.delete.confirm.message=Are you sure to delete role {0}? +usersgroupsroles.roles.delete.confirm.cancel=Cancel +usersgroupsroles.roles.delete.confirm.yes=Delete role +usersgroupsroles.roles.role_details.uuid=UUID +usersgroupsroles.groups.form.errors.name_not_empty=The name of a group can't be empty +usersgroupsroles.groups.form.errors.name_invalid=The name of a group may only contain the characters a to z, A to Z, 0 to 9, the dash (-), and the underscore (_). +usersgroupsroles.roles.form.errors.name_not_empty=The name of a role can't be empty +usersgroupsroles.users.form.errors.username.empty=The username of a user can't be empty +usersgroupsroles.users.form.errors.username.invalid=The username may only contain the characters a to z, A to Z, 0 to 9, the dash (-) and the underscore (_) +usersgroupsroles.users.form.errors.primary_email.empty=The primary email address of a user can't b be empty +usersgroupsroles.users.form.errors.primary_email.invalid=The primary email address of a user must be a valid email address +configuration.table.headings.title=Configuration +configuration.table.headings.description=Description +configuration.settings.label=Settings +configuration.settings.table.headings.label=Setting +configuration.settings.table.headings.value=Current value +configuration.settings.table.headings.defaultValue=Default Value +configuration.settings.table.headings.actions=Aktionen +configuration.settings.table.actions.info=Info +configuration.settings.table.actions.info.help=Show description of setting {0} +configuration.settings.table.actions.edit=Edit +configuration.settings.table.actions.reset.help=Reset setting {0} to default value {1} +configuration.settings.table.actions.reset=Reset +configuration.settings.setting.info.label=Setting {0} Info +configuration.settings.setting.info.close=Close +configuration.settings.setting.info.dismiss=OK +configuration.settings.setting.dialog.title=Edit setting {0} +configuration.settings.setting.dialog.close=Cancel +configuration.settings.setting.dialog.save=Save +configuration.settings.setting.dialog.unsupported_type=Sorry, settings of type "{0}" are not supported yet. +configuration.class.not_found.title=Configuration class not found +configuration.class.not_found.message=Configuration class {0} not found +configuration.setting.type_unsupported.title=Unsupported type of setting +configuration.setting.type_unsupported.message=Type {2} of setting {1} from configuration class {0} is not supported +configuration.setting.invalid_type.title=Invalid type of value +configuration.setting.invalid_type.message=Setting {1} of configuration class {0} is of type {2} but the value {3} can be converted into this type +configuration.setting.not_found.title=Setting not found +configuration.setting.not_found.message=Setting {1} of type {2} not found in configuration class {0} +configuration.settings.setting.dialog.value.label=Value +configuration.setting.failed_to_update.title=Failed to update setting +configuration.setting.failed_to_update.message=Failed to update setting {1} of configuration {0}. +configuration.settings.setting.reset.title=Reset setting {0} +configuration.settings.setting.reset.close=Cancel +configuration.settings.setting.reset.confirm=Are you sure to set reset setting {1} of configuration {0} to its default value {2}? +configuration.settings.setting.reset.submit=Reset to default value +categories.not_found.message=No category identified by {0} available +categorysystems.not_found.message=No category system identified by {0} available +categorysystems.label=Category Systems +categorysystems.add=Add Category System +categorysystems.table.headers.domainKey=Domain Key +categorysystems.table.headers.uri=URI +categorysystems.table.headers.version=Version +categorysystems.table.headers.released=Released +categorysystems.new.label=Create new categorysystem +categorysystems.edit.label=Edit Category System {0} +categorysystems.form.domainKey.label=Domain Key +categorysystems.form.domainKey.help=Unique key for category system +categorysystems.form.uri.label=URI +categorysystems.form.uri.help=URI stub of the categorysystem +categorysystems.form.buttons.create=Create new categorysystem +categorysystems.form.buttons.save=Apply changes +categorysystems.form.buttons.cancel=Cancel +categorysystems.not_found.title=Categorysystem not found +categorysystems.details.title=Categorysystem {0} Details +categorysystems.details.id=ID +categorysystems.details.uuid=UUID +categorysystems.details.domainKey=Domain Key +categorysystems.details.uri=URI +categorysystems.details.title.heading=Localized Title +categorysystems.details.title.table.headings.locale=Language +categorysystems.details.title.table.headings.value=Localized Title +categorysystems.details.title.table.headings.actions=Actions +categorysystems.details.title.table.actions.edit=Edit +categorysystems.details.title.table.actions.remove=Remove +categorysystems.details.title.add=Add title localization +categorysystems.details.title.none=No localized titles +categorysystems.details.description.heading=Localized Description +categorysystems.details.description.table.headings.locale=Language +categorysystems.details.description.table.headings.value=Localized Description +categorysystems.details.description.table.headings.actions=Actions +categorysystems.details.description.table.actions.edit=Edit +categorysystems.details.description.table.actions.remove=Remove +categorysystems.details.description.add=Add localized description +categorysystems.details.description.none=No localized descriptions +categorysystems.details.owners.add=Add application mapping +categorysystems.details.owners.applicationname=Application +categorysystems.details.owners.actions=Actions +categorysystems.details.owners.remove=Remove +categorysystems.details.owners.context=Context +categorysystems.details.owners.heading=Used by +categorysystems.details.owners.none=Not in use by any application +categorysystems.details.edit=Edit +categorysystems.form.version.label=Version +categorysystems.form.version.help=Version of the category system +categorysystems.form.relased.help=Date of release +categorysystems.form.released.label=Released +categorysystems.details.version=Version +categorysystems.details.released=Released +categorysystems.form.errors.uri_invalid=URI is invalid +categorysystems.table.headers.actions=Actions +categorysystems.table.actions.delete=Delete +categorysystems.table.actions.edit=Edit +categorysystems.delete.confirm.title=Confirm Delete +categorysystems.delete.confirm.message=Are you sure to delete the category system {0} and all its categories? +categorysystems.delete.confirm.cancel=Cancel +categorysystems.delete.confirm.yes=Delete Category System +categorysystems.details.title.add.dialog.title=Add localized title +categorysystems.details.title.add.dialog.close=Cancel +categorysystems.details.title.add.dialog.submit=Add title +categorysystems.details.title.add.dialog.value.label=Localized title +categorysystems.details.title.add.dialog.value.help=The localized title to add +categorysystems.details.title.add.dialog.locale.label=Locale +categorysystems.details.title.add.dialog.locale.help=The locale (language) of the title value to add +categorysystems.details.title.table.actions.edit.dialog.title=Edit title +categorysystems.details.title.edit.dialog.close=Cancel +categorysystems.details.title.edit.dialog.value.label=Localized Title +categorysystems.details.title.edit.dialog.value.help=The new value for the title +categorysystems.details.title.edit.dialog.submit=Save +categorysystems.details.title.table.actions.remove.dialog.title=Remove localized title +categorysystems.details.title.remove.dialog.close=Cancel +categorysystems.details.title.remove.dialog.message=Are your sure to remove the following localized value for the title: +categorysystems.details.title.remove.dialog.submit=Delete title +categorysystems.details.description.add.dialog.title=Add localized description +categorysystems.details.description.add.dialog.locale.label=Locale +categorysystems.details.description.add.dialog.locale.help=The locale (language) of the description value to add +categorysystems.details.description.add.dialog.value.label=Localized description +categorysystems.details.description.add.dialog.value.help=The localized description to add +categorysystems.details.description.add.dialog.close=Cancel +categorysystems.details.description.add.dialog.submit=Add description +categorysystems.details.description.table.actions.edit.dialog.title=Edit description +categorysystems.details.description.edit.dialog.close=Cancel +categorysystems.details.description.edit.dialog.value.label=Localized description +categorysystems.details.description.edit.dialog.value.help=The localized description +categorysystems.details.description.edit.dialog.submit=Save +categorysystems.details.description.table.actions.remove.dialog.title=Removed localized description +categorysystems.details.description.remove.dialog.close=Cancel +categorysystems.details.description.remove.dialog.message=Are your sure to delete the following localized value for the description: +categorysystems.details.description.remove.dialog.submit=Remove localized description +categorysstems.details.owner.add.dialog.title=Add Application Mapping +categorysystems.details.owner.add.dialog.application.label=Application +categorysystems.details.owner.add.dialog.application.help=Application for that a new mapping is added +categorysystems.details.owner.add.dialog.close=Cancel +categorysystems.details.owner.add.dialog.submit=Add application mapping +categorysystems.details.owners.remove.title=Remove application mapping +categorysystems.details.owners.remove.close=Cancel +categorysystems.details.owners.remove.message=Are your sure to remove the mapping for application {0}? +categorysystems.details.owners.remove.submit=Remove mapping +categorysystems.details.owner.add.dialog.context.label=Context +categorysystems.details.owner.add.dialog.context.help=Context for the mapping +categorysystems.details.categories.heading=Categories +categorysystems.details.categories.add=Add category +categorysystems.details.categories.none=No categories +categorysystems.details.categories.table.headings.name=Name +categorysystems.details.categories.table.headings.enabled=Enabled? +categorysystems.details.categories.table.headings.visible=Visible? +categorysystems.details.categories.table.headings.abstract=Abstract category? +categorysystems.details.categories.table.headings.enabled.true=Yes +categorysystems.details.categories.table.headings.enabled.false=No +categorysystems.details.categories.table.headings.visible.true=Yes +categorysystems.details.categories.table.headings.visible.false=No +categorysystems.details.categories.table.headings.abstract.true=Yes +categorysystems.details.categories.table.headings.abstract.false=No +categorysystems.details.categories.delete=Delete +categories.label=Categories +categories.new.label=Create new subcategory of category {0} +categories.edit.label=Edit category {0} +categories.new.breadcrumb=Create new category +categories.edit.breadcrumb=Edit category {0} +categories.form.uniqueId.help=Unique ID of the new category. Should be unique for the complete category system +categories.form.uniqueId.label=Unique ID +categories.form.name.help=Name of the category. May only contain the letters, numbers and hypens. +categories.form.name.label=Name +categories.form.enabled.label=Enabled? +categories.form.visible.label=Visible? +categories.form.abstractCategory.label=Abstract Category? +categories.form.buttons.create=Create new category +categories.form.buttons.save=Save +categories.form.buttons.cancel=Cancel +categories.not_found.title=Category not found +categories.details.title=Details of category {0} +categories.details.edit=Edit +categories.details.enabled.yes=Yes +categories.details.enabled.no=No +categories.details.visible.yes=Yes +categories.details.visible.no=No +categories.details.abstract_category.yes=Yes +categories.details.abstract_category.no=No +categories.details.enabled=Enabled? +categories.details.visible=Visible? +categories.details.abstract_category=Abstract Category? +categories.details.subcategories.heading=Subcategories +categories.details.subcategories.add=Add new subcategory +categories.details.subcategories.none=This category has no subcategories +categories.details.subcategories.table.headings.name=Name +categories.details.subcategories.table.headings.enabled=Enabled? +categories.details.subcategories.table.headings.visible=Visible? +categories.details.subcategories.table.headings.abstract=Abstract? +categories.details.subcategories.delete=Delete +categories.details.subcategories.delete.cancel=Cancel +categories.details.subcategories.delete.confirm=Delete category +categories.details.subcategories.delete.title=Delete category? +categories.details.subcategories.delete.message=Are you sure to delete category {0}? +categories.details.path=Path +categories.details.id=ID +categories.details.uuid=UUID +categories.details.uniqueId=Unique ID +categories.details.name=Name +categories.details.title.heading=Localized Title +categories.details.title.add=Add localized title +categories.details.title.none=No localized title available +categories.details.description.heading=Localized description +categories.details.description.add=Add localized description +categories.details.description.none=No localized description available +categories.details.title.add.dialog.close=Cancel +categories.details.title.add.dialog.locale.help=Select the locale of the localized title +categories.details.title.add.dialog.locale.label=Locale +categories.details.title.add.dialog.submit=Add +categories.details.title.add.dialog.title=Add localized title +categories.details.title.add.dialog.value.help=The localized title +categories.details.title.add.dialog.value.label=Localized title +categories.details.title.table.actions.edit=Edit +categories.details.title.edit.dialog.close=Cancel +categories.details.title.edit.dialog.submit=Save +categories.details.title.table.actions.edit.dialog.title=Edit localized title +categories.details.title.edit.dialog.value.help=The localized title +categories.details.title.edit.dialog.value.label=Localized title +categories.details.title.table.actions.remove=Remove +categories.details.title.remove.dialog.close=Cancel +categories.details.title.remove.dialog.submit=Remove +categories.details.title.remove.dialog.message=Are you sure to remove the localized title for the following locale: +categories.details.title.table.actions.remove.dialog.title=Remove localized title? +categories.details.title.table.headings.actions=Actions +categories.details.title.table.headings.locale=Locale +categories.details.title.table.headings.value=Value +categories.details.description.add.dialog.close=Cancel +categories.details.description.add.dialog.locale.help=The locale of the localized description +categories.details.description.add.dialog.locale.label=Localized description +categories.details.description.add.dialog.submit=Add +categories.details.description.add.dialog.title=Add localized description +categories.details.description.add.dialog.value.help=The localized description +categories.details.description.add.dialog.value.label=Localized description +categories.details.description.table.actions.edit=Edit +categories.details.description.edit.dialog.close=Cancel +categories.details.description.edit.dialog.submit=Save +categories.details.description.table.actions.edit.dialog.title=Edit localized description +categories.details.description.edit.dialog.value.help=The localized description +categories.details.description.edit.dialog.value.label=Localized description +categories.details.description.table.actions.remove=Remove +categories.details.description.remove.dialog.close=Cancel +categories.details.description.remove.dialog.submit=Remove +categories.details.description.remove.dialog.message=Are you sure to remove the localized description for the following locale: +categories.details.description.table.actions.remove.dialog.title=Remove localized description? +categories.details.description.table.headings.actions=Actions +categories.details.description.table.headings.locale=Locale +categories.details.description.table.headings.value=Value +categorysystems.details.categories.table.headings.actions=Actions +categories.invalid_direction.message=Invalid direction {0}. Valid direction are: INCREASE, DECREASE +categories.details.subcategories.table.headings.actions=Actions +categories.details.subcategories.reorder.decrease=Move up +categories.details.subcategories.reorder.increase=Move down +sites.table.heading.domain=Domain +sites.table.heading.defaultSite=Default site? +sites.table.heading.defaultTheme=Default Theme +sites.table.heading.actions=Actions +sites.table.defaultSite.yes=Yes +sites.table.defaultSite.no=No +sites.table.edit=Edit +sites.table.delete=Delete +sites.delete_dialog.cancel=Cancel +sites.delete_dialog.confirm=Delete site +sites.delete_dialog.title=Confirm to delete site +sites.delete_dialog.message=Are you sure to delete the site for the domain {0}? +sites.add_site=Add Site +site.not_found.title=Site not found +sites.not_found_message=No site for identifier {0} gefunden. +site.details.title=Site {0} +sites.details.domain=Domain +sites.details.defaultSite=Default Site? +sites.details.defaultSite.yes=Yes +sites.details.defaultSite.no=No +sites.details.defaultTheme=Default Theme +site.details.edit=Edit +sites.site.edit.label=Edit +sites.breadcrumbs.create=Create new site +sites.breadcrumbs.edit=Edit +sites.create.label=Create new Site +sites.edit.label=Edit Site {0} +sites.form.domain.help=The domain of the site +sites.form.domain.label=Domain +sites.form.defaultsite.label=Is default site? +sites.form.defaulttheme.help=Default theme of the site +sites.form.defaulttheme.label=Default Theme +sites.form.buttons.cancel=Cancel +sites.form.buttons.create=Create Site +sites.form.buttons.save=Save +applications.types.singleton=Singleton +applications.number_of_instances={0} instances +applications.number_of_instances_one={0} instance +themes.label=Themes +themes.description=Manage themes +themes.table.headers.name=Name +themes.table.headers.title=Title +themes.table.headers.version=Version +themes.table.headers.type=Type +themes.table.headers.provider=Provided by +themes.table.actions.republish=Republish +themes.table.actions.publish=Publish +themes.table.actions.unpublish=Unpublish +themes.table.actions.delete=Delete +themes.versions.live=Live +themes.versions.draft=Draft +themes.table.headers.actions=Actions +themes.dialog.description.title=Description of theme {0} +themes.dialog.description.close=Close +themes.table.description.show=Show description of theme {0} +themes.create_new_theme=Create new theme +themes.dialog.new_theme.title=Create new theme +themes.dialog.new_theme.close=Cancel +themes.dialog.new_theme.create=Create new theme +themes.dialog.new_theme.name.help=Unique name of the new theme +themes.dialog.new_theme.name.label=Name +themes.dialog.new_theme.provider.help=The provider which manages the theme. +themes.dialog.new_theme.provider.label=Provider +themes.table.actions.unpublish.cancel=Cancel +themes.table.actions.unpublish.confirm=Unpublish theme +themes.table.actions.unpublish.title=Confirm to unpublish theme +themes.table.actions.unpublish.message=Are your sure to unpublish theme {0}? +themes.table.actions.delete.cancel=Cancel +themes.table.actions.delete.confirm=Delete theme +themes.table.actions.delete.title=Confirm theme deletion +themes.table.actions.delete.message=Are you sure to delete the theme {0}? +imexport.export.label=Export +import.label=Import +import.description=Import entities from import archives +export.label=Export +export.description=Export entities +imexport.export.help=Export the following entities +imexport.export.export_name.help=Name of the export archive +imexport.export.export_name.label=to +imexport.export.submit=Export +imexport.export.cancel=Cancel +imexport.export.exportentities.help=Select the entity types to export. Additional required types will be selected automatically +imexport.export.exportentities.label=Export types +imexport.activeexports.heading=Active Exports +imexport.activeexports.table.columns.name.heading=Name +imexport.activeexports.table.columns.started.heading=Started +imexport.activeexports.table.columns.status.heading=Status +imexport.activeexports.table.columns.actions.heading=Actions +imexport.activeexports.table.columns.status.finished=Finished +imexport.activeexports.table.columns.status.running=In progress +imexport.activeexports.table.columns.actions.button_label=Cancel +imexport.activeexports.none=No active exports +imexport.activeimports.heading=Active Imports +imexport.activeimports.none=No active imports +imexport.activeimports.table.columns.name.heading=Name +imexport.activeimports.table.columns.started.heading=Started +imexport.activeimports.table.columns.status.heading=Status +imexport.activeimports.table.columns.actions.heading=Actions +imexport.activeimports.table.columns.status.finished=Finished +imexport.activeimports.table.columns.status.running=In progress +imexport.activeimports.table.columns.actions.button_label=Cancel +imexport.import.label=Import +imexport.import.help=Select one of the available import archives +imexport.import.archives.help=Select the archive to import +imexport.import.archives.label=Available import archives +imexport.import.cancel=Cancel +imexport.import.submit=Start Import diff --git a/ccm-core/src/main/resources/org/libreccm/ui/AdminBundle_de.properties b/ccm-core/src/main/resources/org/libreccm/ui/AdminBundle_de.properties new file mode 100644 index 000000000..c8e8a1711 --- /dev/null +++ b/ccm-core/src/main/resources/org/libreccm/ui/AdminBundle_de.properties @@ -0,0 +1,564 @@ +systeminformation.description=Zeigt Informationen \u00fcber LibreCCM und die Umgebung +systeminformation.label=System Informationen +applications.label=Anwendungen +applications.description=Verwalten der Anwendungsinstanzen +imexport.label=Import/Export +categorymanager.label=Kategorienmanager +categories.description=Verwaltung der Kategorien +configuration.label=Konfiguration +configuration.description=Bearbeiten der Konfiguration +dashboard.label=Dashboard +dashboard.description=Provides access to all applications +imexport.description=Daten importieren und exportieren +sites.label=Sites +sites.description=Sites verwalten +usersgroupsroles.label=Benutzer*innen/Gruppen/Rollen +usersgroupsroles.description=Verwaltungen von Benutzer*innen, Gruppen und Rollen +systeminformation.tabs.libreccm.label=LibreCCM System Informationen +systeminformation.tabs.java.label=Java System Properties +breadcrumbs.start=LibreCCM Admin +usersgroupsroles.users.label=Benutzer*innen +usersgroupsroles.groups.label=Gruppen +usersgroupsroles.roles.label=Rollen +usersgroupsroles.overview.label=\u00dcberblick +usersgroupsroles.active_users_count.label=aktive Benutzer*innen +usersgroupsroles.disabled_users_count.label=inaktive Benutzer*innen +usersgroupsroles.groups_count.label=Gruppen +usersgroupsroles.roles_count.label=Rollen +usersgroupsroles.users.table.headers.username=Benutzername +usersgroupsroles.users.table.headers.givenname=Vorname +usersgroupsroles.users.table.headers.familyname=Familienname +usersgroupsroles.users.table.headers.email=E-Mail +usersgroupsroles.users.table.headers.disabled=Inaktiv? +usersgroupsroles.users.table.headers.actions=Aktionen +usersgroupsroles.users.table.headers.disabled.true=Ja +usersgroupsroles.users.table.headers.disabled.false=Nein +usersgroupsroles.users.detailslink.label=Details +usersgroupsroles.users.disablebutton.label=Sperren +usersgroupsroles.users.add=Benutzer*in hinzuf\u00fcgen +usersgroupsroles.users.filter.label=Benutzer*innen filtern +usersgroupsroles.users.filter.submit=Anwenden +usersgroupsroles.users.not_found.title=Benutzer*in nicht gefunden +usersgroupsroles.users.user_details.title=Details Benutzer*in {0} +usersgroupsroles.users.user_details.id=Benutzer*in ID +usersgroupsroles.users.user_details.uuid=UUID +usersgroupsroles.users.user_details.name=Name +usersgroupsroles.users.user_details.given_name=Vorname +usersgroupsroles.users.user_details.familyname=Familienname +usersgroupsroles.users.user_details.primary_email_address=Prim\u00e4re E-Mail-Adresse +usersgroupsroles.users.user_details.disabled=Inaktiv? +usersgroupsroles.users.user_details.disabled.yes=Ja +usersgroupsroles.users.user_details.disabled.no=Nein +usersgroupsroles.users.user_details.password_reset_required=Neues Password erforderlich? +usersgroupsroles.users.user_details.password_reset_required.yes=Ja +usersgroupsroles.users.user_details.password_reset_required.no=Nein +usersgroupsroles.users.user_details.additional_email_addresses.heading=Weitere E-Mail-Adressen +'usersgroupsroles.users.user_details.additional_email_addresses.cols.address=Adresse +usersgroupsroles.users.user_details.additional_email_addresses.cols.boucing=Wird zur\u00fcckgewiesen? +usersgroupsroles.users.user_details.additional_email_addresses.cols.verified=Verifizizert +usersgroupsroles.users.user_details.additional_email_addresses.cols.actions=Aktionen +usersgroupsroles.users.user_details.email_address.bouncing.yes=Ja +usersgroupsroles.users.user_details.email_address.bouncing.no=Nein +usersgroupsroles.users.user_details.email_address.verified.yes=Ja +usersgroupsroles.users.user_details.email_address.verified.no=Nein +usersgroupsroles.users.user_details.email_addresses.remove=Entfernen +usersgroupsroles.users.user_details.email_addresses.edit=Bearbeiten +usersgroupsroles.users.user_details.groups.heading=Gruppenmitgliedschaften +usersgroupsroles.users.user_details.groups.add=Hinzuf\u00fcgen +usersgroupsroles.users.user_details.groups.remove=Entfernen +usersgroupsroles.users.user_details.roles.heading=Rollen +usersgroupsroles.users.user_details.roles.add=Hinzuf\u00fcgen +usersgroupsroles.users.user_details.roles.remove=Entfernen +usersgroupsroles.users.user_details.edit_user=Bearbeiten +usersgroupsroles.users.new.errors.password.empty=Das Passwort eines neuen Benutzers darf nicht leer sein. +usersgroupsroles.users.new.errors.password.no_match=Password und Best\u00e4tigung stimmen nicht \u00fcberein. +usersgroupsroles.users.edit.title=Benutzer*in {0} bearbeiten +usersgroupsroles.users.form.username.label=Login-Name +usersgroupsroles.users.form.username.help=Eindeutiger Name f\u00fcr den/die Benutzer*in +usersgroupsroles.users.form.givenname.label=Vorname +usersgroupsroles.users.form.givenname.help=Vorname des/der Benutzer*in +usersgroupsroles.users.form.familyname.label=Familienname +usersgroupsroles.users.form.familyname.help=Familienname des/der Benutzer*in +usersgroupsroles.users.form.primaryemailaddress.label=Prim\u00e4re E-Mail-Adresse +usersgroupsroles.users.form.primaryemailaddress.help=Prim\u00e4re E-Mail Adresse des/der Benutzer*in +usersgroupsroles.users.form.primaryemailaddress.boucing.label=Werden Mails an die prim\u00e4re E-Mail-Adresse zur\u00fcckgewiesen? +usersgroupsroles.users.form.primaryemailaddress.verified.label=Ist die prim\u00e4re E-Mail-Adresse verifiziert? +usersgroupsroles.users.form.password=Passwort +usersgroupsroles.users.form.password.help=Passwort des neuen Benutzers +usersgroupsroles.users.form.passwordconfirmation=Passwort-Best\u00e4tigung +usersgroupsroles.users.form.passwordconfirmation.help=Wiederholen Sie das Passwort zur Best\u00e4tigung +usersgroupsroles.users.form.banned.label=Inaktiv? +usersgroupsroles.users.form.passwordresetrequired.label=Neues Passwort erforderlich? +usersgroupsroles.users.form.buttons.cancel=Abbrechen +usersgroupsroles.users.form.buttons.create=Benutzer*in neu anlegen +usersgroupsroles.users.form.buttons.save=Speichern +usersgroupsroles.users.create.title=Neue(n) Benutzer*in anlegen +usersgroupsroles.users.email.not_found.title=E-Mail Adresse nicht gefunden +usersgroupsroles.users.email.not_found.message=Benutzer*in {0} hat keine E-Mail-Adresse mit der ID {1}. +usersgroupsroles.users.breadcrumb.new=Benutzer*in anlegen +usersgroupsroles.users.breadcrumb.edit=Benutzer*in bearbeiten +usersgroupsroles.users.breadcrumbs.email.add=E-Mail-Adresse hinzuf\u00fcgen +usersgroupsroles.users.breadcrumbs.email.edit=E-Mail-Adresse bearbeiten +usersgroupsroles.users.email.edit.title=E-Mail Adresse bearbeiten +usersgroupsroles.users.email.add.title=E-Mail Adresse hinzuf\u00fcgen +usersgroupsroles.users.email.form.address.label=E-Mail-Adresse +usersgroupsroles.users.email.form.address.help=Die E-Mail-Adresse +usersgroupsroles.users.email.form.bouncing.label=Wird zur\u00fcckgewiesen? +usersgroupsroles.users.email.form.verified.label=Verifiziert +usersgroupsroles.users.email.form.buttons.add=Hinzuf\u00fcgen +usersgroupsroles.users.email.form.buttons.save=Speichern +usersgroupsroles.users.disable.confirm.title=Deaktivierung Benutzer*in best\u00e4tigen +usersgroupsroles.users.disable.confirm.cancel=Abbrechen +usersgroupsroles.users.disable.confirm.message=Sind Sie sicher das Sie den/die Benutzer*in {0} deaktivieren wollen? +usersgroupsroles.users.disable.confirm.yes=Benutzer*in deaktivieren +usersgroupsroles.users.user_details.email_addresses.remove.confirm.title=Entfernen E-Mail-Adresse best\u00e4tigen +usersgroupsroles.users.user_details.email_addresses.remove.confirm.cancel=Abbrechen +usersgroupsroles.users.user_details.email_addresses.remove.confirm.message=Sind Sie sicher, dass Sie die E-Mail-Adresse {0} entfernen wollen? +usersgroupsroles.users.user_details.email_addresses.remove.confirm.yes=E-Mail-Adresse entfernen +usersgroupsroles.users.user_details.groups.none=Diese(r) Benutzer*in ist nicht Mitglied einer Gruppe +usersgroupsroles.users.user_details.groups.edit=Bearbeiten +usersgroupsroles.users.user_details.roles.edit=Bearbeiten +usersgroupsroles.users.user_details.roles.none=Dieser(m) Benutzer*in sind keine Rollen zugeordnet +usersgroupsroles.users.user_details.email_addresses.none=Diese(r) Benutzer*in hat keine weiteren E-Mail-Adressen +usersgroupsroles.users.user_details.additional_email_addresses.add=E-Mail-Adresse hinzuf\u00fcgen +usersgroupsroles.users.user_details.groups.dialog.title=Gruppenmitgliedschaften bearbeiten +usersgroupsroles.users.user_details.groups.dialog.close=Abbrechen +usersgroupsroles.users.user_details.groups.dialog.save=Anwenden +usersgroupsroles.users.user_details.roles.dialog.title=Rollenmitgliedschaften bearbeiten +usersgroupsroles.users.user_details.roles.dialog.close=Abbrechen +usersgroupsroles.users.user_details.groups.dialog.save=Anwenden +usersgroupsroles.users.user_details.family_name=Familienname +usersgroupsroles.users.user_details.email_addresses.errors.address_empty=Die Addresse kann nicht leer sein +usersgroupsroles.groups.table.headers.groupname=Gruppe +usersgroupsroles.groups.table.headers.actions=Aktionen +usersgroupsroles.groups.detailslink.label=Details +usersgroupsroles.groups.delete.button.label=L\u00f6schen +usersgroupsroles.groups.delete.confirm.title=Gruppe l\u00f6schen +usersgroupsroles.groups.delete.confirm.cancel=Abbrechen +usersgroupsroles.groups.delete.confirm.yes=Gruppe l\u00f6schen +usersgroupsroles.groups.delete.confirm.message=Sind Sie sicher, dass Sie die Gruppe {0} l\u00f6schen wollen? +usersgroupsroles.groups.add=Gruppe hinzuf\u00fcgen +usersgroupsroles.groups.create.title=Neue Gruppe anlegen +usersgroupsroles.groups.edit.title=Gruppe {0} bearbeiten +usersgroupsroles.groups.breadcrumb.new=Neue Gruppe anlegen +usersgroupsroles.groups.breadcrumb.edit=Gruppe bearbeiten +usersgroupsroles.groups.form.groupname.label=Name der Gruppe +usersgroupsroles.groups.form.username.help=Eindeutiger Name der Gruppe +usersgroupsroles.groups.form.buttons.cancel=Abbrechen +usersgroupsroles.groups.form.buttons.create=Neue Gruppe anlegen +usersgroupsroles.groups.form.buttons.save=Speichern +usersgroupsroles.groups.group_details.title=Details Gruppe {0} +usersgroupsroles.groups.group_details.id=ID der Gruppe +usersgroupsroles.groups.group_details.uuid=UUID der Gruppe +usersgroupsroles.groups.group_details.name=Name der Gruppe +usersgroupsroles.groups.group_details.edit_group=Gruppe bearbeiten +usersgroupsroles.groups.group_details.members.heading=Mitglieder +usersgroupsroles.groups.group_details.members.edit=Mitglieder hinzuf\u00fcgen/entfernen +usersgroupsroles.groups.group_details.members.dialog.title=Mitglieder hinzuf\u00fcgen/entfernen +usersgroupsroles.groups.group_details.members.dialog.close=Abbrechen +usersgroupsroles.groups.group_details.members.dialog.save=Anwenden +usersgroupsroles.groups.group_details.members.none=Diese Gruppe hat keine Mitglieder +usersgroupsroles.groups.groups_details.roles.heading=Rollen +usersgroupsroles.groups.group_details.roles.edit=Bearbeiten +usersgroupsroles.groups.group_details.roles.dialog.title=Rollenmitgliedschaften bearbeiten +usersgroupsroles.groups.group_details.roles.dialog.close=Abbrechen +usersgroupsroles.users.user_details.roles.dialog.save=Anwenden +usersgroupsroles.groups.group_details.roles.dialog.save=Anwenden +usersgroupsroles.groups.group_details.roles.none=Dieser Gruppe sind keine Rollen zugeordnet +usersgroupsroles.groups.not_found.title=Gruppe nicht gefunden +usersgroupsroles.groups.group_details.groupId=ID der Gruppe +usersgroupsroles.groups.group_details.groupName=Name der Gruppe +usersgroupsroles.users.user_details.additional_email_addresses.cols.address=Adresse +usersgroupsroles.roles.not_found_message=Rolle {0} nicht verf\u00fcgbar +usersgroupsroles.roles.not_found.title=Rolle nicht gefunden +usersgroupsroles.roles.role_details.title=Details Rolle {0} +usersgroupsroles.roles.role_details.roleId=Rolle ID +usersgroupsroles.roles.role_details.name=Name +usersgroupsroles.roles.role_details.edit_role=Rolle bearbeiten +usersgroupsroles.roles.role_details.members.heading=Mitglieder +usersgroupsroles.roles.role_details.members.edit=Mitglieder hinzuf\u00fcgen/entfernen +usersgroupsroles.roles.role_details.members.dialog.title=Mitglieder hinzuf\u00fcgen/entfernen +usersgroupsroles.roles.role_details.members.dialog.close=Abbrechen +usersgroupsroles.roles.role_details.members.dialog.save=Anwenden +usersgroupsroles.roles.role_details.members.none=Diese Rolle hat keine Mitglieder +usersgroupsroles.roles.create.title=Neue Rolle anlegen +usersgroupsroles.roles.edit.title=Rolle {0} bearbeiten +usersgroupsroles.roles.breadcrumb.new=Neue Rolle +usersgroupsroles.roles.breadcrumb.edit=Rolle bearbeiten +usersgroupsroles.roles.form.rolename.label=Name +usersgroupsroles.roles.form.rolename.help=Eindeutiger Name der Rolle +usersgroupsroles.roles.form.buttons.create=Rolle anlegen +usersgroupsroles.roles.form.buttons.save=Speichern +usersgroupsroles.roles.add=Rolle hinzuf\u00fcgen +usersgroupsroles.roles.table.headers.rolename=Rolle +usersgroupsroles.roles.table.headers.actions=Aktionen +usersgroupsroles.roles.detailslink.label=Details +usersgroupsroles.roles.delete.button.label=L\u00f6schen +usersgroupsroles.roles.delete.confirm.title=Sind Sie sicher, dass Sie die Rolle l\u00f6schen wollen? +usersgroupsroles.roles.delete.confirm.message=Sind Sie sicher, dass Sie die Rolle {0} l\u00f6schen wollen? +usersgroupsroles.roles.delete.confirm.cancel=Abbrechen +usersgroupsroles.roles.delete.confirm.yes=Rolle l\u00f6schen +usersgroupsroles.roles.role_details.uuid=UUID +usersgroupsroles.groups.form.errors.name_not_empty=Der Name einer Gruppe darf nicht leer sein +usersgroupsroles.groups.form.errors.name_invalid=Der Name einer Gruppe darf nur die Zeichen a bis z, A bis Z, 0 bis 9, den Bindestrich (-) und den Unterstrich (_) enthalten +usersgroupsroles.roles.form.errors.name_not_empty=Der Name einer Rolle darf nicht leer sein +usersgroupsroles.users.form.errors.username.empty=Der Benutzername eines Benutzers darf nicht leer sein +usersgroupsroles.users.form.errors.username.invalid=Der Benutzername eines Benutzers darf nur die Zeichen a bis z, A bis Z, 0 bis 9, den Bindestrich (-) und den Unterstrich (_) enthalten +usersgroupsroles.users.form.errors.primary_email.empty=Die prim\u00e4re E-Mail-Addresse eines Benutzers darf nicht leer sein. +usersgroupsroles.users.form.errors.primary_email.invalid=Die prim\u00e4re E-Mail-Addresse eines Benutzers muss eine valide E-Mail-Addresse sein +configuration.table.headings.title=Konfiguration +configuration.table.headings.description=Beschreibung +configuration.settings.label=Einstellungen +configuration.settings.table.headings.label=Einstellung +configuration.settings.table.headings.value=Aktueller Wert +configuration.settings.table.headings.defaultValue=Standardwert +configuration.settings.table.headings.actions=Aktionen +configuration.settings.table.actions.info=Info +configuration.settings.table.actions.info.help=Beschreibung f\u00fcr Einstellung {0} anzeigen +configuration.settings.table.actions.edit=Bearbeiten +configuration.settings.table.actions.reset.help=Einstellung {0} auf Standardwert {1} zur\u00fccksetzen +configuration.settings.table.actions.reset=Zur\u00fccksetzen +configuration.settings.setting.info.label=Setting {0} Info +configuration.settings.setting.info.close=Schlie\u00dfen +configuration.settings.setting.info.dismiss=OK +configuration.settings.setting.dialog.title=Einstellung {0} bearbeiten +configuration.settings.setting.dialog.close=Abbrechen +configuration.settings.setting.dialog.save=Speichern +configuration.settings.setting.dialog.unsupported_type=Leider werden Einstellunge vom Typ "{0}" noch nicht unterst\u00fctzt. +configuration.class.not_found.title=Konfiguration nicht gefunden +configuration.class.not_found.message=Konfiguration {0} nicht gefunden +configuration.setting.type_unsupported.title=Typ der Einstellung nicht unterst\u00fctzt +configuration.setting.type_unsupported.message=Einstellung {1} aus Konfiguration {0} ist vom Typ {3}, dieser wird aber nicht unterst\u00fctzt +configuration.setting.invalid_type.title=Invalid type of value +configuration.setting.invalid_type.message=Einstellung {1} der Konfigurations-Klasse {0} ist vom Typ {2}, der angegebene Wert {3} kann aber nicht in diesen Typ konvertiert werden +configuration.setting.not_found.title=Einstellung nicht gefunden +configuration.setting.not_found.message=Keine Einstellung {1} vom Typ {2} in Konfiguration {0} gefunden +configuration.settings.setting.dialog.value.label=Wert +configuration.setting.failed_to_update.title=Aktualisieren der Einstellung fehlgeschlagen +configuration.setting.failed_to_update.message=Aktualisieren der Einstellung {1} in Konfiguration {0} fehlgeschlagen. +configuration.settings.setting.reset.title=Einstellung {0} zur\u00fccksetzen +configuration.settings.setting.reset.close=Abbrechen +configuration.settings.setting.reset.confirm=Sind Sie sicher, dass die die Einstellung {1} in Konfiguration {0} auf ihren Standardwert {2} zur\u00fccksetzen wollen? +configuration.settings.setting.reset.submit=Auf Standardwert zur\u00fccksetzen +categories.not_found.message=Keine Kategorie f\u00fcr den Identifier {0} gefunden +categorysystems.not_found.message=Es wurde kein Kategoriensystem f\u00fcr den Identifier {0} gefunden +categorysystems.label=Kategoriensysteme +categorysystems.add=Neues Kategoriensystem erstellen +categorysystems.table.headers.domainKey=Domain Key +categorysystems.table.headers.uri=URI +categorysystems.table.headers.version=Version +categorysystems.table.headers.released=Freigegeben +categorysystems.new.label=Neues Kategoriensystem erstellen +categorysystems.edit.label=Kategoriensystem {0} bearbeiten +categorysystems.form.domainKey.label=Domain Key +categorysystems.form.domainKey.help=Unique key for category system +categorysystems.form.uri.label=URI +categorysystems.form.uri.help=URI stub of the categorysystem +categorysystems.form.buttons.create=Neues Kategoriensystem anlegen +categorysystems.form.buttons.save=\u00c4nderungen anwenden +categorysystems.form.buttons.cancel=Abbrechen +categorysystems.not_found.title=Kategoriensystem nicht gefunden +categorysystems.details.title=Kategoriensystem {0} Details +categorysystems.details.id=ID +categorysystems.details.uuid=UUID +categorysystems.details.domainKey=Domain Key +categorysystems.details.uri=URI +categorysystems.details.title.heading=Lokalisierter Titel +categorysystems.details.title.table.headings.locale=Sprache +categorysystems.details.title.table.headings.value=Lokalisierter Titel +categorysystems.details.title.table.headings.actions=Aktionen +categorysystems.details.title.table.actions.edit=Bearbeiten +categorysystems.details.title.table.actions.remove=Entfernen +categorysystems.details.title.add=\u00dcbersetzung hinzuf\u00fcgen +categorysystems.details.title.none=Keine lokalisierten Titel +categorysystems.details.description.heading=Lokalisierte Beschreibung +categorysystems.details.description.table.headings.locale=Sprache +categorysystems.details.description.table.headings.value=Lokalisierte Beschreibung +categorysystems.details.description.table.headings.actions=Aktionen +categorysystems.details.description.table.actions.edit=Bearbeiten +categorysystems.details.description.table.actions.remove=Entfernen +categorysystems.details.description.add=\u00dcbersetzung hinzuf\u00fcgen +categorysystems.details.description.none=Keine lokalisierten Beschreibungen +categorysystems.details.owners.add=Anwendung hinzuf\u00fcgen +categorysystems.details.owners.applicationname=Anwendung +categorysystems.details.owners.actions=Aktionen +categorysystems.details.owners.remove=Entfernen +categorysystems.details.owners.context=Context +categorysystems.details.owners.heading=Verwendet von +categorysystems.details.owners.none=Wird von keiner Anwendung verwendet +categorysystems.details.edit=Bearbeiten +categorysystems.form.version.label=Version +categorysystems.form.version.help=Version des Kategoriensystems +categorysystems.form.relased.help=Datum der Freigabe +categorysystems.form.released.label=Released +categorysystems.details.version=Version +categorysystems.details.released=Freigegeben +categorysystems.form.errors.uri_invalid=URI ist nicht valide +categorysystems.table.headers.actions=Aktionen +categorysystems.table.actions.delete=L\u00f6schen +categorysystems.table.actions.edit=Bearbeiten +categorysystems.delete.confirm.title=L\u00f6schen best\u00e4tigen +categorysystems.delete.confirm.message=Sind Sie sicher, dass Sie das Kategoriensystem {0} mit allen seinen Kategorien l\u00f6schen wollen? +categorysystems.delete.confirm.cancel=Abbrechen +categorysystems.delete.confirm.yes=Kategoriensystem l\u00f6schen +categorysystems.details.title.add.dialog.title=Lokalisierten Titel hinzuf\u00fcgen +categorysystems.details.title.add.dialog.close=Abbrechen +categorysystems.details.title.add.dialog.submit=Titel hinzuf\u00fcgen +categorysystems.details.title.add.dialog.value.label=Lokalisierter Titel +categorysystems.details.title.add.dialog.value.help=Der hinzuzuf\u00fcgendende Titel +categorysystems.details.title.add.dialog.locale.label=Sprache +categorysystems.details.title.add.dialog.locale.help=Die Sprache des Titels +categorysystems.details.title.table.actions.edit.dialog.title=Titel bearbeiten +categorysystems.details.title.edit.dialog.close=Abbrechen +categorysystems.details.title.edit.dialog.value.label=Lokalisierter Titel +categorysystems.details.title.edit.dialog.value.help=Neuer Wert f\u00fcr den Titel +categorysystems.details.title.edit.dialog.submit=Speichern +categorysystems.details.title.table.actions.remove.dialog.title=Lokalisierten Titel entfernen +categorysystems.details.title.remove.dialog.close=Abbrechen +categorysystems.details.title.remove.dialog.message=Sind Sie sicher, dass die folgende Lokalisierung f\u00fcr den Titel entfernen wollen? +categorysystems.details.title.remove.dialog.submit=Titel entfernen +categorysystems.details.description.add.dialog.title=Lokalisierte Beschreibung hinzuf\u00fcgen +categorysystems.details.description.add.dialog.locale.label=Sprache +categorysystems.details.description.add.dialog.locale.help=Die Sprache der Beschreibung +categorysystems.details.description.add.dialog.value.label=Lokalisierte Beschreibung +categorysystems.details.description.add.dialog.value.help=Die hinzuzuf\u00fcgende Beschreibung +categorysystems.details.description.add.dialog.close=Abbrechen +categorysystems.details.description.add.dialog.submit=Beschreibung hinzuf\u00fcgen +categorysystems.details.description.table.actions.edit.dialog.title=Beschreibung bearbeiten +categorysystems.details.description.edit.dialog.close=Abbrechen +categorysystems.details.description.edit.dialog.value.label=Lokalisierte Beschreibung +categorysystems.details.description.edit.dialog.value.help=Die lokalisierte Beschreibung +categorysystems.details.description.edit.dialog.submit=Speichern +categorysystems.details.description.table.actions.remove.dialog.title=Lokalisierte Beschreibung entfernen +categorysystems.details.description.remove.dialog.close=Abbrechen +categorysystems.details.description.remove.dialog.message=Sind Sie sicher, dass Sie die folgende Lokalisierung f\u00fcr die Sprache entfernen wollen: +categorysystems.details.description.remove.dialog.submit=Lokalisierte Beschreibung entfernen +categorysstems.details.owner.add.dialog.title=Applikation hinzuf\u00fcgen +categorysystems.details.owner.add.dialog.application.label=Anwendung +categorysystems.details.owner.add.dialog.application.help=Hinzuzuf\u00fcgende Anwendung +categorysystems.details.owner.add.dialog.close=Abbrechen +categorysystems.details.owner.add.dialog.submit=Anwendung hinzuf\u00fcgen +categorysystems.details.owners.remove.title=Anwendung entfernen +categorysystems.details.owners.remove.close=Cancel +categorysystems.details.owners.remove.message=Sind Sie sicher das die Application {0} entfernen wollen? +categorysystems.details.owners.remove.submit=Zuordnung entfernen +categorysystems.details.owner.add.dialog.context.label=Context +categorysystems.details.owner.add.dialog.context.help=Context for the mapping +categorysystems.details.categories.heading=Kategorien +categorysystems.details.categories.add=Kategorie hinzuf\u00fcgen +categorysystems.details.categories.none=Keine Kategorien vorhanden +categorysystems.details.categories.table.headings.name=Name +categorysystems.details.categories.table.headings.enabled=Aktiv? +categorysystems.details.categories.table.headings.visible=Sichtbar? +categorysystems.details.categories.table.headings.abstract=Abstrakte Kategorie? +categorysystems.details.categories.table.headings.enabled.true=Ja +categorysystems.details.categories.table.headings.enabled.false=Nein +categorysystems.details.categories.table.headings.visible.true=Ja +categorysystems.details.categories.table.headings.visible.false=Nein +categorysystems.details.categories.table.headings.abstract.true=Ja +categorysystems.details.categories.table.headings.abstract.false=Nein +categorysystems.details.categories.delete=L\u00f6schen +categories.label=Kategorien +categories.new.label=Neue Unterkategorie f\u00fcr Kategorie {0} erstellen +categories.edit.label=Kategorie {0} bearbeiten +categories.new.breadcrumb=Neue Kategorie anlegen +categories.edit.breadcrumb=Kategorie {0} bearbeiten +categories.form.uniqueId.help=Eindeutige ID der Kategorie. Sollte innerhalb des gesamten Kategoriensystems eindeutig sein. +categories.form.uniqueId.label=Eindeutige ID +categories.form.name.help=Name der Kategorie. Darf nur Buchstaben, Zahlen und den Bindestrich enthalten. +categories.form.name.label=Name +categories.form.enabled.label=Aktiv? +categories.form.visible.label=Sichtbar? +categories.form.abstractCategory.label=Abstrakte Kategorie? +categories.form.buttons.create=Neue Kategorie anlegen +categories.form.buttons.save=Speichern +categories.form.buttons.cancel=Abbrechen +categories.not_found.title=Kategorie nicht gefunden +categories.details.title=Details Kategorie {0} +categories.details.edit=Bearbeiten +categories.details.enabled.yes=Ja +categories.details.enabled.no=Nein +categories.details.visible.yes=Ja +categories.details.visible.no=Nein +categories.details.abstract_category.yes=Ja +categories.details.abstract_category.no=Nein +categories.details.enabled=Aktiv? +categories.details.visible=Sichtbar? +categories.details.abstract_category=Abstrakte Kategorie? +categories.details.subcategories.heading=Subkategorien +categories.details.subcategories.add=Neue Unterkategorie erstellen +categories.details.subcategories.none=Diese Kategorie hat keine Unterkategorien +categories.details.subcategories.table.headings.name=Name +categories.details.subcategories.table.headings.enabled=Aktiv? +categories.details.subcategories.table.headings.visible=Sichtbar? +categories.details.subcategories.table.headings.abstract=Abstrakt? +categories.details.subcategories.delete=L\u00f6schen +categories.details.subcategories.delete.cancel=Abbrechen +categories.details.subcategories.delete.confirm=Kategorie l\u00f6schen +categories.details.subcategories.delete.title=Kategorie l\u00f6schen? +categories.details.subcategories.delete.message=Sind Sie sicher, dass Sie die Kategorie {0} l\u00f6schen wollen? +categories.details.path=Pfad +categories.details.id=ID +categories.details.uuid=UUID +categories.details.uniqueId=Eindeutige ID +categories.details.name=Name +categories.details.title.heading=Lokalisierter Titel +categories.details.title.add=Lokalisierten Titel hinzuf\u00fcgen +categories.details.title.none=Keine lokalisierten Titel vorhanden +categories.details.description.heading=Lokalisierte Beschreibung +categories.details.description.add=Lokalisierte Beschreibung hinzuf\u00fcgen +categories.details.description.none=Keine lokalisierte Beschreibung vorhanden +categories.details.title.add.dialog.close=Abbrechen +categories.details.title.add.dialog.locale.help=W\u00e4hlen Sie die Sprache des lokalisierten Titels +categories.details.title.add.dialog.locale.label=Sprache +categories.details.title.add.dialog.submit=Hinzuf\u00fcgen +categories.details.title.add.dialog.title=Lokalisierten Titel hinzuf\u00fcgen +categories.details.title.add.dialog.value.help=Der lokalisierte Titel +categories.details.title.add.dialog.value.label=Lokalisierter Titel +categories.details.title.table.actions.edit=Bearbeiten +categories.details.title.edit.dialog.close=Abbrechen +categories.details.title.edit.dialog.submit=Speichern +categories.details.title.table.actions.edit.dialog.title=Lokalisierten Titel bearbeiten +categories.details.title.edit.dialog.value.help=Der lokalisierte Titel +categories.details.title.edit.dialog.value.label=Lokalisierter Titel +categories.details.title.table.actions.remove=Entfernen +categories.details.title.remove.dialog.close=Abbrechen +categories.details.title.remove.dialog.submit=Entfernen +categories.details.title.remove.dialog.message=Sind Sie sicher, dass die den lokalisierten Titlel f\u00fcr die folgende Sprache entfernen wollen: +categories.details.title.table.actions.remove.dialog.title=Lokalisierten Titel entfernen? +categories.details.title.table.headings.actions=Aktionen +categories.details.title.table.headings.locale=Sprache +categories.details.title.table.headings.value=Wert +categories.details.description.add.dialog.close=Abbrechen +categories.details.description.add.dialog.locale.help=Die Sprache der lokalisierten Beschreibung +categories.details.description.add.dialog.locale.label=Lokalisierte Beschreibung +categories.details.description.add.dialog.submit=Hinzuf\u00fcgen +categories.details.description.add.dialog.title=Lokalisierte Beschreibung hinzuf\u00fcgen +categories.details.description.add.dialog.value.help=Die lokalisierte Beschreibung +categories.details.description.add.dialog.value.label=Localized description +categories.details.description.table.actions.edit=Bearbeiten +categories.details.description.edit.dialog.close=Abbrechen +categories.details.description.edit.dialog.submit=Speichern +categories.details.description.table.actions.edit.dialog.title=Lokalisierte Beschreibung bearbeiten +categories.details.description.edit.dialog.value.help=Die lokalisierte Beschreibung +categories.details.description.edit.dialog.value.label=Lokalisierte Beschreibung +categories.details.description.table.actions.remove=Entfernen +categories.details.description.remove.dialog.close=Abbrechen +categories.details.description.remove.dialog.submit=Entfernen +categories.details.description.remove.dialog.message=Sind Sie sicher, dass Sie die lokalisierte Beschreibung f\u00fcr die folgende Sprache entfernen wollen: +categories.details.description.table.actions.remove.dialog.title=Lokalisierte Beschreibung entfernen? +categories.details.description.table.headings.actions=Aktionen +categories.details.description.table.headings.locale=Sprache +categories.details.description.table.headings.value=Wert +categorysystems.details.categories.table.headings.actions=Aktionen +categories.invalid_direction.message=Ung\u00fcltiger Wert {0} f\u00fcr Parameter direction. G\u00fcltige Werte: INCREASE, DECREASE +categories.details.subcategories.table.headings.actions=Aktionen +categories.details.subcategories.reorder.decrease=Hoch +categories.details.subcategories.reorder.increase=Runter +sites.table.heading.domain=Domain +sites.table.heading.defaultSite=Default site? +sites.table.heading.defaultTheme=Standard Theme +sites.table.heading.actions=Aktionen +sites.table.defaultSite.yes=Ja +sites.table.defaultSite.no=Nein +sites.table.edit=Bearbeiten +sites.table.delete=L\u00f6schen +sites.delete_dialog.cancel=Abbrechen +sites.delete_dialog.confirm=Site l\u00f6schen +sites.delete_dialog.title=L\u00f6schen der Site best\u00e4tigen +sites.delete_dialog.message=Sind Sie sicher, dass Sie die Site f\u00fcr die Domain {0} l\u00f6schen wollen? +sites.add_site=Site hinzuf\u00fcgen +site.not_found.title=Site nicht verf\u00fcgbar +sites.not_found_message=Keine Site mit Identifier {0} gefunden. +site.details.title=Site {0} +sites.details.domain=Domain +sites.details.defaultSite=Standard Seite? +sites.details.defaultSite.yes=Ja +sites.details.defaultSite.no=Nein +sites.details.defaultTheme=Standard Theme +site.details.edit=Bearbeiten +sites.site.edit.label=Bearbeiten +sites.breadcrumbs.create=Neue Site anlegen +sites.breadcrumbs.edit=Bearbeiten +sites.create.label=Neue Site anlegen +sites.edit.label=Site {0} bearbeiten +sites.form.domain.help=Die Domain der Seite +sites.form.domain.label=Domain +sites.form.defaultsite.label=Ist Standard-Seite? +sites.form.defaulttheme.help=Das Standard-Theme der Seite +sites.form.defaulttheme.label=Standard Theme +sites.form.buttons.cancel=Abbrechen +sites.form.buttons.create=Site anlegen +sites.form.buttons.save=Speichern +applications.types.singleton=Singleton +applications.number_of_instances={0} Instanzen +applications.number_of_instances_one={0} instance +themes.label=Themes +themes.description=Themes verwalten +themes.table.headers.name=Name +themes.table.headers.title=Titel +themes.table.headers.version=Version +themes.table.headers.type=Typ +themes.table.headers.provider=Bereitgestellt durch +themes.table.actions.republish=Republizieren +themes.table.actions.publish=Publizieren +themes.table.actions.unpublish=Depublizieren +themes.table.actions.delete=L\u00f6schen +themes.versions.live=Live +themes.versions.draft=Draft +themes.table.headers.actions=Aktionen +themes.dialog.description.title=Beschreibung Theme {0} +themes.dialog.description.close=Schlie\u00dfen +themes.table.description.show=Beschreibung des Themes {0} anzeigen +themes.create_new_theme=Neues Theme anlegen +themes.dialog.new_theme.title=Neues Theme anlegen +themes.dialog.new_theme.close=Abbrechen +themes.dialog.new_theme.create=Neues Theme anlegen +themes.dialog.new_theme.name.help=Eindeutiger Name des neuen Themes +themes.dialog.new_theme.name.label=Name +themes.dialog.new_theme.provider.help=Der Provider, \u00fcber den das Theme verwaltet wird. +themes.dialog.new_theme.provider.label=Provider +themes.table.actions.unpublish.cancel=Abbrechen +themes.table.actions.unpublish.confirm=Theme depublizieren +themes.table.actions.unpublish.title=Depublizieren des Themes best\u00e4tigen +themes.table.actions.unpublish.message=Sind Sie sicher, dass Sie das Theme {0} depublizieren wollen? +themes.table.actions.delete.cancel=Abbrechen +themes.table.actions.delete.confirm=Theme l\u00f6schen +themes.table.actions.delete.title=L\u00f6schen des Themes best\u00e4tigen +themes.table.actions.delete.message=Sind Sie sicher, dass Sie das Theme {0} l\u00f6schen wollen? +imexport.export.label=Export +import.label=Import +import.description=Entities aus Import-Archiven importieren +export.label=Export +export.description=Entitities exportieren +imexport.export.help=Export the following entities +imexport.export.export_name.help=Name des Export Archives +imexport.export.export_name.label=nach +imexport.export.submit=Exportieren +imexport.export.cancel=Abbrechen +imexport.export.exportentities.help=W\u00e4hlen Sie die zu exportierenden Typen aus. Weitere ben\u00f6tigte Typen werden automatisch ausgew\u00e4hlt. +imexport.export.exportentities.label=Exportiere +imexport.activeexports.heading=Aktive Exporte +imexport.activeexports.table.columns.name.heading=Name +imexport.activeexports.table.columns.started.heading=Gestartet +imexport.activeexports.table.columns.status.heading=Status +imexport.activeexports.table.columns.actions.heading=Aktionen +imexport.activeexports.table.columns.status.finished=Abgeschlossen +imexport.activeexports.table.columns.status.running=In Arbeit +imexport.activeexports.table.columns.actions.button_label=Abbrechen +imexport.activeexports.none=Keine aktiven Exporte +imexport.activeimports.heading=Aktive Importe +imexport.activeimports.none=Keine aktiven Importe +imexport.activeimports.table.columns.name.heading=Name +imexport.activeimports.table.columns.started.heading=Gestartet +imexport.activeimports.table.columns.status.heading=Status +imexport.activeimports.table.columns.actions.heading=Aktionen +imexport.activeimports.table.columns.status.finished=Abgeschlossen +imexport.activeimports.table.columns.status.running=In Arbeit +imexport.activeimports.table.columns.actions.button_label=Abbrechen +imexport.import.label=Import +imexport.import.help=W\u00e4hlen Sie das zu importierende Archiv +imexport.import.archives.help=W\u00e4hlen Sie das zu importierende Archiv +imexport.import.archives.label=Verf\u00fcgbare Import-Archive +imexport.import.cancel=Abbrechen +imexport.import.submit=Import starten diff --git a/ccm-core/src/main/scss/ccm-admin/_custom.scss b/ccm-core/src/main/scss/ccm-admin/_custom.scss new file mode 100644 index 000000000..ba3e0c8f6 --- /dev/null +++ b/ccm-core/src/main/scss/ccm-admin/_custom.scss @@ -0,0 +1,60 @@ + +$grid-breakpoints: ( + xs: 0, + sm: 36rem, + md: 48rem, + lg: 62rem, + xl: 75rem +); + +$container-max-widths: ( + sm: 33.75rem, + md: 45rem, + lg: 60rem, + xl: 71.25rem +); + +$grid-gutter-width: 1.875rem; + +$form-grid-gutter-width: 0.625rem; + +$tooltip-max-width: 12.5rem; + +$popover-maxwidth: 17.25rem; + +$toast-max-width: 21.875rem; + +$modal-xl: 71.25rem; +$modal-lg: 50rem; +$modal-md: 31.25rem; +$modal-sm: 18.75rem; + +$modal-fade-transform: translate(0, -3.125rem); + +$carousel-indicator-width: 1.875rem; +$carousel-control-icon-width: 1.25rem; + +$pre-scrollable-max-height: 21.25rem; + +// Navbar default colors have insufficient contrast +$navbar-dark-color: #fff; + +table.users-table, +table.groups-table, +table.roles-table { + tbody { + td.action-col { + width: 8em; + } + } +} + +table.group-members, +table.group-roles, +table.role-members { + tbody { + td.action-col { + width: 8em; + } + } +} diff --git a/ccm-core/src/main/scss/ccm-admin/ccm-admin.scss b/ccm-core/src/main/scss/ccm-admin/ccm-admin.scss new file mode 100644 index 000000000..4c04ef71d --- /dev/null +++ b/ccm-core/src/main/scss/ccm-admin/ccm-admin.scss @@ -0,0 +1,2 @@ +@import "custom"; +@import "../../../../node_modules/bootstrap/scss/bootstrap"; \ No newline at end of file diff --git a/ccm-core/src/main/typescript/ccm-admin/ccm-admin.ts b/ccm-core/src/main/typescript/ccm-admin/ccm-admin.ts new file mode 100644 index 000000000..7eb9154d6 --- /dev/null +++ b/ccm-core/src/main/typescript/ccm-admin/ccm-admin.ts @@ -0,0 +1,2 @@ +import "bootstrap"; + diff --git a/ccm-core/src/main/typescript/ccm-admin/filterable-list.ts b/ccm-core/src/main/typescript/ccm-admin/filterable-list.ts new file mode 100644 index 000000000..8a583220c --- /dev/null +++ b/ccm-core/src/main/typescript/ccm-admin/filterable-list.ts @@ -0,0 +1,139 @@ +/** + * Not ready yet. Don' use + */ + +export function initFilterables(): void { + document + .querySelectorAll("*[data-filter]") + .forEach(filterable => initFilterable(filterable)); +} + +function buildList( + filterable: Element, + options: Record[], + template: HTMLTemplateElement, + filterInput: HTMLInputElement, + filterBy: string[] +) { + console.log("(Re-)Building list..."); + console.dir(filterOptions); + filterable.innerHTML = ""; + + const filteredOptions = filterInput.value + ? filterOptions(options, filterInput.value, filterBy) + : options; + + for (const option of filteredOptions) { + const item = template.content.cloneNode(true); + replacePlaceholders(item, option); + filterable.appendChild(item); + } +} + +function filterOption( + option: Record, + filter: string, + filterBy: string[] +) { + let result: boolean = false; + + for (const filterByProp of filterBy) { + result = result || option[filterByProp].indexOf(filter) !== -1; + } + return result; +} + +function filterOptions( + options: Record[], + filterValue: string, + filterBy: string[] +) { + return options.filter(option => + filterOption(option, filterValue, filterBy) + ); +} + +function getFilterBy(filterable): string[] { + const filterByValue: string = filterable.getAttribute("data-filter-by"); + if (filterByValue) { + return filterByValue.split(","); + } else { + return []; + } +} + +function getFilterInput(filterable): HTMLInputElement { + const filterInputId: string = filterable.getAttribute("data-filter"); + return document.querySelector(`input${filterInputId}`); +} + +function getOptions(filterable: Element): Record[] { + const attrValue: string = filterable.getAttribute("data-options"); + + if (attrValue.startsWith("#")) { + const dataScript: Element = document.querySelector( + `script${attrValue}` + ); + return JSON.parse(dataScript.textContent); + } else { + return JSON.parse(attrValue); + } +} + +function getTemplate(filterable: Element): HTMLTemplateElement { + const templateId: string = filterable.getAttribute("data-template"); + return document.querySelector(templateId); +} + +function initFilterable(filterable: Element): void { + const options: Record[] = getOptions(filterable); + const template = getTemplate(filterable); + const filterInput = getFilterInput(filterable); + const filterBy = getFilterBy(filterable); + + filterInput.addEventListener("keyup", event => + buildList(filterable, options, template, filterInput, filterBy) + ); + + buildList(filterable, options, template, filterInput, filterBy); +} + +function replacePlaceholders(node: Node, data: Record) { + switch (node.nodeType) { + case Node.ELEMENT_NODE: { + const childNodes = node.childNodes; + for (let i = 0; i < childNodes.length; i++) { + replacePlaceholders(childNodes[i], data); + } + break; + } + case Node.TEXT_NODE: { + for (const key in data) { + console.log(`replacing ${key} with ${data[key]}`); + node.textContent = node.textContent.replace( + `{{${key}}}`, + data[key] + ); + } + break; + } + case Node.CDATA_SECTION_NODE: + return; + case Node.DOCUMENT_NODE: { + const childNodes = node.childNodes; + for (let i = 0; i < childNodes.length; i++) { + replacePlaceholders(childNodes[i], data); + } + break; + } + case Node.DOCUMENT_FRAGMENT_NODE: { + const childNodes = node.childNodes; + for (let i = 0; i < childNodes.length; i++) { + replacePlaceholders(childNodes[i], data); + } + break; + } + default: + return; + } +} diff --git a/ccm-core/src/test/resources-wildfly-h2mem/scripts/002_create_ccm_core_tables.sql b/ccm-core/src/test/resources-wildfly-h2mem/scripts/002_create_ccm_core_tables.sql index ddba5e676..47b6a8669 100644 --- a/ccm-core/src/test/resources-wildfly-h2mem/scripts/002_create_ccm_core_tables.sql +++ b/ccm-core/src/test/resources-wildfly-h2mem/scripts/002_create_ccm_core_tables.sql @@ -54,7 +54,7 @@ create table CCM_CORE.CATEGORY_DOMAINS ( DOMAIN_KEY varchar(255) not null, - RELEASED timestamp, + RELEASED date, URI varchar(1024), VERSION varchar(255), OBJECT_ID bigint not null, diff --git a/ccm-core/src/test/resources-wildfly-pgsql/scripts/002_create_ccm_core_tables.sql b/ccm-core/src/test/resources-wildfly-pgsql/scripts/002_create_ccm_core_tables.sql index a1b40403c..971ee5f8f 100644 --- a/ccm-core/src/test/resources-wildfly-pgsql/scripts/002_create_ccm_core_tables.sql +++ b/ccm-core/src/test/resources-wildfly-pgsql/scripts/002_create_ccm_core_tables.sql @@ -48,7 +48,7 @@ create table CCM_CORE.CATEGORY_DOMAINS ( DOMAIN_KEY varchar(255) not null, - RELEASED timestamp, + RELEASED date, URI varchar(1024), VERSION varchar(255), OBJECT_ID int8 not null, diff --git a/ccm-docrepo/src/main/java/org/libreccm/docrepo/BlobObjectImExporter.java b/ccm-docrepo/src/main/java/org/libreccm/docrepo/BlobObjectImExporter.java index 2c83abcf2..8c2fc4082 100644 --- a/ccm-docrepo/src/main/java/org/libreccm/docrepo/BlobObjectImExporter.java +++ b/ccm-docrepo/src/main/java/org/libreccm/docrepo/BlobObjectImExporter.java @@ -23,9 +23,10 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; -import javax.faces.bean.RequestScoped; +import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.transaction.Transactional; @@ -45,7 +46,7 @@ public class BlobObjectImExporter extends AbstractEntityImExporter { private BlobObjectRepository blobObjectRepository; @Override - protected Class getEntityClass() { + public Class getEntityClass() { return BlobObject.class; } @@ -61,4 +62,18 @@ public class BlobObjectImExporter extends AbstractEntityImExporter { return Collections.emptySet(); } + @Override + protected BlobObject reloadEntity(final BlobObject entity) { + return blobObjectRepository + .findById(Objects.requireNonNull(entity).getBlobObjectId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "BlobObject entity %s not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-docrepo/src/main/java/org/libreccm/docrepo/FileImExporter.java b/ccm-docrepo/src/main/java/org/libreccm/docrepo/FileImExporter.java index 09f6112a8..afe03f913 100644 --- a/ccm-docrepo/src/main/java/org/libreccm/docrepo/FileImExporter.java +++ b/ccm-docrepo/src/main/java/org/libreccm/docrepo/FileImExporter.java @@ -23,17 +23,17 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; import javax.enterprise.context.RequestScoped; - import javax.inject.Inject; import javax.transaction.Transactional; /** - * Im/Exporter for importing and exporting {@code File}s from the - * system into a specified file and the other way around. + * Im/Exporter for importing and exporting {@code File}s from the system into a + * specified file and the other way around. * * @author Tobias Osmers * @author Jens Pelzetter @@ -46,7 +46,7 @@ public class FileImExporter extends AbstractEntityImExporter { private FileRepository fileRepository; @Override - protected Class getEntityClass() { + public Class getEntityClass() { return File.class; } @@ -60,6 +60,19 @@ public class FileImExporter extends AbstractEntityImExporter { protected Set> getRequiredEntities() { return Collections.emptySet(); } - - + + @Override + protected File reloadEntity(final File entity) { + return fileRepository + .findById(Objects.requireNonNull(entity).getObjectId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "File entity %s not found in database", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-docrepo/src/main/java/org/libreccm/docrepo/FolderImExporter.java b/ccm-docrepo/src/main/java/org/libreccm/docrepo/FolderImExporter.java index b757b1eff..7645f8307 100644 --- a/ccm-docrepo/src/main/java/org/libreccm/docrepo/FolderImExporter.java +++ b/ccm-docrepo/src/main/java/org/libreccm/docrepo/FolderImExporter.java @@ -22,9 +22,10 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; -import javax.faces.bean.RequestScoped; +import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.transaction.Transactional; @@ -43,7 +44,7 @@ public class FolderImExporter extends AbstractResourceImExporter { private FolderRepository folderRepository; @Override - protected Class getEntityClass() { + public Class getEntityClass() { return Folder.class; } @@ -58,4 +59,18 @@ public class FolderImExporter extends AbstractResourceImExporter { return Collections.emptySet(); } + @Override + protected Folder reloadEntity(final Folder entity) { + return folderRepository + .findById(Objects.requireNonNull(entity).getObjectId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Folder entity %s not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } diff --git a/ccm-docrepo/src/main/java/org/libreccm/docrepo/RepositoryImExporter.java b/ccm-docrepo/src/main/java/org/libreccm/docrepo/RepositoryImExporter.java index bd2e03df2..c159f7ea9 100644 --- a/ccm-docrepo/src/main/java/org/libreccm/docrepo/RepositoryImExporter.java +++ b/ccm-docrepo/src/main/java/org/libreccm/docrepo/RepositoryImExporter.java @@ -23,9 +23,10 @@ import org.libreccm.imexport.Exportable; import org.libreccm.imexport.Processes; import java.util.Collections; +import java.util.Objects; import java.util.Set; -import javax.faces.bean.RequestScoped; +import javax.enterprise.context.RequestScoped; import javax.inject.Inject; /** @@ -40,7 +41,7 @@ public class RepositoryImExporter extends AbstractEntityImExporter { private RepositoryRepository repositoryRepository; @Override - protected Class getEntityClass() { + public Class getEntityClass() { return Repository.class; } @@ -53,7 +54,19 @@ public class RepositoryImExporter extends AbstractEntityImExporter { protected Set> getRequiredEntities() { return Collections.emptySet(); } - - + + @Override + protected Repository reloadEntity(final Repository entity) { + return repositoryRepository + .findById(Objects.requireNonNull(entity).getObjectId()) + .orElseThrow( + () -> new IllegalArgumentException( + String.format( + "Repository entity %s not found in database.", + Objects.toString(entity) + ) + ) + ); + } + } - \ No newline at end of file diff --git a/ccm-shortcuts/pom.xml b/ccm-shortcuts/pom.xml index ef64710d5..fc21ab4ff 100644 --- a/ccm-shortcuts/pom.xml +++ b/ccm-shortcuts/pom.xml @@ -73,6 +73,22 @@ provided + + javax.mvc + javax.mvc-api + provided + + + org.eclipse.krazo + krazo-core + provided + + + org.eclipse.krazo.ext + krazo-freemarker + provided + + com.fasterxml.jackson.jaxrs jackson-jaxrs-json-provider diff --git a/ccm-shortcuts/src/main/java/org/libreccm/shortcuts/Shortcuts.java b/ccm-shortcuts/src/main/java/org/libreccm/shortcuts/Shortcuts.java index f9a838e64..2415c8f3d 100644 --- a/ccm-shortcuts/src/main/java/org/libreccm/shortcuts/Shortcuts.java +++ b/ccm-shortcuts/src/main/java/org/libreccm/shortcuts/Shortcuts.java @@ -27,24 +27,29 @@ import org.libreccm.modules.RequiredModule; import org.libreccm.modules.ShutdownEvent; import org.libreccm.modules.UnInstallEvent; import org.libreccm.shortcuts.ui.ShortcutsSettingsPane; +import org.libreccm.ui.admin.applications.shortcuts.ShortcutsApplicationController; import org.libreccm.web.ApplicationType; /** - * The {@code Shortcuts} module for CCM. Defines the {@code Shortcuts} + * The {@code Shortcuts} module for CCM. Defines the {@code Shortcuts} * application and sets up the module when the module is installed. - * + * * @author Jens Pelzetter */ @Module( - requiredModules = { - @RequiredModule(module = CcmCore.class) - }, - applicationTypes = { - @ApplicationType(name = ShortcutsConstants.SHORTCUTS_APP_TYPE, - descBundle = ShortcutsConstants.SHORTCUTS_BUNDLE, - singleton = true, - settingsPane = ShortcutsSettingsPane.class, - creator = ShortcutsApplicationCreator.class)}) + requiredModules = { + @RequiredModule(module = CcmCore.class) + }, + applicationTypes = { + @ApplicationType( + name = ShortcutsConstants.SHORTCUTS_APP_TYPE, + descBundle = ShortcutsConstants.SHORTCUTS_BUNDLE, + singleton = true, + settingsPane = ShortcutsSettingsPane.class, + creator = ShortcutsApplicationCreator.class, + applicationController = ShortcutsApplicationController.class + )} +) public class Shortcuts implements CcmModule { @Override diff --git a/ccm-shortcuts/src/main/java/org/libreccm/ui/admin/applications/shortcuts/ShortcutAdminMessages.java b/ccm-shortcuts/src/main/java/org/libreccm/ui/admin/applications/shortcuts/ShortcutAdminMessages.java new file mode 100644 index 000000000..29ccbb774 --- /dev/null +++ b/ccm-shortcuts/src/main/java/org/libreccm/ui/admin/applications/shortcuts/ShortcutAdminMessages.java @@ -0,0 +1,140 @@ +/* + * 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.shortcuts; + +import org.libreccm.l10n.GlobalizationHelper; +import org.libreccm.shortcuts.ShortcutsConstants; + +import java.text.MessageFormat; +import java.util.AbstractMap; +import java.util.List; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("ShortcutAdminMessages") +public class ShortcutAdminMessages extends AbstractMap { + + /** + * Provides access to the locale negoiated by LibreCCM. + */ + @Inject + private GlobalizationHelper globalizationHelper; + + /** + * The {@link ResourceBundle} to use. + */ + private ResourceBundle messages; + + /** + * Loads the resource bundle. + */ + @PostConstruct + private void init() { + messages = ResourceBundle.getBundle( + ShortcutsConstants.SHORTCUTS_BUNDLE, + globalizationHelper.getNegotiatedLocale() + ); + } + + /** + * Retrieves a message from the resource bundle. + * + * @param key The key of the message. + * + * @return The translated message or {@code ???message???} if the the key is + * not found in the resource bundle (message is replaced with the + * key). + */ + public String getMessage(final String key) { + if (messages.containsKey(key)) { + return messages.getString(key); + } else { + return String.format("???%s???", key); + } + } + + /** + * Retrieves a message with placeholders. + * + * @param key The key of the message. + * @param parameters The parameters for the placeholders. + * + * @return The translated message or {@code ???message???} if the the key is + * not found in the resource bundle (message is replaced with the + * key). + */ + public String getMessage( + final String key, final List parameters + ) { + return getMessage(key, parameters.toArray()); + } + + /** + * The translated message or {@code ???message???} if the the key is not + * found in the resource bundle (message is replaced with the key). + * + * @param key The key of the message. + * @param parameters The parameters for the placeholders. + * + * @return The translated message or {@code ???message???} if the the key is + * not found in the resource bundle (message is replaced with the + * key). + */ + public String getMessage( + final String key, final Object[] parameters + ) { + if (messages.containsKey(key)) { + return MessageFormat.format(messages.getString(key), parameters); + } else { + return String.format("???%s???", key); + } + } + + @Override + public String get(final Object key) { + return get((String) key); + } + + public String get(final String key) { + return getMessage(key); + } + + @Override + public Set> entrySet() { + return messages + .keySet() + .stream() + .collect( + Collectors.toMap(key -> key, key -> messages.getString(key)) + ) + .entrySet(); + } + +} diff --git a/ccm-shortcuts/src/main/java/org/libreccm/ui/admin/applications/shortcuts/ShortcutsApplicationController.java b/ccm-shortcuts/src/main/java/org/libreccm/ui/admin/applications/shortcuts/ShortcutsApplicationController.java new file mode 100644 index 000000000..3a2eafe9f --- /dev/null +++ b/ccm-shortcuts/src/main/java/org/libreccm/ui/admin/applications/shortcuts/ShortcutsApplicationController.java @@ -0,0 +1,129 @@ +/* + * 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.shortcuts; + +import org.libreccm.core.CoreConstants; +import org.libreccm.security.AuthorizationRequired; +import org.libreccm.security.RequiresPrivilege; +import org.libreccm.shortcuts.Shortcut; +import org.libreccm.shortcuts.ShortcutRepository; +import org.libreccm.ui.admin.applications.ApplicationController; + +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; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +@Controller +@Path("/applications/shortcuts") +public class ShortcutsApplicationController implements ApplicationController { + + @Inject + private Models models; + + @Inject + private ShortcutRepository shortcutRepository; + + @GET + @Path("/") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + @Override + public String getApplication() { + models.put("shortcuts", shortcutRepository.findAll()); + + return "org/libreccm/ui/admin/applications/shortcuts/shortcuts.xhtml"; + } + + @POST + @Path("/add") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String addShortcut( + @FormParam("urlKey") final String urlKey, + @FormParam("redirect") final String redirect + ) { + final Shortcut shortcut = new Shortcut(); + shortcut.setUrlKey(urlKey); + shortcut.setRedirect(redirect); + + shortcutRepository.save(shortcut); + + return "redirect:applications/shortcuts"; + } + + @POST + @Path("/{shortcutId}/edit") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String updateShortcut( + @PathParam("shortcutId") final long shortcutId, + @FormParam("urlKey") final String urlKey, + @FormParam("redirect") final String redirect + ) { + final Optional result = shortcutRepository + .findById(shortcutId); + + if (result.isPresent()) { + final Shortcut shortcut = result.get(); + shortcut.setUrlKey(urlKey); + shortcut.setRedirect(redirect); + + shortcutRepository.save(shortcut); + } + + return "redirect:applications/shortcuts"; + } + + @POST + @Path("/{shortcutId}/remove") + @AuthorizationRequired + @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) + @Transactional(Transactional.TxType.REQUIRED) + public String removeShortcut( + @PathParam("shortcutId") final long shortcutId, + @FormParam("confirmed") final String confirmed + ) { + final Optional result = shortcutRepository + .findById(shortcutId); + + if (result.isPresent() && "true".equals(confirmed)) { + shortcutRepository.delete(result.get()); + } + + return "redirect:applications/shortcuts"; + } + +} diff --git a/ccm-shortcuts/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/applications/shortcuts/shortcuts.xhtml b/ccm-shortcuts/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/applications/shortcuts/shortcuts.xhtml new file mode 100644 index 000000000..83e258cd7 --- /dev/null +++ b/ccm-shortcuts/src/main/resources/WEB-INF/views/org/libreccm/ui/admin/applications/shortcuts/shortcuts.xhtml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + +
    +

    #{ShortcutAdminMessages['application_title']}

    + +
    + + +

    #{ShortcutAdminMessages['shortcuts.ui.admin.add_shortcut.dialog.title']}

    +
    + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + +
    #{ShortcutAdminMessages['shortcuts.ui.admin.shortcuts_table.col_url_key.header']}#{ShortcutAdminMessages['shortcuts.ui.admin.shortcuts_table.col_redirect.header']}#{ShortcutAdminMessages['shortcuts.ui.admin.shortcuts_table.col_actions.header']}
    #{shortcut.urlKey}#{shortcut.redirect} + + +

    #{ShortcutAdminMessages['shortcuts.ui.admin.edit_shortcut.dialog.title']}

    +
    + + + + + + + + +
    +
    + +
    +
    +
    +
    + + diff --git a/ccm-shortcuts/src/main/resources/org/libreccm/shortcuts/ShortcutsResources.properties b/ccm-shortcuts/src/main/resources/org/libreccm/shortcuts/ShortcutsResources.properties index 38469b544..fa449df70 100644 --- a/ccm-shortcuts/src/main/resources/org/libreccm/shortcuts/ShortcutsResources.properties +++ b/ccm-shortcuts/src/main/resources/org/libreccm/shortcuts/ShortcutsResources.properties @@ -19,6 +19,7 @@ application_title=Shortcuts application_desc=Manage short URLs for internal and external redirects. shortcuts.ui.admin.shortcuts_table.col_url_key.header=URL shortcuts.ui.admin.shortcuts_table.col_redirect.header=Redirect +shortcuts.ui.admin.shortcuts_table.col_actions.header=Actions shortcuts.ui.admin.shortcuts_table.col_edit.header=Edit shortcuts.ui.admin.shortcuts_table.col_delete.header=Delete shortcuts.ui.admin.shortcuts_table.edit=Edit @@ -32,3 +33,20 @@ shortcuts.ui.admin.redirect.error.invalid=The target of the redirect must be a a shortcuts.ui.admin.heading=Manage shortcuts shortcuts.ui.admin.table.empty=No shortcuts definied yet shortcuts.ui.admin.add_shortcut=Add shortcut +shortcuts.ui.admin.delete_dialog.cancel=Cancel +shortcuts.ui.admin.delete_dialog.confirm=Remove shortcut +shortcuts.ui.admin.delete_dialog.title=Confirm removal of shortcut +shortcuts.ui.admin.delete_dialog.message=Are you sure to remove the shortcut which redirects the URL {0} to {1}? +shortcuts.ui.admin.add_shortcut.dialog.title=Add shortcut +shortcuts.ui.admin.add_shortcut.dialog.urlkey.help=The short URL to redirect +shortcuts.ui.admin.add_shortcut.dialog.urlkey.label=Short URL +shortcuts.ui.admin.add_shortcut.dialog.redirect.help=The target URL to which the user is redirected +shortcuts.ui.admin.add_shortcut.dialog.redirect.label=Redirect target +shortcuts.ui.admin.add_shortcut.dialog.cancel=Cancel +shortcuts.ui.admin.add_shortcut.dialog.submit=Create shortcut +shortcuts.ui.admin.edit_shortcut.dialog.urlkey.help=The short URL +shortcuts.ui.admin.edit_shortcut.dialog.urlkey.label=Short URL +shortcuts.ui.admin.edit_shortcut.dialog.redirect.help=The target URL to which the user is redirected +shortcuts.ui.admin.edit_shortcut.dialog.redirect.label=Redirect target +shortcuts.ui.admin.edit_shortcut.dialog.cancel=Cancel +shortcuts.ui.admin.edit_shortcut.dialog.submit=Save diff --git a/ccm-shortcuts/src/main/resources/org/libreccm/shortcuts/ShortcutsResources_de.properties b/ccm-shortcuts/src/main/resources/org/libreccm/shortcuts/ShortcutsResources_de.properties index 1fefabf78..d7348a459 100644 --- a/ccm-shortcuts/src/main/resources/org/libreccm/shortcuts/ShortcutsResources_de.properties +++ b/ccm-shortcuts/src/main/resources/org/libreccm/shortcuts/ShortcutsResources_de.properties @@ -32,3 +32,21 @@ shortcuts.ui.admin.redirect.error.invalid=Das Ziel der Weiterleitung muss eine a shortcuts.ui.admin.heading=Shortcuts verwalten shortcuts.ui.admin.table.empty=Es wurden noch keine Shortcuts angelegt. shortcuts.ui.admin.add_shortcut=Shortcut hinzuf\u00fcgen +shortcuts.ui.admin.shortcuts_table.col_actions.header=Aktionen +shortcuts.ui.admin.delete_dialog.cancel=Abbbrechen +shortcuts.ui.admin.delete_dialog.confirm=Shortcut entfernen +shortcuts.ui.admin.delete_dialog.title=Confirm removal of shortcut +shortcuts.ui.admin.delete_dialog.message=Sind Sie sicher, dass Sie den Shortcut f\u00fcr die Umleitung von {0} nach {1} entfernen wollen? +shortcuts.ui.admin.add_shortcut.dialog.title=Shortcut hinzuf\u00fcgen +shortcuts.ui.admin.add_shortcut.dialog.urlkey.help=Die Kurz-URL, die umgeleitetet werden soll +shortcuts.ui.admin.add_shortcut.dialog.urlkey.label=Kurz-URL +shortcuts.ui.admin.add_shortcut.dialog.redirect.help=An diese URL wird weitergeleitet. +shortcuts.ui.admin.add_shortcut.dialog.redirect.label=Weiterleitungsziel +shortcuts.ui.admin.add_shortcut.dialog.cancel=Abbrechen +shortcuts.ui.admin.add_shortcut.dialog.submit=Shortcut anlegen +shortcuts.ui.admin.edit_shortcut.dialog.urlkey.help=Die Kurz-URL, die umgeleitetet werden soll +shortcuts.ui.admin.edit_shortcut.dialog.urlkey.label=Kurz-URL +shortcuts.ui.admin.edit_shortcut.dialog.redirect.help=An diese URL wird weitergeleitet. +shortcuts.ui.admin.edit_shortcut.dialog.redirect.label=Weiterleitungsziel +shortcuts.ui.admin.edit_shortcut.dialog.cancel=Abbrechen +shortcuts.ui.admin.edit_shortcut.dialog.submit=Speichern diff --git a/pom.xml b/pom.xml index 017bab17b..abe3fa346 100644 --- a/pom.xml +++ b/pom.xml @@ -693,6 +693,11 @@ commons-primitives 1.0 + + commons-validator + commons-validator + 1.7 +