diff --git a/ccm-bundle-devel-wildfly-web/src/main/resources/log4j2.xml b/ccm-bundle-devel-wildfly-web/src/main/resources/log4j2.xml index 88e77939f..47ce2c010 100644 --- a/ccm-bundle-devel-wildfly-web/src/main/resources/log4j2.xml +++ b/ccm-bundle-devel-wildfly-web/src/main/resources/log4j2.xml @@ -36,6 +36,9 @@ + + diff --git a/ccm-cms/src/main/java/com/arsdigita/cms/ui/assets/AssetFolderBrowserController.java b/ccm-cms/src/main/java/com/arsdigita/cms/ui/assets/AssetFolderBrowserController.java index 37e0bcb17..3e9490db2 100644 --- a/ccm-cms/src/main/java/com/arsdigita/cms/ui/assets/AssetFolderBrowserController.java +++ b/ccm-cms/src/main/java/com/arsdigita/cms/ui/assets/AssetFolderBrowserController.java @@ -18,7 +18,6 @@ */ package com.arsdigita.cms.ui.assets; -import com.arsdigita.cms.ui.folder.FolderBrowser; import com.arsdigita.kernel.KernelConfig; import org.libreccm.categorization.Category; @@ -29,13 +28,10 @@ import org.librecms.CmsConstants; import org.librecms.assets.AssetTypeInfo; import org.librecms.assets.AssetTypesManager; import org.librecms.contentsection.Asset; -import org.librecms.contentsection.ContentType; import org.librecms.contentsection.Folder; -import org.librecms.contenttypes.ContentTypeInfo; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -53,10 +49,18 @@ import javax.persistence.criteria.Order; import javax.persistence.criteria.Path; import javax.persistence.criteria.Root; import javax.transaction.Transactional; + import org.libreccm.core.CcmObject; +import org.librecms.contentsection.AssetManager; import org.librecms.contentsection.AssetRepository; +import org.librecms.contentsection.FolderManager; import org.librecms.contentsection.FolderRepository; +import java.util.Collections; +import java.util.Optional; + +import static org.librecms.CmsConstants.*; + /** * * @author Jens Pelzetter @@ -79,9 +83,15 @@ public class AssetFolderBrowserController { @Inject private FolderRepository folderRepo; + @Inject + private FolderManager folderManager; + @Inject private AssetRepository assetRepo; + @Inject + private AssetManager assetManager; + @Inject private GlobalizationHelper globalizationHelper; @@ -96,7 +106,7 @@ public class AssetFolderBrowserController { @PostConstruct private void init() { final KernelConfig kernelConfig = confManager.findConfiguration( - KernelConfig.class); + KernelConfig.class); defaultLocale = kernelConfig.getDefaultLocale(); } @@ -107,14 +117,15 @@ public class AssetFolderBrowserController { final int firstResult, final int maxResults) { final List subFolders = findSubFolders(folder, + "%", orderBy, orderDirection, firstResult, maxResults); final List subFolderRows = subFolders - .stream() - .map(subFolder -> buildRow(subFolder)) - .collect(Collectors.toList()); + .stream() + .map(subFolder -> buildRow(subFolder)) + .collect(Collectors.toList()); if (subFolders.size() > maxResults) { return subFolderRows; @@ -123,14 +134,15 @@ public class AssetFolderBrowserController { final int firstAsset = firstResult - subFolders.size(); final List assets = findAssetsInFolder(folder, + "%", orderBy, orderDirection, firstAsset, maxAssets); final List assetRows = assets - .stream() - .map(asset -> buildRow(asset)) - .collect(Collectors.toList()); + .stream() + .map(asset -> buildRow(asset)) + .collect(Collectors.toList()); final List rows = new ArrayList<>(); rows.addAll(subFolderRows); @@ -143,7 +155,15 @@ public class AssetFolderBrowserController { @Transactional(Transactional.TxType.REQUIRED) protected long countObjects(final Folder folder) { + return countObjects(folder, "%"); + + } + + @Transactional(Transactional.TxType.REQUIRED) + protected long countObjects(final Folder folder, final String filterTerm) { + Objects.requireNonNull(folder); + Objects.requireNonNull(filterTerm); final CriteriaBuilder builder = entityManager.getCriteriaBuilder(); CriteriaQuery criteriaQuery = builder.createQuery(Long.class); @@ -152,21 +172,23 @@ public class AssetFolderBrowserController { criteriaQuery = criteriaQuery.select(builder.count(from)); final List subFolders = findSubFolders( - folder, - AssetFolderBrowser.SORT_KEY_NAME, - AssetFolderBrowser.SORT_ACTION_UP, - -1, - -1); + folder, + filterTerm, + AssetFolderBrowser.SORT_KEY_NAME, + AssetFolderBrowser.SORT_ACTION_UP, + -1, + -1); final List assets = findAssetsInFolder( - folder, - AssetFolderBrowser.SORT_KEY_NAME, - AssetFolderBrowser.SORT_ACTION_UP, - -1, - -1); - + folder, + filterTerm, + AssetFolderBrowser.SORT_KEY_NAME, + AssetFolderBrowser.SORT_ACTION_UP, + -1, + -1); + if (subFolders.isEmpty() && assets.isEmpty()) { return 0; - } else if(subFolders.isEmpty() && !assets.isEmpty()) { + } else if (subFolders.isEmpty() && !assets.isEmpty()) { criteriaQuery = criteriaQuery.where(from.in(assets)); } else if (!subFolders.isEmpty() && assets.isEmpty()) { criteriaQuery = criteriaQuery.where(from.in(subFolders)); @@ -180,6 +202,218 @@ public class AssetFolderBrowserController { } + @Transactional(Transactional.TxType.REQUIRED) + protected void copyObjects(final Folder targetFolder, + final String[] objectIds) { + + Objects.requireNonNull(targetFolder); + Objects.requireNonNull(objectIds); + + for (final String objectId : objectIds) { + if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) { + copyFolder(targetFolder, + Long.parseLong(objectId.substring( + FOLDER_BROWSER_KEY_PREFIX_FOLDER.length()))); + } else if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_ASSET)) { + copyAsset(targetFolder, + Long.parseLong(objectId.substring( + FOLDER_BROWSER_KEY_PREFIX_ITEM.length()))); + } else { + throw new IllegalArgumentException(String.format( + "ID '%s' does not start with '%s' or '%s'.", + objectId, + FOLDER_BROWSER_KEY_PREFIX_FOLDER, + FOLDER_BROWSER_KEY_PREFIX_ASSET)); + } + } + + } + + private void copyFolder(final Folder targetFolder, + final long folderId) { + + Objects.requireNonNull(targetFolder); + + final Folder folder = folderRepo.findById(folderId) + .orElseThrow(() -> new IllegalArgumentException(String.format( + "No folder with ID %d in the database. " + + "Where did that ID come from?", + folderId))); + + folderManager.copyFolder(folder, targetFolder); + + } + + private void copyAsset(final Folder targetFolder, + final long assetId) { + + Objects.requireNonNull(targetFolder); + + final Asset asset = assetRepo + .findById(assetId) + .orElseThrow(() -> new IllegalArgumentException(String.format( + "No asset ith ID %d in the database. Where did that ID come from?", + assetId))); + + assetManager.copy(asset, targetFolder); + } + + @Transactional(Transactional.TxType.REQUIRED) + public void moveObjects(final Folder targetFolder, + final String[] objectIds) { + + Objects.requireNonNull(targetFolder); + Objects.requireNonNull(objectIds); + + for (final String objectId : objectIds) { + if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) { + moveFolder(targetFolder, + Long.parseLong(objectId.substring( + FOLDER_BROWSER_KEY_PREFIX_FOLDER.length()))); + } else if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_ASSET)) { + moveAsset(targetFolder, + Long.parseLong(objectId.substring( + FOLDER_BROWSER_KEY_PREFIX_ASSET.length()))); + } else { + throw new IllegalArgumentException(String.format( + "ID '%s' does not start with '%s' or '%s'.", + objectId, + FOLDER_BROWSER_KEY_PREFIX_FOLDER, + FOLDER_BROWSER_KEY_PREFIX_ASSET)); + } + } + } + + private void moveFolder(final Folder targetFolder, final long folderId) { + + Objects.requireNonNull(targetFolder); + + final Folder folder = folderRepo.findById(folderId) + .orElseThrow(() -> new IllegalArgumentException(String.format( + "No folder with ID %d in the database. " + + "Where did that ID come from?", + folderId))); + + folderManager.moveFolder(folder, targetFolder); + } + + private void moveAsset(final Folder targetFolder, final long assetId) { + + Objects.requireNonNull(targetFolder); + + final Asset asset = assetRepo + .findById(assetId) + .orElseThrow(() -> new IllegalArgumentException(String.format( + "No asset with ID %d in the database. Where did that ID come from?", + assetId))); + + assetManager.move(asset, targetFolder); + } + + @Transactional(Transactional.TxType.REQUIRED) + protected List createInvalidTargetsList(final List sources) { + + Objects.requireNonNull(sources); + + final List sourceFolderIds = sources + .stream() + .filter(source -> source.startsWith( + FOLDER_BROWSER_KEY_PREFIX_FOLDER)) + .collect(Collectors.toList()); + final List parentFolderIds = sourceFolderIds + .stream() + .map(sourceFolderId -> findParentFolderId(sourceFolderId)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + final List> subFolderIds = sourceFolderIds + .stream() + .map(sourceFolderId -> findSubFolderIds(sourceFolderId)) + .collect(Collectors.toList()); + + final List invalidTargetIds = new ArrayList<>(); + invalidTargetIds.addAll(sourceFolderIds); + invalidTargetIds.addAll(parentFolderIds); + for (final List subFolderIdList : subFolderIds) { + invalidTargetIds.addAll(subFolderIdList); + } + + return invalidTargetIds; + + } + + private Optional findParentFolderId(final String folderId) { + + Objects.requireNonNull(folderId); + + if (!folderId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) { + throw new IllegalArgumentException(String.format( + "Provided string '%s' is not an ID of a folder.", + folderId)); + } + + final long objectId = Long.parseLong(folderId.substring( + FOLDER_BROWSER_KEY_PREFIX_FOLDER.length())); + final Folder folder = folderRepo.findById(objectId) + .orElseThrow(() -> new IllegalArgumentException(String.format( + "No folder with ID %d found in database. " + + "Where did that ID come form?", + objectId))); + final Optional parentFolder = folderManager.getParentFolder( + folder); + if (parentFolder.isPresent()) { + return Optional.empty(); + } else { + return Optional.ofNullable(String.format( + "%s%d", + FOLDER_BROWSER_KEY_PREFIX_FOLDER, + parentFolder.get().getObjectId())); + } + } + + private List findSubFolderIds(final String folderId) { + + Objects.requireNonNull(folderId); + + if (!folderId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) { + throw new IllegalArgumentException(String.format( + "Provided string '%s' is not the ID of a folder.", + folderId)); + } + + final long objectId = Long.parseLong(folderId.substring( + FOLDER_BROWSER_KEY_PREFIX_FOLDER.length())); + final Folder folder = folderRepo.findById(objectId) + .orElseThrow(() -> new IllegalArgumentException(String.format( + "No folder with ID %d found in database. " + + "Where did that ID come form?", + objectId))); + return findSubFolders(folder) + .stream() + .map(subFolder -> String.format("%s%d", + FOLDER_BROWSER_KEY_PREFIX_FOLDER, + subFolder.getObjectId())) + .collect(Collectors.toList()); + } + + private List findSubFolders(final Folder folder) { + + Objects.requireNonNull(folder); + + if (folder.getSubFolders() == null + || folder.getSubFolders().isEmpty()) { + return Collections.emptyList(); + } + + final List subFolders = new ArrayList<>(); + for (final Folder subFolder : folder.getSubFolders()) { + subFolders.add(subFolder); + subFolders.addAll(findSubFolders(subFolder)); + } + + return subFolders; + } + /** * Called by the {@link AssetFolderBrowser} to delete an object. * @@ -192,21 +426,21 @@ public class AssetFolderBrowserController { if (objectId.startsWith("folder-")) { final long folderId = Long.parseLong( - objectId.substring("folder-".length())); + objectId.substring("folder-".length())); folderRepo - .findById(folderId) - .ifPresent(folderRepo::delete); + .findById(folderId) + .ifPresent(folderRepo::delete); } else if (objectId.startsWith("asset-")) { final long assetId = Long.parseLong( - objectId.substring("asset-".length())); + objectId.substring("asset-".length())); assetRepo - .findById(assetId) - .ifPresent(assetRepo::delete); + .findById(assetId) + .ifPresent(assetRepo::delete); } else { throw new IllegalArgumentException( - "The objectId is expected to start with 'folder-' or 'item.'."); + "The objectId is expected to start with 'folder-' or 'item.'."); } } @@ -218,15 +452,15 @@ public class AssetFolderBrowserController { row.setObjectUuid(folder.getUuid()); row.setName(folder.getName()); if (folder.getTitle().hasValue(globalizationHelper - .getNegotiatedLocale())) { + .getNegotiatedLocale())) { row.setTitle(folder.getTitle().getValue(globalizationHelper - .getNegotiatedLocale())); + .getNegotiatedLocale())); } else { row.setTitle(folder.getTitle().getValue(defaultLocale)); } row.setFolder(true); row.setDeletable(!categoryManager.hasSubCategories(folder) - && !categoryManager.hasObjects(folder)); + && !categoryManager.hasObjects(folder)); return row; } @@ -239,23 +473,26 @@ public class AssetFolderBrowserController { row.setObjectUuid(asset.getUuid()); row.setName(asset.getDisplayName()); if (asset.getTitle().hasValue(globalizationHelper - .getNegotiatedLocale())) { + .getNegotiatedLocale())) { row.setTitle(asset.getTitle().getValue(globalizationHelper - .getNegotiatedLocale())); + .getNegotiatedLocale())); } else { row.setTitle(asset.getTitle().getValue(defaultLocale)); } final AssetTypeInfo typeInfo = typesManager - .getAssetTypeInfo(asset.getClass()); + .getAssetTypeInfo(asset.getClass()); row.setTypeLabelBundle(typeInfo.getLabelBundle()); row.setTypeLabelKey(typeInfo.getLabelKey()); row.setFolder(false); + row.setDeletable(!assetManager.isAssetInUse(asset)); + return row; } private List findSubFolders(final Folder folder, + final String filterTerm, final String orderBy, final String orderDirection, final int firstResult, @@ -264,26 +501,30 @@ public class AssetFolderBrowserController { final CriteriaBuilder builder = entityManager.getCriteriaBuilder(); final CriteriaQuery criteria = builder - .createQuery(Folder.class); + .createQuery(Folder.class); final Root from = criteria.from(Folder.class); final Order order; if (AssetFolderBrowser.SORT_KEY_NAME.equals(orderBy) - && AssetFolderBrowser.SORT_ACTION_DOWN. - equals(orderDirection)) { + && AssetFolderBrowser.SORT_ACTION_DOWN. + equals(orderDirection)) { order = builder.desc(from.get("name")); } else { order = builder.asc(from.get("name")); } final TypedQuery query = entityManager - .createQuery( - criteria.where( - builder. - equal(from.get("parentCategory"), folder) - ) - .orderBy(order) - ); + .createQuery( + criteria.where( + builder.and( + builder.equal(from.get("parentCategory"), + folder), + builder.like(builder.lower(from.get("name")), + filterTerm) + ) + ) + .orderBy(order) + ); if (firstResult >= 0) { query.setFirstResult(firstResult); @@ -297,6 +538,7 @@ public class AssetFolderBrowserController { } private List findAssetsInFolder(final Folder folder, + final String filterTerm, final String orderBy, final String orderDirection, final int firstResult, @@ -332,18 +574,21 @@ public class AssetFolderBrowserController { } final TypedQuery query = entityManager - .createQuery( - criteria.select(fromAsset) - .where( - builder.and( - builder.equal(join.get( - "category"), folder), - builder.equal(join.get("type"), - CmsConstants.CATEGORIZATION_TYPE_FOLDER) - ) - ) - .orderBy(order) - ); + .createQuery( + criteria.select(fromAsset) + .where( + builder.and( + builder.equal(join.get( + "category"), folder), + builder.equal(join.get("type"), + CmsConstants.CATEGORIZATION_TYPE_FOLDER), + builder.like(builder.lower(fromAsset.get( + "displayName")), + filterTerm) + ) + ) + .orderBy(order) + ); if (firstResult >= 0) { query.setFirstResult(firstResult); diff --git a/ccm-cms/src/main/java/com/arsdigita/cms/ui/assets/AssetPane.java b/ccm-cms/src/main/java/com/arsdigita/cms/ui/assets/AssetPane.java index 446b5572d..abfb073c2 100644 --- a/ccm-cms/src/main/java/com/arsdigita/cms/ui/assets/AssetPane.java +++ b/ccm-cms/src/main/java/com/arsdigita/cms/ui/assets/AssetPane.java @@ -19,27 +19,35 @@ package com.arsdigita.cms.ui.assets; import com.arsdigita.bebop.ActionLink; +import com.arsdigita.bebop.BoxPanel; import com.arsdigita.bebop.Component; +import com.arsdigita.bebop.ControlLink; import com.arsdigita.bebop.Form; +import com.arsdigita.bebop.FormData; import com.arsdigita.bebop.FormProcessException; import com.arsdigita.bebop.Label; import com.arsdigita.bebop.Page; import com.arsdigita.bebop.PageState; import com.arsdigita.bebop.Paginator; +import com.arsdigita.bebop.RequestLocal; import com.arsdigita.bebop.Resettable; +import com.arsdigita.bebop.SaveCancelSection; import com.arsdigita.bebop.SegmentedPanel; import com.arsdigita.bebop.SimpleContainer; import com.arsdigita.bebop.SingleSelectionModel; import com.arsdigita.bebop.Table; import com.arsdigita.bebop.Text; +import com.arsdigita.bebop.Tree; import com.arsdigita.bebop.event.ActionEvent; import com.arsdigita.bebop.event.ActionListener; import com.arsdigita.bebop.event.FormProcessListener; import com.arsdigita.bebop.event.FormSectionEvent; import com.arsdigita.bebop.event.FormSubmissionListener; +import com.arsdigita.bebop.event.FormValidationListener; import com.arsdigita.bebop.event.PrintEvent; import com.arsdigita.bebop.event.PrintListener; import com.arsdigita.bebop.form.CheckboxGroup; +import com.arsdigita.bebop.form.FormErrorDisplay; import com.arsdigita.bebop.form.Option; import com.arsdigita.bebop.form.SingleSelect; import com.arsdigita.bebop.form.Submit; @@ -47,6 +55,7 @@ import com.arsdigita.bebop.parameters.ArrayParameter; import com.arsdigita.bebop.parameters.StringParameter; import com.arsdigita.bebop.table.TableCellRenderer; import com.arsdigita.bebop.table.TableColumn; +import com.arsdigita.bebop.tree.TreeCellRenderer; import com.arsdigita.cms.CMS; import com.arsdigita.cms.ui.BaseTree; import com.arsdigita.cms.ui.folder.FolderCreateForm; @@ -59,6 +68,8 @@ import com.arsdigita.globalization.GlobalizedMessage; import com.arsdigita.toolbox.ui.ActionGroup; import com.arsdigita.toolbox.ui.LayoutPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.libreccm.categorization.Category; import org.libreccm.cdi.utils.CdiUtil; import org.librecms.CmsConstants; @@ -68,8 +79,21 @@ import org.librecms.contentsection.Folder; import java.util.List; import org.arsdigita.cms.CMSConfig; +import org.libreccm.categorization.CategoryManager; +import org.libreccm.core.CcmObject; +import org.libreccm.core.UnexpectedErrorException; +import org.libreccm.security.PermissionChecker; +import org.librecms.contentsection.Asset; +import org.librecms.contentsection.AssetManager; +import org.librecms.contentsection.AssetRepository; +import org.librecms.contentsection.FolderManager; +import org.librecms.contentsection.FolderRepository; +import org.librecms.contentsection.privileges.ItemPrivileges; -import javax.swing.CellRendererPane; +import java.util.Arrays; +import java.util.Objects; + +import static org.librecms.CmsConstants.*; /** * @@ -77,6 +101,8 @@ import javax.swing.CellRendererPane; */ public class AssetPane extends LayoutPanel implements Resettable { + private static final Logger LOGGER = LogManager.getLogger(AssetPane.class); + public static final String SET_FOLDER = "set_folder"; private static final String SOURCES_PARAM = "sources"; private static final String ACTION_PARAM = "action"; @@ -89,10 +115,14 @@ public class AssetPane extends LayoutPanel implements Resettable { private final FolderRequestLocal folderRequestLocal; private final ArrayParameter sourcesParameter = new ArrayParameter( new StringParameter(SOURCES_PARAM)); + private final StringParameter actionParameter = new StringParameter( + ACTION_PARAM); private AssetFolderBrowser folderBrowser; + private Form browserForm; private SingleSelect actionSelect; private Submit actionSubmit; + private TargetSelector targetSelector; private SegmentedPanel.Segment browseSegment; private SegmentedPanel.Segment currentFolderSegment; @@ -145,8 +175,8 @@ public class AssetPane extends LayoutPanel implements Resettable { final SegmentedPanel panel = new SegmentedPanel(); browseSegment = panel.addSegment(); - final Form browserForm = new Form("assetFolderBrowser", - new SimpleContainer()); + browserForm = new Form("assetFolderBrowser", + new SimpleContainer()); browserForm.setMethod(Form.GET); folderBrowser = new AssetFolderBrowser(folderSelectionModel); final Paginator paginator = new Paginator( @@ -186,7 +216,7 @@ public class AssetPane extends LayoutPanel implements Resettable { new GlobalizedMessage( "cms.ui.folder.edit_selection", CmsConstants.CMS_FOLDER_BUNDLE))); - actionSelect = new SingleSelect(ACTION_PARAM); + actionSelect = new SingleSelect(actionParameter); actionSelect.addOption( new Option(COPY, new Label(new GlobalizedMessage( @@ -203,8 +233,68 @@ public class AssetPane extends LayoutPanel implements Resettable { new GlobalizedMessage("cms.ui.folder.go", CmsConstants.CMS_FOLDER_BUNDLE)); actionFormContainer.add(actionSubmit); + browserForm.addProcessListener(new FormProcessListener() { + + @Override + public void process(final FormSectionEvent event) + throws FormProcessException { + + final PageState state = event.getPageState(); + + moveCopyMode(state); + + } + + }); browserForm.add(actionFormContainer); + targetSelector = new TargetSelector(); + targetSelector.addProcessListener(new FormProcessListener() { + + @Override + public void process(final FormSectionEvent event) + throws FormProcessException { + + final PageState state = event.getPageState(); + + browseMode(state); + targetSelector.setVisible(state, false); + + final Folder folder = targetSelector.getTarget(state); + final String[] objectIds = getSources(state); + + if (isCopy(state)) { + copyObjects(folder, objectIds); + } else if (isMove(state)) { + moveObjects(folder, objectIds); + } + + reset(state); + } + + }); + targetSelector.addValidationListener( + new TargetSelectorValidationListener()); + targetSelector.addSubmissionListener(new FormSubmissionListener() { + + @Override + public void submitted(final FormSectionEvent event) + throws FormProcessException { + + final PageState state = event.getPageState(); + + if (targetSelector.isCancelled(state)) { + reset(state); + browseMode(state); + throw new FormProcessException(new GlobalizedMessage( + "cms.ui.folder.cancelled", + CmsConstants.CMS_FOLDER_BUNDLE)); + } + } + + }); + browseSegment.add(targetSelector); + // browseSegment.add(paginator); // browseSegment.add(folderBrowser); browseSegment.add(browserForm); @@ -352,22 +442,44 @@ public class AssetPane extends LayoutPanel implements Resettable { } protected void browseMode(final PageState state) { + tree.setVisible(state, true); browseSegment.setVisible(state, true); + folderBrowser.setVisible(state, true); + browserForm.setVisible(state, true); + targetSelector.setVisible(state, false); actionsSegment.setVisible(state, true); newFolderSegment.setVisible(state, false); editFolderSegment.setVisible(state, false); } + protected void moveCopyMode(final PageState state) { + tree.setVisible(state, false); + browseSegment.setVisible(state, true); + folderBrowser.setVisible(state, false); + browserForm.setVisible(state, false); + targetSelector.setVisible(state, true); + actionsSegment.setVisible(state, false); + newFolderSegment.setVisible(state, false); + editFolderSegment.setVisible(state, false); + targetSelector.expose(state); + } + protected void newFolderMode(final PageState state) { + tree.setVisible(state, false); browseSegment.setVisible(state, false); + folderBrowser.setVisible(state, false); + browserForm.setVisible(state, false); + targetSelector.setVisible(state, false); actionsSegment.setVisible(state, false); newFolderSegment.setVisible(state, true); editFolderSegment.setVisible(state, false); } protected void editFolderMode(final PageState state) { + tree.setVisible(state, false); browseSegment.setVisible(state, false); + targetSelector.setVisible(state, false); actionsSegment.setVisible(state, false); newFolderSegment.setVisible(state, false); editFolderSegment.setVisible(state, true); @@ -381,10 +493,17 @@ public class AssetPane extends LayoutPanel implements Resettable { page.addActionListener(new TreeListener()); page.addActionListener(new FolderListener()); + page.setVisibleDefault(tree, true); page.setVisibleDefault(browseSegment, true); + page.setVisibleDefault(folderBrowser, true); + page.setVisibleDefault(browserForm, true); + page.setVisibleDefault(targetSelector, false); page.setVisibleDefault(actionsSegment, true); page.setVisibleDefault(newFolderSegment, false); page.setVisibleDefault(editFolderSegment, false); + + page.addComponentStateParam(this, actionParameter); + page.addComponentStateParam(this, sourcesParameter); } @Override @@ -394,6 +513,50 @@ public class AssetPane extends LayoutPanel implements Resettable { folderBrowser.getPaginator().reset(state); + state.setValue(actionParameter, null); + state.setValue(sourcesParameter, null); + + } + + private String[] getSources(final PageState state) { + + final String[] result = (String[]) state.getValue(sourcesParameter); + + if (result == null) { + return new String[0]; + } else { + return result; + } + } + + protected final boolean isMove(final PageState state) { + return MOVE.equals(getAction(state)); + } + + protected final boolean isCopy(final PageState state) { + return COPY.equals(getAction(state)); + } + + private String getAction(final PageState state) { + return (String) state.getValue(actionParameter); + } + + protected void moveObjects(final Folder target, final String[] objectIds) { + + final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); + final AssetFolderBrowserController controller = cdiUtil.findBean( + AssetFolderBrowserController.class); + + controller.moveObjects(target, objectIds); + } + + protected void copyObjects(final Folder target, final String[] objectIds) { + + final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); + final AssetFolderBrowserController controller = cdiUtil.findBean( + AssetFolderBrowserController.class); + + controller.copyObjects(target, objectIds); } private final class FolderListener implements ActionListener { @@ -452,4 +615,385 @@ public class AssetPane extends LayoutPanel implements Resettable { } + private class TargetSelector extends Form implements Resettable { + + private final FolderSelectionModel targetFolderModel; + private final AssetFolderTree folderTree; + private final Submit cancelButton; + + public TargetSelector() { + super("targetSelector", new BoxPanel()); + setMethod(GET); + targetFolderModel = new FolderSelectionModel("target") { + + @Override + protected Long getRootFolderID(final PageState state) { + final ContentSection section = CMS + .getContext() + .getContentSection(); + return section.getRootAssetsFolder().getObjectId(); + } + + }; + folderTree = new AssetFolderTree(targetFolderModel); + + folderTree.setCellRenderer(new FolderTreeCellRenderer()); + + final Label label = new Label(new PrintListener() { + + @Override + public void prepare(final PrintEvent event) { + + final PageState state = event.getPageState(); + final Label label = (Label) event.getTarget(); + final int numberOfItems = getSources(state).length; + final Category folder = (Category) folderSelectionModel + .getSelectedObject(state); + final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); + final CategoryManager categoryManager = cdiUtil + .findBean(CategoryManager.class); + + if (isMove(state)) { + label.setLabel(new GlobalizedMessage( + "cms.ui.folder.move", + CmsConstants.CMS_FOLDER_BUNDLE, + new Object[]{numberOfItems, + categoryManager.getCategoryPath(folder)})); + } else if (isCopy(state)) { + label.setLabel(new GlobalizedMessage( + "cms.ui.folder.copy", + CMS_BUNDLE, + new Object[]{numberOfItems, + categoryManager.getCategoryPath( + folder)})); + } + } + + }); + + label.setOutputEscaping(false); + add(label); + add(folderTree); + add(new FormErrorDisplay(this)); + final SaveCancelSection saveCancelSection = new SaveCancelSection(); + cancelButton = saveCancelSection.getCancelButton(); + add(saveCancelSection); + } + + @Override + public void register(final Page page) { + super.register(page); + page.addComponentStateParam(this, targetFolderModel + .getStateParameter()); + } + + public void expose(final PageState state) { + + final Folder folder = folderSelectionModel.getSelectedObject(state); + targetFolderModel.clearSelection(state); + if (folder != null) { + final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); + final FolderManager folderManager = cdiUtil.findBean( + FolderManager.class); + if (!folderManager.getParentFolder(folder).isPresent()) { + folderTree.expand(Long.toString(folder.getObjectId()), + state); + } else { + final List parents = folderManager + .getParentFolders(folder); + parents + .stream() + .map(parent -> Long.toString(parent.getObjectId())) + .forEach(folderId -> folderTree.expand(folderId, state)); + } + } + } + + @Override + public void reset(final PageState state) { + folderTree.clearSelection(state); + state.setValue(folderTree.getSelectionModel().getStateParameter(), + null); + } + + public Folder getTarget(final PageState state) { + return targetFolderModel.getSelectedObject(state); + } + + public boolean isCancelled(final PageState state) { + return cancelButton.isSelected(state); + } + + } + + private class FolderTreeCellRenderer implements TreeCellRenderer { + + private final RequestLocal invalidFoldersRequestLocal + = new RequestLocal(); + + /** + * Render the folders appropriately. The selected folder is a bold + * label. Invalid folders are plain labels. Unselected, valid folders + * are control links. Invalid folders are: the parent folder of the + * sources, any of the sources, and any subfolders of the sources. + */ + @Override + @SuppressWarnings("unchecked") + public Component getComponent(final Tree tree, + final PageState state, + final Object value, + final boolean isSelected, + final boolean isExpanded, + final boolean isLeaf, + final Object key) { + + // Get the list of invalid folders once per request. + final List invalidFolders; + + if (invalidFoldersRequestLocal.get(state) == null) { + final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); + final AssetFolderBrowserController controller = cdiUtil + .findBean(AssetFolderBrowserController.class); + invalidFolders = controller.createInvalidTargetsList( + Arrays.asList(getSources(state))); + invalidFoldersRequestLocal.set(state, invalidFolders); + } else { + invalidFolders = (List) invalidFoldersRequestLocal + .get(state); + } + final Label label = new Label(value.toString()); + + if (invalidFolders.contains(String.format( + FOLDER_BROWSER_KEY_PREFIX_FOLDER + "%s", key))) { + return label; + } + + // Bold if selected + if (isSelected) { + label.setFontWeight(Label.BOLD); + return label; + } + + return new ControlLink(label); + } + + } + + private class TargetSelectorValidationListener + implements FormValidationListener { + + @Override + public void validate(final FormSectionEvent event) + throws FormProcessException { + + final PageState state = event.getPageState(); + + if (getSources(state).length <= 0) { + throw new IllegalStateException("No source items specified"); + } + + final Folder target = targetSelector.getTarget(state); + final FormData data = event.getFormData(); + if (target == null) { + data.addError(new GlobalizedMessage( + "cms.ui.folder.need_select_target_folder", + CmsConstants.CMS_FOLDER_BUNDLE)); + //If the target is null, we can skip the rest of the checks + return; + } + + if (target.equals(folderSelectionModel.getSelectedObject(state))) { + data.addError(new GlobalizedMessage( + "cms.ui.folder.not_within_same_folder", + CmsConstants.CMS_FOLDER_BUNDLE)); + } + + // check create item permission + final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); + final PermissionChecker permissionChecker = cdiUtil.findBean( + PermissionChecker.class); + if (!permissionChecker.isPermitted( + ItemPrivileges.CREATE_NEW, target)) { + data.addError("cms.ui.folder.no_permission_for_item", + CmsConstants.CMS_FOLDER_BUNDLE); + } + + for (String source : getSources(state)) { + + validateObject(source, target, state, data); + + } + } + + private void validateObject(final String objectId, + final Folder target, + final PageState state, + final FormData data) { + + Objects.requireNonNull(objectId, "objectId can't be null."); + + final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); + final FolderRepository folderRepo = cdiUtil + .findBean(FolderRepository.class); + final AssetRepository assetRepo = cdiUtil + .findBean(AssetRepository.class); + final AssetManager assetManager = cdiUtil + .findBean(AssetManager.class); + final AssetFolderBrowserController controller = cdiUtil + .findBean(AssetFolderBrowserController.class); + final FolderManager folderManager = cdiUtil + .findBean(FolderManager.class); + final PermissionChecker permissionChecker = cdiUtil.findBean( + PermissionChecker.class); + + final CcmObject object; + final String name; + if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_FOLDER)) { + + final long folderId = Long.parseLong(objectId.substring( + FOLDER_BROWSER_KEY_PREFIX_FOLDER.length())); + final Folder folder = folderRepo.findById(folderId).orElseThrow( + () -> new IllegalArgumentException(String.format( + "No folder with id %d in database.", folderId))); + + name = folder.getName(); + + //Check if folder or subfolder contains in use assets + if (isMove(state)) { + final FolderManager.FolderIsMovable movable = folderManager + .folderIsMovable(folder, target); + switch (movable) { + case DIFFERENT_SECTIONS: + addErrorMessage(data, + "cms.ui.folder.different_sections", + name); + break; + case HAS_IN_USE_ASSETS: + addErrorMessage(data, + "cms.ui.folder.has_in_use_assets", + name); + break; + case DIFFERENT_TYPES: + addErrorMessage(data, + "cms.ui.folder.different_folder_types", + name); + break; + case IS_ROOT_FOLDER: + addErrorMessage(data, + "cms.ui.folder.is_root_folder", + name); + break; + case SAME_FOLDER: + addErrorMessage(data, + "cms.ui.folder.same_folder", + name); + break; + case YES: + //Nothing + break; + default: + throw new UnexpectedErrorException(String.format( + "Unknown state '%s' for '%s'.", + movable, + FolderManager.FolderIsMovable.class.getName())); + } + } + + object = folder; + } else if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_ASSET)) { + final long assetId = Long.parseLong(objectId.substring( + FOLDER_BROWSER_KEY_PREFIX_ASSET.length())); + final Asset asset = assetRepo + .findById(assetId) + .orElseThrow(() -> new IllegalArgumentException( + String.format( + "No asset with id %d in the database.", + assetId))); + + name = asset.getDisplayName(); + + if (isMove(state) && assetManager.isAssetInUse(asset)) { + addErrorMessage(data, "cms.ui.folder.item_is_live", name); + } + + object = asset; + } else { + throw new IllegalArgumentException(String.format( + "Provided objectId '%s' does not start with '%s' " + + "or '%s'.", + objectId, + FOLDER_BROWSER_KEY_PREFIX_FOLDER, + FOLDER_BROWSER_KEY_PREFIX_ASSET)); + } + + final long count = controller.countObjects(target, name); + if (count > 0) { + // there is an item or subfolder in the target folder that already has this name + addErrorMessage(data, "cms.ui.folder.item_already_exists", + name); + } + + if (!(permissionChecker.isPermitted( + ItemPrivileges.DELETE, object)) + && isMove(state)) { + addErrorMessage(data, + "cms.ui.folder.no_permission_for_item", + object.getDisplayName()); + } + + } + + } + + private void addErrorMessage(final FormData data, + final String message, + final String itemName) { + data.addError(new GlobalizedMessage(message, + CmsConstants.CMS_FOLDER_BUNDLE, + new Object[]{itemName})); + } + + private class AssetFolderTree extends Tree { + + public AssetFolderTree(final FolderSelectionModel folderSelectionModel) { + + super(new FolderTreeModelBuilder() { + + @Override + protected Folder getRootFolder(final PageState state) { + final ContentSection section = CMS + .getContext() + .getContentSection(); + + return section.getRootAssetsFolder(); + } + + }); + setSelectionModel(selectionModel); + } + + @Override + public void setSelectedKey(final PageState state, final Object key) { + if (key instanceof String) { + final Long keyAsLong; + if (((String) key).startsWith( + FOLDER_BROWSER_KEY_PREFIX_FOLDER)) { + keyAsLong = Long.parseLong(((String) key).substring( + FOLDER_BROWSER_KEY_PREFIX_FOLDER.length())); + } else { + keyAsLong = Long.parseLong((String) key); + } + super.setSelectedKey(state, keyAsLong); + } else if (key instanceof Long) { + super.setSelectedKey(state, key); + } else { + //We know that a FolderSelectionModel only takes keys of type Long. + //Therefore we try to cast here + final Long keyAsLong = (Long) key; + super.setSelectedKey(state, keyAsLong); + } + } + + } + } diff --git a/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderBrowserController.java b/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderBrowserController.java index a55b0a1f1..ce35cd74d 100644 --- a/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderBrowserController.java +++ b/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderBrowserController.java @@ -573,7 +573,7 @@ public class FolderBrowserController { CmsConstants.CATEGORIZATION_TYPE_FOLDER), builder.equal(fromItem.get("version"), ContentItemVersion.DRAFT), - builder.like(fromItem.get("displayName"), + builder.like(builder.lower(fromItem.get("displayName")), filterTerm) ) ) @@ -780,7 +780,8 @@ public class FolderBrowserController { Objects.requireNonNull(targetFolder); - final ContentItem item = itemRepo.findById(itemId) + final ContentItem item = itemRepo + .findById(itemId) .orElseThrow(() -> new IllegalArgumentException(String.format( "No content item with ID %d in the database. " + "Where did that ID come from?", @@ -834,7 +835,8 @@ public class FolderBrowserController { Objects.requireNonNull(targetFolder); - final ContentItem item = itemRepo.findById(itemId) + final ContentItem item = itemRepo + .findById(itemId) .orElseThrow(() -> new IllegalArgumentException(String.format( "No content item with ID %d in the database. " + "Where did that ID come from?", diff --git a/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderManipulator.java b/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderManipulator.java index 3b4cb8152..d97fb7399 100755 --- a/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderManipulator.java +++ b/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderManipulator.java @@ -414,8 +414,8 @@ public class FolderManipulator extends SimpleContainer implements } @Override - public void process(final FormSectionEvent event) throws - FormProcessException { + public void process(final FormSectionEvent event) + throws FormProcessException { final PageState state = event.getPageState(); @@ -484,16 +484,16 @@ public class FolderManipulator extends SimpleContainer implements } - private class TargetSelectorValidationListener implements - FormValidationListener { + private class TargetSelectorValidationListener + implements FormValidationListener { public TargetSelectorValidationListener() { //Nothing } @Override - public void validate(final FormSectionEvent event) throws - FormProcessException { + public void validate(final FormSectionEvent event) + throws FormProcessException { final PageState state = event.getPageState(); @@ -519,7 +519,6 @@ public class FolderManipulator extends SimpleContainer implements // check create item permission final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); - final Shiro shiro = cdiUtil.findBean(Shiro.class); final PermissionChecker permissionChecker = cdiUtil.findBean( PermissionChecker.class); if (!permissionChecker.isPermitted( @@ -574,28 +573,28 @@ public class FolderManipulator extends SimpleContainer implements .folderIsMovable(folder, target); switch (movable) { case DIFFERENT_SECTIONS: - addErrorMessage(data, - "cms.ui.folder.different_sections", + addErrorMessage(data, + "cms.ui.folder.different_sections", name); break; case HAS_LIVE_ITEMS: - addErrorMessage(data, - "cms.ui.folder.item_is_live", + addErrorMessage(data, + "cms.ui.folder.item_is_live", name); break; case DIFFERENT_TYPES: - addErrorMessage(data, - "cms.ui.folder.different_folder_types", + addErrorMessage(data, + "cms.ui.folder.different_folder_types", name); break; case IS_ROOT_FOLDER: - addErrorMessage(data, - "cms.ui.folder.is_root_folder", + addErrorMessage(data, + "cms.ui.folder.is_root_folder", name); break; case SAME_FOLDER: - addErrorMessage(data, - "cms.ui.folder.same_folder", + addErrorMessage(data, + "cms.ui.folder.same_folder", name); break; case YES: @@ -614,8 +613,10 @@ public class FolderManipulator extends SimpleContainer implements } else if (objectId.startsWith(FOLDER_BROWSER_KEY_PREFIX_ITEM)) { final long itemId = Long.parseLong(objectId.substring( FOLDER_BROWSER_KEY_PREFIX_ITEM.length())); - final ContentItem item = itemRepo.findById(itemId).orElseThrow( - () -> new IllegalArgumentException(String.format( + final ContentItem item = itemRepo + .findById(itemId) + .orElseThrow(() -> new IllegalArgumentException( + String.format( "No content item with id %d in database.", itemId))); @@ -726,11 +727,11 @@ public class FolderManipulator extends SimpleContainer implements @Override protected Long getRootFolderID(final PageState state) { final ContentSection section = CMS - .getContext() - .getContentSection(); + .getContext() + .getContentSection(); return section.getRootDocumentsFolder().getObjectId(); } - + }; folderTree = new FolderTree(targetModel); folderTree.setCellRenderer(new FolderTreeCellRenderer()); @@ -742,8 +743,8 @@ public class FolderManipulator extends SimpleContainer implements final PageState state = event.getPageState(); final Label label = (Label) event.getTarget(); final int numberOfItems = getSources(state).length; - final Category folder = (Category) sourceFolderModel. - getSelectedObject(state); + final Category folder = (Category) sourceFolderModel + .getSelectedObject(state); final CdiUtil cdiUtil = CdiUtil.createCdiUtil(); final CategoryManager categoryManager = cdiUtil. findBean(CategoryManager.class); @@ -795,9 +796,10 @@ public class FolderManipulator extends SimpleContainer implements folderTree.expand(Long.toString(folder.getObjectId()), state); } else { - final List parents = folderManager.getParentFolders( - folder); - parents.stream() + final List parents = folderManager + .getParentFolders(folder); + parents + .stream() .map(parent -> Long.toString(parent.getObjectId())) .forEach(folderId -> folderTree.expand(folderId, state)); } diff --git a/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderTree.java b/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderTree.java index 6af564622..ccc1687a7 100755 --- a/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderTree.java +++ b/ccm-cms/src/main/java/com/arsdigita/cms/ui/folder/FolderTree.java @@ -60,7 +60,7 @@ public class FolderTree extends Tree { } else if (key instanceof Long) { super.setSelectedKey(state, key); } else { - //We now that a FolderSelectionModel only takes keys of type Long. + //We know that a FolderSelectionModel only takes keys of type Long. //Therefore we try to cast here final Long keyAsLong = (Long) key; super.setSelectedKey(state, keyAsLong); diff --git a/ccm-cms/src/main/java/org/librecms/CmsConstants.java b/ccm-cms/src/main/java/org/librecms/CmsConstants.java index 7618ba8f5..662f9a92d 100644 --- a/ccm-cms/src/main/java/org/librecms/CmsConstants.java +++ b/ccm-cms/src/main/java/org/librecms/CmsConstants.java @@ -55,6 +55,7 @@ public class CmsConstants { public static final String CATEGORIZATION_TYPE_FOLDER = "folder"; public static final String FOLDER_BROWSER_KEY_PREFIX_FOLDER = "folder-"; + public static final String FOLDER_BROWSER_KEY_PREFIX_ASSET = "asset-"; public static final String FOLDER_BROWSER_KEY_PREFIX_ITEM = "item-"; /** diff --git a/ccm-cms/src/main/java/org/librecms/contentsection/FolderManager.java b/ccm-cms/src/main/java/org/librecms/contentsection/FolderManager.java index ab3c368a4..d334162e2 100644 --- a/ccm-cms/src/main/java/org/librecms/contentsection/FolderManager.java +++ b/ccm-cms/src/main/java/org/librecms/contentsection/FolderManager.java @@ -62,10 +62,13 @@ public class FolderManager { @Inject private ContentItemRepository itemRepo; - + @Inject private ContentItemManager itemManager; + @Inject + private AssetManager assetManager; + /** * An enum describing if a folder can be deleted or not and why. * @@ -118,7 +121,11 @@ public class FolderManager { /** * The folder to move contains live items. */ - HAS_LIVE_ITEMS + HAS_LIVE_ITEMS, + /** + * The folder to contains assets which are in use. + */ + HAS_IN_USE_ASSETS } @Transactional(Transactional.TxType.REQUIRED) @@ -368,6 +375,10 @@ public class FolderManager { return FolderIsMovable.HAS_LIVE_ITEMS; } + if (usedAssetsInFolder(movingFolder)) { + return FolderIsMovable.HAS_IN_USE_ASSETS; + } + return FolderIsMovable.YES; } @@ -379,17 +390,17 @@ public class FolderManager { final Folder copiedFolder = folderRepo.findById( folder.getObjectId()) - .orElseThrow(() -> new IllegalArgumentException(String.format( + .orElseThrow(() -> new IllegalArgumentException(String.format( "No folder with ID %d in the database. Where did that ID come from?", folder.getObjectId()))); final Folder targetFolder = folderRepo.findById( target.getObjectId()) - .orElseThrow(() -> new IllegalArgumentException(String.format( + .orElseThrow(() -> new IllegalArgumentException(String.format( "No folder with ID %d in the database. Where did that ID come from?", target.getObjectId()))); - + final Folder copy = createFolder(copiedFolder.getName(), targetFolder); - + final List items = itemRepo.findByFolder(copiedFolder); // final List items = copiedFolder.getObjects() // .stream() @@ -434,6 +445,22 @@ public class FolderManager { return liveItemsInFolder || liveItemsInSubFolders; } + private boolean usedAssetsInFolder(final Folder folder) { + + final boolean usedAssetsInFolder = folder.getObjects() + .stream() + .map(categorization -> categorization.getCategorizedObject()) + .filter(object -> object instanceof Asset) + .map(asset -> (Asset) asset) + .anyMatch(asset -> assetManager.isAssetInUse(asset)); + + final boolean usedAssetsInSubFolders = folder.getSubFolders() + .stream() + .anyMatch(subFolder -> usedAssetsInFolder(folder)); + + return usedAssetsInFolder || usedAssetsInSubFolders; + } + /** * Returns the path of folder. *