diff --git a/ccm-cms/pom.xml b/ccm-cms/pom.xml
index ff021d971..49a8c9740 100644
--- a/ccm-cms/pom.xml
+++ b/ccm-cms/pom.xml
@@ -122,6 +122,11 @@
org.jsoup
jsoup
+
+
+ org.apache.commons
+ commons-lang3
+
junit
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/article/MvcArticleTextBodyStep.java b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/article/MvcArticleTextBodyStep.java
index 4a6ad528e..aaef37a9b 100644
--- a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/article/MvcArticleTextBodyStep.java
+++ b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/article/MvcArticleTextBodyStep.java
@@ -18,7 +18,6 @@
*/
package org.librecms.ui.contenttypes.article;
-import com.arsdigita.kernel.KernelConfig;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionStepService.java b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionStepService.java
index 8c8697612..7a9444fb5 100644
--- a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionStepService.java
+++ b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionStepService.java
@@ -18,13 +18,23 @@
*/
package org.librecms.ui.contenttypes.mpa;
+import org.librecms.contentsection.ContentItem;
+import org.librecms.contentsection.ContentItemRepository;
+import org.librecms.contentsection.ContentSection;
+import org.librecms.contenttypes.MultiPartArticle;
+import org.librecms.contenttypes.MultiPartArticleSection;
+import org.librecms.contenttypes.MultiPartArticleSectionRepository;
+import org.librecms.ui.contentsections.ContentSectionsUi;
import org.librecms.ui.contentsections.documents.MvcAuthoringSteps;
import java.util.List;
import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
import javax.transaction.Transactional;
+import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
+import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@@ -39,6 +49,15 @@ import javax.ws.rs.core.Response;
@Path(MvcAuthoringSteps.PATH_PREFIX + "mpa-sections-service")
public class MpaSectionStepService {
+ @Inject
+ private ContentItemRepository itemRepo;
+
+ @Inject
+ private MultiPartArticleSectionRepository sectionRepo;
+
+ @Inject
+ private ContentSectionsUi sectionsUi;
+
@POST
@Path("/save-order")
@Consumes(MediaType.APPLICATION_JSON)
@@ -50,8 +69,80 @@ public class MpaSectionStepService {
final String documentPath,
final List sectionsOrder
) {
- // ToDo
- throw new UnsupportedOperationException();
+ final ContentSection contentSection = sectionsUi
+ .findContentSection(sectionIdentifier)
+ .orElseThrow(
+ () -> new NotFoundException(
+ String.format(
+ "No content identifed by %s found.",
+ sectionIdentifier
+ )
+ )
+ );
+
+ final ContentItem document = itemRepo
+ .findByPath(contentSection, documentPath)
+ .orElseThrow(
+ () -> new NotFoundException(
+ String.format(
+ "No document for path %s in section %s.",
+ documentPath,
+ contentSection.getLabel()
+ )
+ )
+ );
+
+ if ((document instanceof MultiPartArticle)) {
+ throw new BadRequestException(
+ String.format(
+ "Document %s is not a %s.",
+ documentPath,
+ MultiPartArticle.class.getSimpleName()
+ )
+ );
+ }
+
+ final MultiPartArticle mpa = (MultiPartArticle) document;
+
+ final List sections = mpa.getSections();
+
+ if (sectionsOrder.size() != sections.size()) {
+ throw new BadRequestException(
+ String.format(
+ "Number of sections of the MultiPartArticle %s does "
+ + "not match the number of values in the oder "
+ + "list. Number of sections: %d, number of values in "
+ + "the sections order list: %d",
+ documentPath,
+ sections.size(),
+ sectionsOrder.size()
+ )
+ );
+ }
+
+ for (int i = 0; i < sectionsOrder.size(); i++) {
+ final String sectionIdParam = sectionsOrder.get(i);
+ final long sectionId = Long.parseLong(sectionIdParam);
+ final MultiPartArticleSection section = sections
+ .stream()
+ .filter(sec -> sec.getSectionId() == sectionId)
+ .findAny()
+ .orElseThrow(
+ () -> new BadRequestException(
+ String.format(
+ "sectionsOrder has an entry for section with "
+ + "ID %d, but there is not section with that "
+ + "ID.",
+ sectionId
+ )
+ )
+ );
+
+ section.setRank(i);
+ sectionRepo.save(section);
+ }
+
+ return Response.ok().build();
}
}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionsStepModel.java b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionsStepModel.java
index ed0bb9ebf..f5f4adee3 100644
--- a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionsStepModel.java
+++ b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionsStepModel.java
@@ -18,6 +18,10 @@
*/
package org.librecms.ui.contenttypes.mpa;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@@ -31,6 +35,8 @@ public class MpaSectionsStepModel {
private boolean canEdit;
+ private List rows;
+
public boolean getCanEdit() {
return canEdit;
}
@@ -39,4 +45,12 @@ public class MpaSectionsStepModel {
this.canEdit = canEdit;
}
+ public List getRows() {
+ return Collections.unmodifiableList(rows);
+ }
+
+ protected void setRows(final List rows) {
+ this.rows = new ArrayList<>(rows);
+ }
+
}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionsTableRow.java b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionsTableRow.java
new file mode 100644
index 000000000..6e85e9362
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MpaSectionsTableRow.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 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.librecms.ui.contenttypes.mpa;
+
+import java.util.Comparator;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+public class MpaSectionsTableRow implements Comparable {
+
+ private long sectionId;
+
+ private String title;
+
+ private int rank;
+
+ private boolean pageBreak;
+
+ private String textPreview;
+
+ public long getSectionId() {
+ return sectionId;
+ }
+
+ protected void setSectionId(final long sectionId) {
+ this.sectionId = sectionId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ protected void setTitle(final String title) {
+ this.title = title;
+ }
+
+ public int getRank() {
+ return rank;
+ }
+
+ protected void setRank(final int rank) {
+ this.rank = rank;
+ }
+
+ public boolean isPageBreak() {
+ return pageBreak;
+ }
+
+ protected void setPageBreak(final boolean pageBreak) {
+ this.pageBreak = pageBreak;
+ }
+
+ public String getTextPreview() {
+ return textPreview;
+ }
+
+ protected void setTextPreview(final String textPreview) {
+ this.textPreview = textPreview;
+ }
+
+ @Override
+ public int compareTo(final MpaSectionsTableRow other) {
+ return Comparator
+ .comparing(MpaSectionsTableRow::getRank)
+ .thenComparing(
+ Comparator.comparing(MpaSectionsTableRow::getTitle)
+ )
+ .compare(this, other);
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MvcMpaCreateStep.java b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MvcMpaCreateStep.java
index 6dbcdb495..920dd725e 100644
--- a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MvcMpaCreateStep.java
+++ b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MvcMpaCreateStep.java
@@ -35,7 +35,6 @@ import java.util.Optional;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
-import javax.mvc.Models;
import javax.transaction.Transactional;
/**
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MvcMpaSectionsStep.java b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MvcMpaSectionsStep.java
index aa5be86b6..46dc39f51 100644
--- a/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MvcMpaSectionsStep.java
+++ b/ccm-cms/src/main/java/org/librecms/ui/contenttypes/mpa/MvcMpaSectionsStep.java
@@ -18,7 +18,11 @@
*/
package org.librecms.ui.contenttypes.mpa;
+import org.apache.commons.lang3.StringUtils;
+import org.libreccm.l10n.GlobalizationHelper;
import org.librecms.contenttypes.MultiPartArticle;
+import org.librecms.contenttypes.MultiPartArticleSection;
+import org.librecms.contenttypes.MultiPartArticleSectionManager;
import org.librecms.ui.contentsections.ContentSectionNotFoundException;
import org.librecms.ui.contentsections.ItemPermissionChecker;
import org.librecms.ui.contentsections.documents.AbstractMvcAuthoringStep;
@@ -27,10 +31,18 @@ import org.librecms.ui.contentsections.documents.DocumentUi;
import org.librecms.ui.contentsections.documents.MvcAuthoringStepDef;
import org.librecms.ui.contentsections.documents.MvcAuthoringSteps;
+import java.util.Comparator;
+import java.util.Locale;
+import java.util.Objects;
+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.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@@ -59,9 +71,18 @@ public class MvcMpaSectionsStep extends AbstractMvcAuthoringStep {
@Inject
private DocumentUi documentUi;
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
@Inject
private ItemPermissionChecker itemPermissionChecker;
+ @Inject
+ private MultiPartArticleSectionManager sectionManager;
+
+ @Inject
+ private Models models;
+
@Inject
private MpaSectionsStepModel mpaSectionsStepModel;
@@ -88,6 +109,15 @@ public class MvcMpaSectionsStep extends AbstractMvcAuthoringStep {
}
if (itemPermissionChecker.canEditItem(getMpa())) {
+ mpaSectionsStepModel.setRows(
+ getMpa()
+ .getSections()
+ .stream()
+ .map(this::buildMpaSectionsTableRow)
+ .sorted()
+ .collect(Collectors.toList())
+ );
+
return "org/librecms/ui/contenttypes/mpa/mpa-sections.xhtml";
} else {
return documentUi.showAccessDenied(
@@ -99,34 +129,293 @@ public class MvcMpaSectionsStep extends AbstractMvcAuthoringStep {
}
@POST
- @Path("/add")
+ @Path("/@add")
@Transactional(Transactional.TxType.REQUIRED)
public String addSection(
@PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
final String sectionIdentifier,
@PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
final String documentPath,
- @FormParam("initialLocal")
+ @FormParam("initialLocal") @DefaultValue("")
final String initialLocaleParam,
- @FormParam("title")
+ @FormParam("title") @DefaultValue("")
final String title,
- @FormParam("text")
+ @FormParam("text") @DefaultValue("")
final String text
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getMpa())) {
+ if (initialLocaleParam.isBlank()) {
+ models.put("initialLocaleMissing", true);
+ models.put("title", title);
+ models.put("text", text);
+ return showStep(sectionIdentifier, documentPath);
+ }
+ if (title.isBlank()) {
+ models.put("titleMissing", true);
+ models.put("initialLocale", initialLocaleParam);
+ models.put("text", text);
+
+ return showStep(sectionIdentifier, documentPath);
+ }
+
+ final MultiPartArticleSection section
+ = new MultiPartArticleSection();
+ section.setPageBreak(false);
+ section.setRank(
+ getMpa()
+ .getSections()
+ .stream()
+ .map(MultiPartArticleSection::getRank)
+ .max(Comparator.naturalOrder())
+ .orElse(0) + 1
+ );
+
+ final Locale initialLocale = new Locale(initialLocaleParam);
+
+ section.getText().addValue(initialLocale, text);
+ section.getTitle().addValue(initialLocale, title);
+
+ sectionManager.addSectionToMultiPartArticle(section, getMpa());
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getMpa(),
+ mpaMessageBundle.getMessage("mpa.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/@remove/{sectionId}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeSection(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam
+ ) {
+ try {
+ init();
+ } catch (ContentSectionNotFoundException ex) {
+ return ex.showErrorMessage();
+ } catch (DocumentNotFoundException ex) {
+ return ex.showErrorMessage();
+ }
+
+ if (itemPermissionChecker.canEditItem(getMpa())) {
+ if (!sectionIdParam.matches("[0-9]*")) {
+ models.put("invalidSectionId", true);
+ return showStep(sectionIdentifier, documentPath);
+ }
+
+ final long sectionId = Long.parseLong(sectionIdParam);
+
+ final Optional result
+ = getMpa()
+ .getSections()
+ .stream()
+ .filter(section -> section.getSectionId() == sectionId)
+ .findAny();
+
+ if (!result.isPresent()) {
+ models.put("sectionNotFound", true);
+ return showStep(sectionIdentifier, documentPath);
+ }
+
+ final MultiPartArticleSection sectionToRemove = result.get();
+
+ sectionManager.removeSectionFromMultiPartArticle(
+ sectionToRemove, getMpa()
+ );
+
+ return buildRedirectPathForStep();
+ } else {
+ return documentUi.showAccessDenied(
+ getContentSection(),
+ getMpa(),
+ mpaMessageBundle.getMessage("mpa.edit.denied")
+ );
+ }
+ }
+
+ @POST
+ @Path("/{sectionId}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String showSection(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam
) {
// ToDo
throw new UnsupportedOperationException();
}
@POST
- @Path("/remove/{sectionId}")
+ @Path("/{sectionId}/title/@add")
@Transactional(Transactional.TxType.REQUIRED)
- public String removeSection(
- @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ public String addTitleValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
final String sectionIdentifier,
@PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
final String documentPath,
- @PathParam("sectionId")
- final String sectionIdParam
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @FormParam("locale")
+ final String localeParam
+ ) {
+ // ToDo
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/{sectionId}/title/@edit/{locale}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editTitleValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @PathParam("locale")
+ final String localeParam
+ ) {
+ // ToDo
+ throw new UnsupportedOperationException();
+ }
+
+ @POST
+ @Path("/{sectionId}/title/@edit/{locale}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editTitleValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @PathParam("locale")
+ final String localeParam,
+ @FormParam("value")
+ final String value
+ ) {
+ // ToDo
+ throw new UnsupportedOperationException();
+ }
+
+ @POST
+ @Path("/{sectionId}/title/@remove")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeTitleValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @FormParam("locale")
+ final String localeParam
+ ) {
+ // ToDo
+ throw new UnsupportedOperationException();
+ }
+
+ @POST
+ @Path("/{sectionId}/text/@add")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String addTextValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @FormParam("locale")
+ final String localeParam
+ ) {
+ // ToDo
+ throw new UnsupportedOperationException();
+ }
+
+ @GET
+ @Path("/{sectionId}/text/@edit/{locale}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editTextValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @PathParam("locale")
+ final String localeParam
+ ) {
+ // ToDo
+ throw new UnsupportedOperationException();
+ }
+
+ @POST
+ @Path("/{sectionId}/text/@edit/{locale}")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editTextValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @PathParam("locale")
+ final String localeParam,
+ @FormParam("value")
+ final String value
+ ) {
+ // ToDo
+ throw new UnsupportedOperationException();
+ }
+
+ @POST
+ @Path("/{sectionId}/text/@remove")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String removeTextValue(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @FormParam("locale")
+ final String localeParam
+ ) {
+ // ToDo
+ throw new UnsupportedOperationException();
+ }
+
+ @POST
+ @Path("/{sectionId}/pagebreak")
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String setPageBreak(
+ @PathParam(MvcAuthoringSteps.SECTION_IDENTIFIER_PATH_PARAM)
+ final String sectionIdentifier,
+ @PathParam(MvcAuthoringSteps.DOCUMENT_PATH_PATH_PARAM_NAME)
+ final String documentPath,
+ @PathParam("sectionId")
+ final String sectionIdParam,
+ @FormParam("pageBreak") final String pageBreakParam
) {
// ToDo
throw new UnsupportedOperationException();
@@ -149,4 +438,34 @@ public class MvcMpaSectionsStep extends AbstractMvcAuthoringStep {
return (MultiPartArticle) getDocument();
}
+ private MpaSectionsTableRow buildMpaSectionsTableRow(
+ final MultiPartArticleSection section
+ ) {
+ final MpaSectionsTableRow row = new MpaSectionsTableRow();
+ row.setPageBreak(section.isPageBreak());
+ row.setRank(section.getRank());
+ row.setSectionId(section.getSectionId());
+ final String text = Objects.requireNonNullElse(
+ globalizationHelper.getValueFromLocalizedString(section.getText()),
+ ""
+ );
+ if (text.length() <= 100) {
+ row.setTextPreview(text);
+ } else {
+ row.setTextPreview(
+ String.format(
+ "%s...",
+ StringUtils.truncate(text, 97)
+ )
+ );
+ }
+ row.setTitle(
+ globalizationHelper.getValueFromLocalizedString(
+ section.getText()
+ )
+ );
+
+ return row;
+ }
+
}
diff --git a/pom.xml b/pom.xml
index 7691c31d0..b4ff45e41 100644
--- a/pom.xml
+++ b/pom.xml
@@ -708,6 +708,11 @@
commons-lang
2.6
+
+ org.apache.commons
+ commons-lang3
+ 3.12.0
+
commons-logging
commons-logging