diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AbstractMvcAssetCreateStep.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AbstractMvcAssetCreateStep.java
new file mode 100644
index 000000000..8e5196a16
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AbstractMvcAssetCreateStep.java
@@ -0,0 +1,157 @@
+/*
+ * 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.contentsections.assets;
+
+import org.libreccm.l10n.GlobalizationHelper;
+import org.librecms.contentsection.Asset;
+import org.librecms.contentsection.ContentSection;
+import org.librecms.contentsection.Folder;
+import org.librecms.contentsection.FolderManager;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+import javax.transaction.Transactional;
+
+/**
+ *
+ * @author Jens Pelzetter
+ * @param
+ */
+public abstract class AbstractMvcAssetCreateStep
+ implements MvcAssetCreateStep {
+
+ /**
+ * Provides operations for folders.
+ */
+ @Inject
+ private FolderManager folderManager;
+
+ /**
+ * Provides functions for working with {@link LocalizedString}s.
+ */
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ private boolean canCreate;
+
+ /**
+ * The current folder.
+ */
+ private Folder folder;
+
+ /**
+ * The current content section.
+ */
+ private ContentSection section;
+
+ /**
+ * Messages to be shown to the user.
+ */
+ private SortedMap messages;
+
+ public AbstractMvcAssetCreateStep() {
+ messages = new TreeMap<>();
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ @Override
+ public Map getAvailableLocales() {
+ return globalizationHelper
+ .getAvailableLocales()
+ .stream()
+ .collect(
+ Collectors.toMap(
+ locale -> locale.toString(),
+ locale -> locale.toString(),
+ (value1, value2) -> value1,
+ () -> new LinkedHashMap()
+ )
+ );
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ @Override
+ public ContentSection getContentSection() {
+ return section;
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ @Override
+ public void setContentSection(final ContentSection section) {
+ this.section = section;
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ @Override
+ public String getContentSectionLabel() {
+ return section.getLabel();
+ }
+
+ @Transactional(Transactional.TxType.REQUIRED)
+ @Override
+ public String getContentSectionTitle() {
+ return globalizationHelper.getValueFromLocalizedString(
+ section.getTitle()
+ );
+ }
+
+ @Override
+ public boolean getCanCreate() {
+ return canCreate;
+ }
+
+ @Override
+ public Folder getFolder() {
+ return folder;
+ }
+
+ @Override
+ public void setFolder(final Folder folder) {
+ this.folder = folder;
+ }
+
+ @Override
+ public String getFolderPath() {
+ if (folder.getParentFolder() == null) {
+ return "";
+ } else {
+ return folderManager.getFolderPath(folder);
+ }
+ }
+
+ @Override
+ public Map getMessages() {
+ return Collections.unmodifiableSortedMap(messages);
+ }
+
+ public void addMessage(final String context, final String message) {
+ messages.put(context, message);
+ }
+
+ public void setMessages(final SortedMap messages) {
+ this.messages = new TreeMap<>(messages);
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetEditStepsValidator.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetEditStepsValidator.java
new file mode 100644
index 000000000..cc931a324
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetEditStepsValidator.java
@@ -0,0 +1,112 @@
+/*
+ * 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.contentsections.assets;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.librecms.contentsection.Asset;
+
+import java.util.Optional;
+
+import javax.enterprise.context.Dependent;
+import javax.mvc.Controller;
+import javax.ws.rs.Path;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@Dependent
+public class AssetEditStepsValidator {
+
+ private static final Logger LOGGER = LogManager.getLogger(
+ AssetEditStepsValidator.class
+ );
+
+ public boolean validateEditStep(final Class> stepClass) {
+ if (stepClass.getAnnotation(Controller.class) == null) {
+ LOGGER.warn(
+ "Class {} is part of a set of asset edit steps, but is not"
+ + " annotated with {}. The class will be ignored.",
+ stepClass.getName(),
+ Controller.class.getName()
+ );
+ return false;
+ }
+
+ final Path pathAnnotation = stepClass.getAnnotation(Path.class);
+ if (pathAnnotation == null) {
+ LOGGER.warn(
+ "Class {} is part of a set of asset edit steps, but is not "
+ + "annotated with {}. the class will be ignored.",
+ stepClass.getName(),
+ Path.class.getName()
+ );
+ return false;
+ }
+
+ final String path = pathAnnotation.value();
+ if (path == null
+ || !path.startsWith(MvcAssetEditSteps.PATH_PREFIX)) {
+ LOGGER.warn(
+ "Class {} is part of a set of asset edit steps, but the value"
+ + "of the {} annotation of the class does not start "
+ + "with {}. The class will be ignored.",
+ stepClass.getName(),
+ Path.class.getName(),
+ MvcAssetEditSteps.PATH_PREFIX
+ );
+ return false;
+ }
+
+ if (stepClass.getAnnotation(MvcAssetEditStep.class) == null) {
+ LOGGER.warn(
+ "Class {} is part of a set of asset edit steps, but is not "
+ + "annotated with {}. The class will be ignored.",
+ stepClass.getName(),
+ MvcAssetEditStep.class
+ );
+ }
+
+ return true;
+ }
+
+ public boolean supportsAsset(final Class> stepClass, final Asset asset) {
+ return Optional
+ .ofNullable(stepClass.getAnnotation(MvcAssetEditStep.class))
+ .map(
+ stepAnnotation -> asset.getClass().isAssignableFrom(
+ stepAnnotation.supportedAssetType()
+ )
+ )
+ .orElse(false);
+
+// final MvcAssetEditStep stepAnnotation = stepClass.getAnnotation(
+// MvcAssetEditStep.class
+// );
+//
+// if (stepAnnotation == null) {
+// return false;
+// } else {
+// return asset.getClass().isAssignableFrom(
+// stepAnnotation.supportedAssetType());
+// }
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetStepsDefaultMessagesBundle.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetStepsDefaultMessagesBundle.java
new file mode 100644
index 000000000..b8a3337d3
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetStepsDefaultMessagesBundle.java
@@ -0,0 +1,39 @@
+/*
+ * 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.contentsections.assets;
+
+import org.libreccm.ui.AbstractMessagesBean;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Named;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CmsAssetsStepsDefaultMessagesBundle")
+public class AssetStepsDefaultMessagesBundle extends AbstractMessagesBean {
+
+ @Override
+ public String getMessageBundle() {
+ return MvcAssetStepsConstants.BUNDLE;
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetUi.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetUi.java
new file mode 100644
index 000000000..e46cd32c1
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetUi.java
@@ -0,0 +1,72 @@
+/*
+ * 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.contentsections.assets;
+
+import org.librecms.contentsection.Asset;
+import org.librecms.contentsection.AssetManager;
+import org.librecms.contentsection.ContentSection;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Models;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+public class AssetUi {
+
+ @Inject
+ private AssetManager assetManager;
+
+ /**
+ * Used to provide data for the views without a named bean.
+ */
+ @Inject
+ private Models models;
+
+ public String showAccessDenied(
+ final ContentSection section,
+ final Asset asset,
+ final String step
+ ) {
+ return showAccessDenied(
+ section, assetManager.getAssetPath(asset), step
+ );
+ }
+
+ public String showAccessDenied(
+ final ContentSection section, final String assetPath, final String step
+ ) {
+ models.put("section", section.getLabel());
+ models.put("assetPath", assetPath);
+ models.put(step, step);
+ return "org/librecms/ui/contentsections/assets/access-denied.xhtml";
+ }
+
+ public String showAssetNotFound(
+ final ContentSection section, final String assetPath
+ ) {
+ models.put("section", section.getLabel());
+ models.put("assetPath", assetPath);
+ return "/org/librecms/ui/contentsections/assets/asset-not-found.xhtml";
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetsController.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetsController.java
new file mode 100644
index 000000000..8ccd9dcd9
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/AssetsController.java
@@ -0,0 +1,503 @@
+/*
+ * 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.contentsections.assets;
+
+import org.libreccm.l10n.GlobalizationHelper;
+import org.libreccm.security.AuthorizationRequired;
+import org.libreccm.security.PermissionChecker;
+import org.librecms.contentsection.Asset;
+import org.librecms.contentsection.AssetManager;
+import org.librecms.contentsection.AssetRepository;
+import org.librecms.contentsection.ContentItemRepository;
+import org.librecms.contentsection.ContentSection;
+import org.librecms.contentsection.Folder;
+import org.librecms.contentsection.FolderRepository;
+import org.librecms.contentsection.FolderType;
+import org.librecms.contentsection.privileges.AssetPrivileges;
+import org.librecms.ui.contentsections.AssetPermissionsChecker;
+import org.librecms.ui.contentsections.ContentSectionModel;
+import org.librecms.ui.contentsections.ContentSectionsUi;
+
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.enterprise.inject.Any;
+import javax.enterprise.inject.Instance;
+import javax.inject.Inject;
+import javax.mvc.Controller;
+import javax.mvc.Models;
+import javax.servlet.http.HttpServletRequest;
+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.Context;
+import javax.ws.rs.core.MediaType;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Path("/{sectionIdentifier}/assets")
+@Controller
+public class AssetsController {
+
+ @Inject
+ private AssetEditStepsValidator stepsValidator;
+
+ @Inject
+ private AssetManager assetManager;
+
+ @Inject
+ private ContentSectionModel sectionModel;
+
+ /**
+ * {@link ContentSectionsUi} instance providing for helper functions for
+ * dealing with {@link ContentSection}s.
+ */
+ @Inject
+ private ContentSectionsUi sectionsUi;
+
+ /**
+ * {@link AssetUi} instance providing some common functions for managing
+ * assets.
+ */
+ @Inject
+ private AssetUi assetUi;
+
+ /**
+ * {@link FolderRepository} instance for retrieving folders.
+ */
+ @Inject
+ private FolderRepository folderRepo;
+
+ /**
+ * {@link ContentItemRepository} instance for retrieving content items.
+ */
+ @Inject
+ private AssetRepository assetRepo;
+
+ @Inject
+ @Any
+ private Instance> assetCreateSteps;
+
+ @Inject
+ private AssetStepsDefaultMessagesBundle defaultStepsMessageBundle;
+
+ /**
+ * {@link GlobalizationHelper} for working with localized texts etc.
+ */
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ @Inject
+ private AssetPermissionsChecker assetPermissionsChecker;
+
+ /**
+ * Used to make avaiable in the views without a named bean.
+ */
+ @Inject
+ private Models models;
+
+ /**
+ * Used to check permissions on content items.
+ */
+ @Inject
+ private PermissionChecker permissionChecker;
+
+ /**
+ * Named bean providing access to the properties of the selected asset from
+ * the view.
+ */
+ @Inject
+ private SelectedAssetModel selectedAssetModel;
+
+ /*
+ * Redirect requests to the root path of this controller to the path for
+ * displaying the content of the root asset folder. The root path of this
+ * controller has no function. We assume that somebody who access the root
+ * folders wants to browse all asset in the content section. Therefore we
+ * redirect these requests to the root folder.
+ *
+ * @param sectionIdentifier The identififer of the current content section.
+ *
+ * @return A redirect to the root assets folder.
+ */
+ @GET
+ @Path("/")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String redirectToAssetFolders(
+ @PathParam("sectionIdentifier") final String sectionIdentifier
+ ) {
+ return String.format(
+ "redirect:/%s/assetfolders/",
+ sectionIdentifier
+ );
+ }
+
+ /**
+ * Delegates requests for the path {@code @create} to the create step
+ * (subresource) of the asset type. The new asset will be created in the
+ * root folder of the current content section.
+ *
+ * @param sectionIdentifier The identifier of the current content section.
+ * @param assetType The type of the asset to create.
+ *
+ * @return The template of the create step.
+ */
+ @GET
+ @Path("/@create/{assetType}")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String showCreateStep(
+ @PathParam("sectionIdentifier") final String sectionIdentifier,
+ @PathParam("assetType") final String assetType
+ ) {
+ return showCreateStep(sectionIdentifier, "", assetType);
+ }
+
+ @POST
+ @Path("/@create")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String showCreateStepPost(
+ @PathParam("sectionIdentifier") final String sectionIdentifier,
+ @FormParam("documentType") final String assetType
+ ) {
+ return String.format(
+ "redirect:/%s/assets/@create/%s",
+ sectionIdentifier,
+ assetType
+ );
+ }
+
+ @GET
+ @Path("/{folderPath:(.+)?}/@create/{assetType}")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String showCreateStep(
+ @PathParam("sectionIdentifier") final String sectionIdentifier,
+ @PathParam("folderPath") final String folderPath,
+ @FormParam("assetType") final String assetType
+ ) {
+ final CreateStepResult result = findCreateStep(
+ sectionIdentifier,
+ folderPath,
+ assetType
+ );
+
+ if (result.isCreateStepAvailable()) {
+ return result.getCreateStep().showCreateStep();
+ } else {
+ return result.getErrorTemplate();
+ }
+ }
+
+ @POST
+ @Path("/{folderPath:(.+)?}/@create")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String showCreateStepPost(
+ @PathParam("sectionIdentifier") final String sectionIdentifier,
+ @PathParam("folderPath") final String folderPath,
+ @FormParam("assetType") final String assetType
+ ) {
+ return String.format(
+ "redirect:/%s/documents/%s/@create/%s",
+ sectionIdentifier,
+ folderPath,
+ assetType
+ );
+ }
+
+ @POST
+ @Path("/@create/{assetType}")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String createAsset(
+ @PathParam("sectionIdentifier") final String sectionIdentifier,
+ @PathParam("assetType") final String assetType,
+ @Context final HttpServletRequest request
+ ) {
+ return createAsset(
+ sectionIdentifier,
+ "",
+ assetType,
+ request
+ );
+ }
+
+ @POST
+ @Path("/{folderPath:(.+)?}/@create/{assetType}")
+ @AuthorizationRequired
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String createAsset(
+ @PathParam("sectionIdentifier") final String sectionIdentifier,
+ @PathParam("folderPath") final String folderPath,
+ @PathParam("assetType") final String assetType,
+ @Context final HttpServletRequest request
+ ) {
+ final CreateStepResult result = findCreateStep(
+ sectionIdentifier,
+ folderPath,
+ assetType
+ );
+
+ if (result.isCreateStepAvailable()) {
+ return result.getCreateStep().createAsset(
+ request.getParameterMap()
+ );
+ } else {
+ return result.getErrorTemplate();
+ }
+ }
+
+ @GET
+ @Path("/{assetPath:(.+)?")
+ @AuthorizationRequired
+ @Transactional(Transactional.TxType.REQUIRED)
+ public String editAsset(
+ @PathParam("sectionIdentifier") final String sectionIdentifier,
+ @PathParam("assetPath") final String assetPath
+ ) {
+ final Optional sectionResult = sectionsUi
+ .findContentSection(sectionIdentifier);
+ if (!sectionResult.isPresent()) {
+ return sectionsUi.showContentSectionNotFound(sectionIdentifier);
+ }
+ final ContentSection section = sectionResult.get();
+
+ final Optional assetResult = assetRepo
+ .findByPath(section, assetPath);
+ if (!assetResult.isPresent()) {
+ return assetUi.showAssetNotFound(section, assetPath);
+ }
+ final Asset asset = assetResult.get();
+ if (!permissionChecker.isPermitted(AssetPrivileges.EDIT, asset)) {
+ return assetUi.showAccessDenied(section, asset, assetPath);
+ }
+
+ return String.format("redirect:%s", findEditStep(asset, section));
+ }
+
+ /**
+ * Helper method for finding the path fragment for the edit step of an
+ * asset.
+ *
+ * @param asset The asset.
+ *
+ * @return The path of the edit step of the asset.
+ *
+ */
+ private String findEditStep(
+ final Asset asset, final ContentSection section
+ ) {
+ final MvcAssetEditKit editKit = asset
+ .getClass()
+ .getAnnotation(MvcAssetEditKit.class);
+
+ final Class> step = editKit.editStep();
+ final Path pathAnnotation = step.getAnnotation(Path.class);
+ return pathAnnotation
+ .value()
+ .replace(
+ String.format(
+ "{%s}",
+ MvcAssetEditSteps.SECTION_IDENTIFIER_PATH_PARAM
+ ),
+ section.getLabel()
+ )
+ .replace(
+ String.format(
+ "/{%s}",
+ MvcAssetEditSteps.ASSET_PATH_PATH_PARAM
+ ),
+ assetManager.getAssetPath(asset)
+ );
+ }
+
+ /**
+ * Helper method for showing the "asset folder not found" page if there
+ * is no folder for the provided path.
+ *
+ * @param section The content section.
+ * @param folderPath The folder path.
+ *
+ * @return The template of the "document folder not found" page.
+ */
+ private String showAssetFolderNotFound(
+ final ContentSection section, final String folderPath
+ ) {
+ models.put("contentSection", section.getLabel());
+ models.put("folderPath", folderPath);
+
+ return "org/librecms/ui/contentsection/assetfolder/assetfolder-not-found.xhtml";
+ }
+ /**
+ * Helper method for showing the "asset type not available" page if the
+ * requested asset type is not available.
+ *
+ * @param section The content section.
+ * @param assetType The asset type.
+ *
+ * @return The template of the "asset type not found" page.
+ */
+ public String showAssetTypeNotFound(
+ final ContentSection section, final String assetType
+ ) {
+ models.put("contentSection", section.getLabel());
+ models.put("assetType", assetType);
+
+ return "org/librecms/ui/contentsection/assetfolder/asset-type-not-found.xhtml";
+ }
+
+ private String showCreateStepNotAvailable(
+ final ContentSection section,
+ final String folderPath,
+ final String assetType
+ ) {
+ models.put("contentSection", section.getLabel());
+ models.put("folderPath", folderPath);
+ models.put("assetType", assetType);
+
+ return "org/librecms/ui/contentsection/assetfolder/create-step-not-available.xhtml";
+ }
+
+
+ private CreateStepResult findCreateStep(
+ final String sectionIdentifier,
+ final String folderPath,
+ final String assetType
+ ) {
+ final Optional sectionResult = sectionsUi
+ .findContentSection(sectionIdentifier);
+ if (!sectionResult.isPresent()) {
+ return new CreateStepResult(
+ sectionsUi.showContentSectionNotFound(sectionIdentifier)
+ );
+ }
+ final ContentSection section = sectionResult.get();
+ sectionModel.setSection(section);
+
+ final Folder folder;
+ if (folderPath.isEmpty()) {
+ folder = section.getRootAssetsFolder();
+ } else {
+ final Optional folderResult = folderRepo
+ .findByPath(section, folderPath, FolderType.ASSETS_FOLDER
+ );
+ if (!folderResult.isPresent()) {
+ return new CreateStepResult(
+ showAssetFolderNotFound(section, folderPath)
+ );
+ }
+ folder = folderResult.get();
+ }
+
+ if (!assetPermissionsChecker.canCreateAssets(folder)) {
+ return new CreateStepResult(
+ sectionsUi.showAccessDenied(
+ "sectionidentifier", sectionIdentifier,
+ "folderPath", folderPath,
+ "step", defaultStepsMessageBundle.getMessage("create_step")
+ )
+ );
+ }
+
+ final Class> clazz;
+ try {
+ clazz = Class.forName(assetType);
+ } catch(ClassNotFoundException ex) {
+ return new CreateStepResult(
+ showAssetTypeNotFound(section, assetType)
+ );
+ }
+ @SuppressWarnings("unchecked")
+ final Class extends Asset> assetClass = (Class extends Asset>) clazz;
+
+ final Optional editKitResult = Optional.ofNullable(
+ assetClass.getDeclaredAnnotation(MvcAssetEditKit.class)
+ );
+ if (!editKitResult.isPresent()) {
+ return new CreateStepResult(
+ showCreateStepNotAvailable(section, folderPath, assetType)
+ );
+ }
+ final MvcAssetEditKit editKit = editKitResult.get();
+ final Class extends MvcAssetCreateStep>> createStepClass
+ = editKit.createStep();
+
+ final Instance extends MvcAssetCreateStep>> instance
+ = assetCreateSteps.select(createStepClass);
+ if (instance.isUnsatisfied() || instance.isAmbiguous()) {
+ return new CreateStepResult(
+ showCreateStepNotAvailable(section, folderPath, assetType)
+ );
+ }
+ final MvcAssetCreateStep extends Asset> createStep = instance.get();
+
+ createStep.setContentSection(section);
+ createStep.setFolder(folder);
+
+ return new CreateStepResult(createStep);
+ }
+
+ private class CreateStepResult {
+
+ private final MvcAssetCreateStep extends Asset> createStep;
+
+ private final boolean createStepAvailable;
+
+ private final String errorTemplate;
+
+ public CreateStepResult(
+ final MvcAssetCreateStep extends Asset> createStep
+ ) {
+ this.createStep = createStep;
+ createStepAvailable = true;
+ errorTemplate = null;
+ }
+
+ public CreateStepResult(final String errorTemplate) {
+ this.createStep = null;
+ createStepAvailable = false;
+ this.errorTemplate = errorTemplate;
+ }
+
+ public MvcAssetCreateStep extends Asset> getCreateStep() {
+ return createStep;
+ }
+
+ public boolean isCreateStepAvailable() {
+ return createStepAvailable;
+ }
+
+ public String getErrorTemplate() {
+ return errorTemplate;
+ }
+
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetCreateStep.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetCreateStep.java
new file mode 100644
index 000000000..f513f5fd8
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetCreateStep.java
@@ -0,0 +1,153 @@
+/*
+ * 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.contentsections.assets;
+
+import org.librecms.contentsection.Asset;
+import org.librecms.contentsection.ContentSection;
+import org.librecms.contentsection.Folder;
+
+import java.util.Map;
+
+/**
+ * A create step for an asset. Implmenting classes MUST be CDI beans (request
+ * scope is recommended). They are are retrieved by the {@link AssetController}
+ * using CDI. The {@link AssetController} will first call
+ * {@link #setContentSection(org.librecms.contentsection.ContentSection)} and {@link #setFolder(org.librecms.contentsection.Folder)
+ * } to provided the current current content section and folder. After that,
+ * dpending on the request method, either {@link #showCreateStep} or {@link #createAsset(java.util.Map)
+ * } will be called.
+ *
+ * In most cases, {@link AbstractMvcAssetCreateStep} should be used as base for
+ * implementations. {@link AbstractMvcAssetCreateStep} implements several common
+ * operations.
+ *
+ * @author Jens Pelzetter
+ * @param The asset type created by the create step.
+ */
+public interface MvcAssetCreateStep {
+
+ /**
+ * Return the template for the create step.
+ *
+ * @return
+ */
+ String showCreateStep();
+
+ String createAsset(Map formParams);
+
+ /**
+ * Should be set by the implementing class to indicate if the current user
+ * can create document in the current folder.
+ *
+ * @return
+ */
+ boolean getCanCreate();
+
+ /**
+ * The asset type generated by the create step described by an instance of
+ * this class.
+ *
+ * @return Asset type generated.
+ */
+ String getAssetType();
+
+ /**
+ * Localized description of the create step. The current locale as returned
+ * by {@link GlobalizationHelper#getNegotiatedLocale()} should be used to
+ * select the language variant to return.
+ *
+ * @return The localized description of the create step.
+ */
+ String getDescription();
+
+ /**
+ * Returns {@link ResourceBundle} providing the localized description of the
+ * create step.
+ *
+ * @return The {@link ResourceBundle} providing the localized description of
+ * the create step.
+ */
+ String getBundle();
+
+ /**
+ * The locales that can be used for documents.
+ *
+ * @return The locales that can be used for documents.
+ */
+ Map getAvailableLocales();
+
+ /**
+ * The current content section.
+ *
+ * @return The current content section.
+ */
+ ContentSection getContentSection();
+
+ /**
+ * Convinient method for getting the label of the current content section.
+ *
+ * @return The label of the current content section.
+ */
+ String getContentSectionLabel();
+
+ /**
+ * Convinient method for getting the title of the current content section.
+ *
+ * @return The title of the current content section for the current locale.
+ */
+ String getContentSectionTitle();
+
+ /**
+ * The current content section is provided by the
+ * {@link DocumentController}.
+ *
+ * @param section The current content section.
+ */
+ void setContentSection(final ContentSection section);
+
+ /**
+ * The parent folder of the new asset.
+ *
+ * @return The parent folder of the new asset.
+ */
+ Folder getFolder();
+
+ /**
+ * Gets the path the the parent folder of the new asset.
+ *
+ * @return The path of the parent folder of the new asset.
+ */
+ String getFolderPath();
+
+ /**
+ * The parent folder of the new asset is provided by the
+ * {@link DocumentController}.
+ *
+ * @param folder The parent folder of the new doucment.
+ */
+ void setFolder(final Folder folder);
+
+ /**
+ * Gets messages from the create step.
+ *
+ * @return
+ */
+ Map getMessages();
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditKit.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditKit.java
new file mode 100644
index 000000000..012a200c1
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditKit.java
@@ -0,0 +1,44 @@
+/*
+ * 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.contentsections.assets;
+
+import org.librecms.contentsection.Asset;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Provides the steps for creating, viewing, and editing an asset.
+ *
+ * This annotation can only be used on classes extending the {@link Asset}
+ * class.
+ *
+ * @author Jens Pelzetter
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MvcAssetEditKit {
+
+ Class extends MvcAssetCreateStep>> createStep();
+
+ Class> editStep();
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditStep.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditStep.java
new file mode 100644
index 000000000..37b982faa
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditStep.java
@@ -0,0 +1,68 @@
+/*
+ * 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.contentsections.assets;
+
+import org.librecms.contentsection.Asset;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Metadata of an edit step for assets.
+ *
+ * @author Jens Pelzetter
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MvcAssetEditStep {
+
+ /**
+ * The name of the resource bundle providing the localized values for
+ * {@link #labelKey} and {@link descriptionKey}.
+ *
+ * @return The resource bundle providing the localized labelKey and
+ * descriptionKey.
+ */
+ String bundle();
+
+ /**
+ * The key for the localized description of the step.
+ *
+ * @return The key for the localized description of the step.
+ */
+ String descriptionKey();
+
+ /**
+ * The key for the localized label of the authoring step..
+ *
+ * @return The key for the localized label of the authoring step...
+ */
+ String labelKey();
+
+ /**
+ * Edit steps only support a specific type, and all subtypes.
+ *
+ * @return The asset type supported by the edit step.
+ */
+
+ Class extends Asset> supportedAssetType();
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditSteps.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditSteps.java
new file mode 100644
index 000000000..66441bc20
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetEditSteps.java
@@ -0,0 +1,47 @@
+/*
+ * 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.contentsections.assets;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+public interface MvcAssetEditSteps {
+
+ public static final String PATH_PREFIX
+ = "/{sectionIdentifier}/assets/{assetPath:(.+)?}/@";
+
+ public static final String SECTION_IDENTIFIER_PATH_PARAM
+ = "sectionIdentifier";
+
+ public static final String ASSET_PATH_PATH_PARAM_NAME = "assetPath";
+
+ public static final String ASSET_PATH_PATH_PARAM
+ = ASSET_PATH_PATH_PARAM_NAME + ":(.+)?";
+
+ Set> getClasses();
+
+ default Set> getResourceClasses() {
+ return Collections.emptySet();
+ }
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetStepsConstants.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetStepsConstants.java
new file mode 100644
index 000000000..9c62ce76a
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/MvcAssetStepsConstants.java
@@ -0,0 +1,38 @@
+/*
+ * 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.contentsections.assets;
+
+/**
+ * Some constants shared by most asset create and edit steps.
+ *
+ * @author Jens Pelzetter
+ */
+public class MvcAssetStepsConstants {
+
+ private MvcAssetStepsConstants() {
+ // Nothing
+ }
+
+ /**
+ * Fully qualified name of the bundle provdiding texts shared by most asset
+ * create and edit steps.
+ */
+ public static final String BUNDLE = "org.librecms.ui.MvcAssetStepsBundle";
+
+}
diff --git a/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/SelectedAssetModel.java b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/SelectedAssetModel.java
new file mode 100644
index 000000000..9fc0678f2
--- /dev/null
+++ b/ccm-cms/src/main/java/org/librecms/ui/contentsections/assets/SelectedAssetModel.java
@@ -0,0 +1,164 @@
+/*
+ * 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.contentsections.assets;
+
+import org.libreccm.l10n.GlobalizationHelper;
+import org.libreccm.security.PermissionChecker;
+import org.libreccm.security.Shiro;
+import org.librecms.contentsection.Asset;
+import org.librecms.contentsection.AssetManager;
+import org.librecms.contentsection.Folder;
+import org.librecms.contentsection.FolderManager;
+import org.librecms.ui.contentsections.FolderBreadcrumbsModel;
+
+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.servlet.http.HttpServletRequest;
+
+/**
+ * Model/named bean providing data about the currently selected asset for
+ * several views.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("CmsSelectedAssetModel")
+public class SelectedAssetModel {
+
+ /**
+ * Checks if edit step classes have all required annotations.
+ */
+ @Inject
+ private AssetEditStepsValidator stepsValidator;
+
+ @Inject
+ private AssetManager assetManager;
+
+ @Inject
+ private FolderManager folderManager;
+
+ /**
+ * Used to retrieve some localized data.
+ */
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ @Inject
+ private HttpServletRequest request;
+
+ /**
+ * Used to check permissions
+ */
+ @Inject
+ private PermissionChecker permissionChecker;
+
+ /**
+ * Used to get the current user.
+ */
+ @Inject
+ private Shiro shiro;
+
+ /**
+ * The current asset.
+ */
+ private Asset asset;
+
+ /**
+ * The name of the current asset.
+ */
+ private String assetName;
+
+ /**
+ * The title of the current asset. This value is determined from
+ * {@link Asset#title} using {@link GlobalizationHelper#getValueFromLocalizedString(org.libreccm.l10n.LocalizedString)
+ * }.
+ */
+ private String assetTitle;
+
+ /**
+ * The path of the current asset.
+ */
+ private String assetPath;
+
+ /**
+ * The breadcrumb trail of the folder of the current item.
+ */
+ private List parentFolderBreadcrumbs;
+
+ public String getAssetName() {
+ return assetName;
+ }
+
+ public String getAssetTitle() {
+ return assetTitle;
+ }
+
+ public String getAssetPath() {
+ return assetPath;
+ }
+
+ public List getParentFolderBreadcrumbs() {
+ return Collections.unmodifiableList(parentFolderBreadcrumbs);
+ }
+
+ /**
+ * Sets the current asset and sets the properties of this model based on the
+ * asset.
+ *
+ * @param asset
+ */
+ void setAsset(final Asset asset) {
+ this.asset = Objects.requireNonNull(asset);
+ assetName = asset.getDisplayName();
+ assetTitle = globalizationHelper.getValueFromLocalizedString(
+ asset.getTitle()
+ );
+ assetPath = assetManager.getAssetPath(asset).substring(1); // Without leasding slash.
+ parentFolderBreadcrumbs = assetManager
+ .getAssetFolders(asset)
+ .stream()
+ .map(this::buildFolderBreadcrumbsModel)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Helper method for building the breadcrumb trail for the folder of the
+ * current item.
+ *
+ * @param folder The folder of the current item.
+ *
+ * @return The breadcrumb trail of the folder.
+ */
+ private FolderBreadcrumbsModel buildFolderBreadcrumbsModel(
+ final Folder folder
+ ) {
+ final FolderBreadcrumbsModel model = new FolderBreadcrumbsModel();
+ model.setCurrentFolder(false);
+ model.setPath(folderManager.getFolderPath(folder));
+ model.setPathToken(folder.getName());
+ return model;
+ }
+
+}