/* * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. * * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package com.arsdigita.cms; import com.arsdigita.auditing.BasicAuditTrail; import com.arsdigita.cms.lifecycle.Lifecycle; import com.arsdigita.cms.lifecycle.LifecycleDefinition; import com.arsdigita.cms.util.SecurityConstants; import com.arsdigita.domain.DataObjectNotFoundException; import com.arsdigita.domain.DomainObjectFactory; import com.arsdigita.domain.DomainCollectionIterator; import com.arsdigita.kernel.ACSObject; import com.arsdigita.kernel.Party; import com.arsdigita.kernel.User; import com.arsdigita.kernel.permissions.PermissionService; import com.arsdigita.kernel.permissions.PrivilegeDescriptor; import com.arsdigita.persistence.DataCollection; import com.arsdigita.persistence.DataObject; import com.arsdigita.persistence.DataQuery; import com.arsdigita.persistence.DataQueryDataCollectionAdapter; import com.arsdigita.persistence.OID; import com.arsdigita.persistence.PersistenceException; import com.arsdigita.persistence.Session; import com.arsdigita.persistence.SessionManager; import com.arsdigita.persistence.metadata.Property; import com.arsdigita.util.Assert; import com.arsdigita.util.UncheckedWrapperException; import org.apache.log4j.Logger; import java.math.BigDecimal; import java.util.Date; import java.util.Iterator; /** * This class represents folders for which to organize items in a tree hierarchy. * * Folders will only ever exist as draft or live versions. There should never be any folders that are pending. The * pending versions of ordinary content items are stored in the live version of folders. * * Folders cannot have their own lifecycles. The methods to get or set lifecycles are no-ops. * * You should never call {@link #publish} or {@link #unpublish} on a folder; at present, these methods only log a * warning when they are called. In the future, these warnings may be turned into actual errors. * * @author Jack Chung * @author Michael Pih * @author David Lutterkort * @version $Id: Folder.java 1639 2007-09-17 13:20:13Z chrisg23 $ */ public class Folder extends ContentItem { private static final Logger s_log = Logger.getLogger(Folder.class); public static final String BASE_DATA_OBJECT_TYPE = "com.arsdigita.cms.Folder"; public static final String INDEX = "index"; public static final String HOME_FOLDER = "homeFolder"; public static final String HOME_SECTION = "homeSection"; private static final String ITEMS_QUERY = "com.arsdigita.cms.ItemsInFolder"; private static final String PRIMARY_INSTANCES_QUERY = "com.arsdigita.cms.PrimaryInstancesInFolder"; private static final String ITEM_QUERY = "com.arsdigita.cms.ItemInFolder"; private static final String FOLDER_QUERY = "com.arsdigita.cms.FolderInFolder"; private static final String LABEL = "label"; private static final String NAME = "name"; private final static String ITEM = "item"; private boolean m_wasNew; protected static final String ITEMS = "items"; /** * Default constructor. This creates a new folder. */ public Folder() { super(BASE_DATA_OBJECT_TYPE); } /** * Constructor. The contained * DataObject is retrieved from the persistent storage mechanism with an * OID specified by oid. * * @param oid The OID for the retrieved DataObject. */ public Folder(final OID oid) throws DataObjectNotFoundException { super(oid); } /** * Constructor. The contained * DataObject is retrieved from the persistent storage mechanism with an * OID specified by * id and * Folder.BASE_DATA_OBJECT_TYPE. * * @param id The id for the retrieved DataObject */ public Folder(final BigDecimal id) throws DataObjectNotFoundException { this(new OID(BASE_DATA_OBJECT_TYPE, id)); } public Folder(final DataObject obj) { super(obj); } public Folder(final String type) { super(type); } protected ContentItem makeCopy() { final Folder newItem = (Folder) super.makeCopy(); DomainCollectionIterator items = new DomainCollectionIterator(getItems()); newItem.copyItemsToFolder(items); return newItem; } public void copyItemsToFolder(final Iterator items) { while (items.hasNext()) { ContentItem item = (ContentItem) items.next(); item.copy(this, true); } } /** * @return the base PDL object type for this item. Child classes should override this method to return the correct * value */ public String getBaseDataObjectType() { return BASE_DATA_OBJECT_TYPE; } /** * Deletes the folder. * * @throws IllegalStateException if the folder is not empty. */ public void delete() throws IllegalStateException { s_log.debug("Deleting folder"); if (!isEmpty()) { throw new IllegalStateException("Attempt to delete non-empty folder " + getOID() + "; " + "only empty folders can be deleted"); } super.delete(); } protected void beforeDelete() { DataCollection maps = SessionManager.getSession().retrieve(UserHomeFolderMap.BASE_DATA_OBJECT_TYPE); maps.addEqualsFilter(HOME_FOLDER + "." + ID, getID()); while (maps.next()) { maps.getDataObject().delete(); } super.beforeDelete(); } protected void beforeSave() { m_wasNew = isNew(); super.beforeSave(); } protected void afterSave() { super.afterSave(); if (m_wasNew) { // If the parent of this folder is null, and this folder // is the root folder of its content section, then the // content section is set as the context of this folder, // so that permissions cascade correctly. // NB: Because folder is saved before the creation of the // content in the CMS Initializer, content section is null // and this isn't run! final ACSObject parent = getParent(); if (parent == null) { // Verify that the content section is not null and // this is its root folder. final ContentSection section = getContentSection(); if (section != null && (this.equals(section.getRootFolder()) || this. equals(section.getTemplatesFolder()))) { PermissionService.setContext(this, section); } } } // All folder versions should inherit their permissions from the // working version final ContentItem workingVersion = getWorkingVersion(); if (!this.equals(workingVersion)) { PermissionService.setContext(this, workingVersion); } } /** * Fetches the child items of this folder. The returned collection provides methods to filter by various criteria, * for example by name or by whether items are folders or not. * * @param bSort whether to sort the collection by isFolder and ID * @return child items of this folder */ public ItemCollection getItems(boolean bSort) { DataQueryDataCollectionAdapter adapter = new DataQueryDataCollectionAdapter(ITEMS_QUERY, ITEM); adapter.setParameter(PARENT, getID()); Assert.isNotEqual(PENDING, getVersion()); adapter.setParameter(VERSION, getVersion()); return new ItemCollection(adapter, bSort); } /** * Fetches the child items of this folder. The returned collection provides methods to filter by various criteria, * for example by name or by whether items are folders or not. The items returned by this method are sorted by * isFolder and ID * * @return child items of this folder, sorted by isFolder and ID */ public ItemCollection getItems() { return getItems(true); } /** * Returns collection of primary language instances for bundles in this folder. */ public ItemCollection getPrimaryInstances() { final DataQuery query = SessionManager.getSession().retrieveQuery(PRIMARY_INSTANCES_QUERY); query.setParameter(PARENT, getID()); Assert.isNotEqual(PENDING, getVersion()); query.setParameter(VERSION, getVersion()); return new ItemCollection(query); } /** * Returns a child content item in this folder (which could itself be a folder) with the specified name. * * @param name The name of the item * @param isFolder If true, only return a subfolder. Otherwise, return any subitem * @return The item with the given name, or null if no such item exists in the folder */ public ContentItem getItem(final String name, final boolean isFolder) { DataQuery query; if (isFolder) { query = SessionManager.getSession().retrieveQuery(FOLDER_QUERY); } else { query = SessionManager.getSession().retrieveQuery(ITEM_QUERY); } query.setParameter(PARENT, getID()); query.setParameter(VERSION, getVersion()); query.setParameter(NAME, name); DataCollection items = new DataQueryDataCollectionAdapter(query, ITEM); if (items.next()) { DataObject dataObj = items.getDataObject(); ContentItem result = (ContentItem) DomainObjectFactory .newInstance(dataObj); if (items.next()) { s_log.warn("Item in folder has a duplicate name; one " + "is " + result + " and one is " + (ContentItem) DomainObjectFactory .newInstance(items.getDataObject())); throw new IllegalStateException(); } return result; } else { return null; } } public void addItem(final ContentBundle item) { // TODO: saving the item here is a little weird, but // the only way we can guarantee that it ever gets saved. item.setParent(this); item.save(); } public String getDisplayName() { final String result = getLabel(); if (result == null) { return super.getDisplayName(); } else { return result; } } /** * Fetches the label of the folder. */ public final String getLabel() { return (String) get(LABEL); } /** * Set the label of this folder. */ public final void setLabel(final String value) { if (s_log.isDebugEnabled()) { s_log.debug("Setting label to " + value); } set(LABEL, value); } /** * Set the version of the folder. An attempt to set the version to pending will result in the folder's version being * set to live. We will never have any pending versions of folders, only live or draft. * * Pending versions of items are stored in the live version of a folder. */ protected void setVersion(String version) { if (ContentItem.PENDING.equals(version)) { version = ContentItem.LIVE; } super.setVersion(version); } // // Publish/unpublish stuff // public void unpublish() { if (s_log.isInfoEnabled()) { s_log.info("Unpublishing folder " + this); } super.unpublish(); } public ContentItem publish(final LifecycleDefinition cycleDef, final Date startDate) { if (s_log.isInfoEnabled()) { s_log.info("Publishing folder " + this); } return super.publish(cycleDef, startDate); } // // Lifecycle stuff // /** * Always returns * null, as folders do not have lifecycles. * * @return a Lifecycle value */ public Lifecycle getLifecycle() { return null; } /** * Does not do anything, as folders do not have lifecycles. */ public void setLifecycle(final Lifecycle cycle) { return; } /** * Does not do anything, as folders do not have lifecycles. */ public void removeLifecycle() { return; } protected void addPendingVersion(final ContentItem version) { if (Assert.isEnabled()) { assertDraft(); } version.setVersion(LIVE); //version.save(); setLiveVersion(version); } public void removePendingVersion(final ContentItem version) { Assert.isNotEqual(PENDING, version.getVersion()); return; } // // Index item // /** * Get the (special) index item for the folder. The index item is what carries all the user-editable attributes of * the folder. The index item is what should be published when a index page for a folder is desired. * * The index item is an ordinary item in every respect, i.e., it is part of the collection returned by * getItems(), you cannot delete a folder if it still has an index item etc. */ public ContentBundle getIndexItem() { // BECAUSE INDEX ITEM MIGHT NOT BE UPDATED FOR PUBLISHED // FOLDERS, CHECK IF DRAFT VERSIONS OF LIVE FOLDERS HAVE INDEX // ITEM. if (getVersion().compareTo(ContentItem.LIVE) == 0) { final ContentItem indexItem = ((Folder) getWorkingVersion()).getIndexItem(); if (indexItem == null) { return null; } return (ContentBundle) indexItem.getLiveVersion(); } final DataObject index = (DataObject) get(INDEX); if (index == null) { return null; } try { return (ContentBundle) DomainObjectFactory.newInstance(index); } catch (PersistenceException pe) { throw new UncheckedWrapperException(pe); } } /** * Sets the index item. This also adds the item to the folder. * * @param item The index item with the folder's user-editable attributes */ public final void setIndexItem(final ContentBundle item) { setAssociation(INDEX, item); addItem(item); // XXX Why is this needed? } /** * Removes the index item. */ public final void removeIndexItem() { setAssociation(INDEX, null); } /** * Returns * true if the folder is empty. * * @return true if the folder is empty */ public boolean isEmpty() { final Session session = SessionManager.getSession(); final DataQuery query = session.retrieveQuery("com.arsdigita.cms.folderNotEmpty"); query.setParameter("id", getID()); final boolean result = !query.next(); query.close(); return result; } /** * Returns * true if the folder contains at least one folder, * false if the folder does not contain any folders, but is either empty or contains only ordinary * items. * * @return true if the folder contains other folders. */ public boolean containsFolders() { final Session session = SessionManager.getSession(); final DataQuery query = session.retrieveQuery("com.arsdigita.cms.folderHasNoSubFolders"); query.setParameter("id", getID()); final boolean result = !query.next(); query.close(); return result; } /** * Copy the specified property (attribute or association) from the specified source folder. This method almost * completely overrides the metadata-driven methods in * ObjectCopier. If the property in question is an association to * ContentItem(s), this method should only call * FooContentItem newChild = * copier.copyItem(originalChild). An attempt to call any other method in order to copy the child will most * likely have disastrous consequences. * * If a child class overrides this method, it should return * super.copyProperty in order to indicate that it is not interested in handling the property in any * special way. * * @param srcItem the source item * @param property the property to copy * @param copier the ItemCopier * @return true if the property was copied, false to indicate that regular metadata-driven methods should be used to * copy the property */ public boolean copyProperty(final CustomCopy srcItem, final Property property, final ItemCopier copier) { // Ignore the items association. String attrName = property.getName(); if (ITEMS.equals(attrName) || INDEX.equals(attrName)) { return true; } return super.copyProperty(srcItem, property, copier); } /** * Folders aren't explicitly p2fs'd */ protected boolean canPublishToFS() { return false; } /** * A collection of items that can be filtered to return only folders or only nonfolders. */ public static class ItemCollection extends com.arsdigita.cms.ItemCollection { private final static String IS_FOLDER = "isFolder"; private final static String HAS_CHILDREN = "hasChildren"; private final static String ITEM = "item"; private final static String HAS_LIVE_VERSION = "hasLiveVersion"; private final static String TYPE_LABEL = "type.label"; private final static String AUDIT_TRAIL = "item.auditing"; private DataQuery m_query; /** * Constructor * * @param adapter an adapter constructed using the query name rather than a DataQuery object. This constructor * must be used if there is any intention to permission filter the results as only a * DataQueryDataCollectionAdapter constructed using query name has the bug fix to allow permission filtering * * @param bSort whether to sort the collection by isFolder and ID */ public ItemCollection(DataQueryDataCollectionAdapter adapter, boolean bSort) { super(adapter); doAlias(adapter); init(adapter, bSort); } public ItemCollection(DataQueryDataCollectionAdapter adapter) { this(adapter, true); } /** * Constructor * * @param query the Data Query to use to retrieve the collection * @param bSort whether to sort the collection by isFolder and ID */ //ideally, we wouldn't sort the collection by default and only provide //one constructor. But, that would break the existing API public ItemCollection(DataQuery query, boolean bSort) { super(new DataQueryDataCollectionAdapter(doAlias(query), ITEM)); init(query, bSort); } /** * Convenience Constructor that always sorts the collection by isFolder and ID * * jensp 2011-06: I changed this because this silly sorting affects the ItemSearchWidget and makes it pretty * useless... I've not noticed any negative effects, so it seams no problem. Sorting is now set by the * caller/user of the {@code ItemCollection}. * * @param query the Data Query to use to retrieve the collection */ public ItemCollection(DataQuery query) { super(new DataQueryDataCollectionAdapter(doAlias(query), ITEM)); init(query, false); } private void init(DataQuery query, boolean bSort) { m_query = query; if (bSort) { m_query.addOrder("isFolder desc"); addOrder("id desc"); } } private static DataQuery doAlias(final DataQuery query) { query.alias("isFolder", "isFolder"); return query; } /** * Sets the range of the dataquery. This is used by the paginator. * * @param beginIndex The start index * @param endIndex The end index */ public void setRange(final Integer beginIndex, final Integer endIndex) { m_dataQuery.setRange(beginIndex, endIndex); } public String getDisplayName() { return (String) get(DISPLAY_NAME); } /** * For performance reaons, override superclass methods and try to get the audit info without instantiating a * content item. We know this can help because the getPrimaryInstances query retrieves the audit info directly */ public Date getCreationDate() { DataObject dobj = (DataObject) get(AUDIT_TRAIL); if (dobj != null) { BasicAuditTrail audit = new BasicAuditTrail(dobj); return audit.getCreationDate(); } else { return super.getCreationDate(); } } public Date getLastModifiedDate() { DataObject dobj = (DataObject) get(AUDIT_TRAIL); if (dobj != null) { BasicAuditTrail audit = new BasicAuditTrail(dobj); return audit.getLastModifiedDate(); } else { return super.getLastModifiedDate(); } } /** * Return the pretty name of the content type of the current item. If the current item is a folder, the string * Folder is returned, otherwise the label of the item's content type. * * @return the pretty name of the content type of the current item. */ public String getTypeLabel() { if (isFolder()) { return "Folder"; } else { return (String) get(TYPE_LABEL); } } /** * Filter the collection by whether items are folders or not. * * @param v true if the data query should only list folders, false if the data query * should only list non-folder items. * */ public void addFolderFilter(final boolean v) { m_query.addEqualsFilter(IS_FOLDER, v ? "1" : "0"); } /** * Return * true if the current item in the collection is a folder. * * @return true if the current item in the collection is a folder. */ public boolean isFolder() { Boolean result = (Boolean) m_query.get(IS_FOLDER); return result.booleanValue(); } public boolean hasChildren() { Boolean result = (Boolean) m_query.get(HAS_CHILDREN); return result.booleanValue(); } public boolean isLive() { String version = (String) get(ContentItem.VERSION); if (ContentItem.LIVE.equals(version)) { return true; } Boolean hasLive = (Boolean) m_query.get(HAS_LIVE_VERSION); return hasLive.booleanValue(); } /** * Only used on CollectionS returned by getPrimaryInstance() */ public BigDecimal getBundleID() { if (isFolder()) { return null; } else { return (BigDecimal) m_query.get("bundleID"); } } } /** * Called by * VersionCopier to determine whether to publish associated items when an item goes live. This will * only have an effect for non-component associations where the item is not yet published. Override default for * Folders since they don't have their own lifecycles and a folder must be published when an item in it * goes live. * * @return whether to publish this item */ public boolean autoPublishIfAssociated() { return true; } public static void setUserHomeFolder(User user, Folder folder) { UserHomeFolderMap map = UserHomeFolderMap.findOrCreateUserHomeFolderMap(user, folder.getContentSection()); map.setHomeFolder(folder); map.save(); } public static Folder getUserHomeFolder(User user, ContentSection section) { Folder folder = null; UserHomeFolderMap map = UserHomeFolderMap.findUserHomeFolderMap(user, section); if (map != null) { folder = map.getHomeFolder(); if (folder != null) { CMSContext context = CMS.getContext(); SecurityManager sm; if (context.hasSecurityManager()) { sm = CMS.getContext().getSecurityManager(); } else { sm = new SecurityManager(section); } if (!sm.canAccess(user, SecurityConstants.PREVIEW_PAGES, folder)) { folder = null; } } } return folder; } /** * Retrieves a folder by its path from a given content section. * * @param section The content section from which the folder should be retrieved. * @param path The path of the folder, relative to the content section. * @return The folder with the given path from the provided content section. If there is no such folder, * {@code null} is returned. It is up to the caller to check the returned value for {@code null} and take * appropriate actions. */ public static Folder retrieveFolder(final ContentSection section, final String path) { if (section == null) { throw new IllegalArgumentException("No content section provided."); } if ((path == null) || path.isEmpty()) { throw new IllegalArgumentException("No path provided."); } if (path.charAt(0) != '/') { throw new IllegalArgumentException("Provided path is not an absolute path (starting with '/')."); } final String[] pathTokens = path.split("/"); final Folder rootFolder = section.getRootFolder(); Folder folder = rootFolder; for (String token : pathTokens) { if ((token == null) || token.isEmpty() || "/".equals(token)) { continue; } folder = getSubFolder(token, folder); if (folder == null) { break; } } return folder; } private static Folder getSubFolder(final String name, final Folder fromFolder) { final ItemCollection items = fromFolder.getItems(); items.addFolderFilter(true); items.addNameFilter(name); if (items.next()) { return (Folder) items.getDomainObject(); } else { return null; } } /** * Retrieves a folder of the current content section by its path. The path is given in a UNIX like synatax and must * be an absolute path starting with '/'. The path may be precceded with the name of content section, separated by * ':'. If a content section if given, the path is relative to this content section. If the no content section is * given, the current content section returned by {@code CMS.getContext().getContentSection()}. Please note that * {@code CMS.getContext().getContentSection()} may return null. * * Examples for valid paths: * *
     * /persons/members
     * content:/persons/members
     * publications:/monographs
     * 
* * @param path The path of the folder to retrieve relative to the current content section. * @return The folder with the given path from the content section. If there is no such folder, {@code null} is * returned. It is up to the caller to check the returned value for {@code null} and take appropriate actions. */ public static Folder retrieveFolder(final String path) { final String[] tokens = path.split(":"); if (tokens.length == 1) { return retrieveFolder(CMS.getContext().getContentSection(), path); } else if (tokens.length == 2) { final ContentSectionCollection sections = ContentSection.getAllSections(); sections.addEqualsFilter("label", tokens[0]); if (sections.isEmpty()) { return null; } else { sections.next(); final ContentSection section = sections.getContentSection(); return retrieveFolder(section, tokens[1]); } } else { throw new IllegalArgumentException("Invalid path syntax. Valid syntax: " + "[contentsection:]/path/to/folder'"); } } }