/* * 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.AuditingObserver; import com.arsdigita.auditing.BasicAuditTrail; import com.arsdigita.categorization.CategorizedObject; import com.arsdigita.categorization.Category; import com.arsdigita.categorization.CategoryCollection; import com.arsdigita.cms.contenttypes.ContentGroupAssociation; import com.arsdigita.cms.contenttypes.Link; import com.arsdigita.cms.lifecycle.Lifecycle; import com.arsdigita.cms.lifecycle.LifecycleDefinition; import com.arsdigita.cms.lifecycle.LifecycleService; import com.arsdigita.cms.lifecycle.PublishLifecycleListener; import com.arsdigita.cms.publishToFile.QueueManager; import com.arsdigita.domain.AbstractDomainObjectObserver; import com.arsdigita.domain.DataObjectNotFoundException; import com.arsdigita.domain.DomainObject; import com.arsdigita.domain.DomainObjectFactory; import com.arsdigita.domain.DomainObjectObserver; import com.arsdigita.globalization.GlobalizationException; import com.arsdigita.globalization.Locale; import com.arsdigita.kernel.ACSObject; import com.arsdigita.kernel.User; import com.arsdigita.kernel.permissions.PermissionService; import com.arsdigita.persistence.DataAssociation; import com.arsdigita.persistence.DataAssociationCursor; import com.arsdigita.persistence.DataCollection; import com.arsdigita.persistence.DataObject; import com.arsdigita.persistence.DataQuery; import com.arsdigita.persistence.DataQueryDataCollectionAdapter; import com.arsdigita.persistence.Filter; import com.arsdigita.persistence.OID; import com.arsdigita.persistence.SessionManager; import com.arsdigita.persistence.metadata.Property; import com.arsdigita.util.Assert; import com.arsdigita.util.Reporter; import com.arsdigita.util.UncheckedWrapperException; import com.arsdigita.versioning.VersionedACSObject; import com.arsdigita.versioning.Versions; import org.apache.log4j.Logger; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * This class represents a content item. * *

Publishing Items

* * The {@link #publish(LifecycleDefinition,java.util.Date)} method can be used * to schedule the item for publication. The publication of an item proceeds in * two steps: * * 1. Pending Version * * A pending version is immediately created for the item, and each subitem of * the item. When the internal * createPendingVersion method is called, the content item will * attempt to clone itself in order to create the pending version. * * First, the item will clone itself and all of its scalar attributes.Then, the * item will clone all of its composite relations. After that, * the item will copy all of its non-composite associations "by reference". If a * target of any association is a * ContentItem, the cloned item will reference the live or pending * version of the target item. * * For example, consider * Articles A and B, both of which reference an * ImageAsset I: * *
 * A ---> I <--- B
 * 
* * When A is published, creating a pending version A', I will be published as * well: * *
 * A ---> I <--- B
 * A'---> I'
 * 
* * When B is later published as B', B' will reference I': * *
 * A ---> I <--- B
 * A'---> I'<--- B'
 * 
* * In order to work correctly with the automatic publishing code, every subclass * of * ContentItem (such as "FooItem extends ContentItem") must * adhere to the following guidelines: * * * * After the pending version is created, the version copier will assign it a new * lifecycle, based on the values passed in to {@link * #publish(LifecycleDefinition, java.util.Date)}, but only if the new * pending version is a regular aggregation (not a composition). In theory, it * should make no difference whether the new pending version is a composition or * not; however, some bugs within the publishing code currently prevent this * from working correctly. For this reason, it is critically important * to pass the right parameter to {@link ItemCopier#copy} the {@link #copyProperty(ContentItem, String, * ItemCopier)} method. * * 2. Live Version * * When the lifecycle finally rolls around to the start date specified in the * publish method, the pending versions for the item and all the * subitems will be promoted to live, and the item will appear on the live site. * Another publishing bug currently makes it a requirement to reload * the original item from the database after it has been successfully published; * I am working on fixing this. * * 3. Unpublishing * * When the lifecycle for an item expires, its live version is deleted and * removed from the live site, along with all its subitems. * * 4. Future work * * The new data model makes it possible to have multiple pending versions for a * content item; it should also be theoretically possible to archive expired * live versions, as opposed to deletin g them. There are no Java APIs for this * functionality as of yet, however. * *

Copying Items

* * The {@link ItemCopier#copy} method may be used to create a nearly identical * copy of the item, according to the rules described above. The new item will * be a full-fledged, standalone item. Note that the services (such as * categories) will not be automatically transferred to the new copy of the * item; the {@link * #copyServicesFrom(ContentItem)} method must be called on the new item to * transfer the services. Calling this method is not a requirement, however. * * @author Uday Mathur * @author Jack Chung * @author Michael Pih * @author Stanislav Freidin <sfreidin@redhat.com> * @author Jens Pelzetter * * @version $Id: ContentItem.java 2218 2011-06-22 23:55:36Z pboy $ */ public class ContentItem extends VersionedACSObject implements CustomCopy { private static final Logger s_log = Logger.getLogger(ContentItem.class); private static final Logger s_logDenorm = Logger.getLogger(ContentItem.class.getName() + ".Denorm"); private static final String MODEL = "com.arsdigita.cms"; private static final String QUERY_PENDING_ITEMS = MODEL + ".getPendingSortedByLifecycle"; public static final String BASE_DATA_OBJECT_TYPE = MODEL + ".ContentItem"; /** * A state marking the draft or master item corresponding to a live or * pending version of that item. */ public static final String DRAFT = "draft"; /** * A state marking the live version, a copy of the draft item. */ public static final String LIVE = "live"; /** * A state marking the live version, a copy of the draft item. */ public static final String PENDING = "pending"; // Metadata attribute constants public static final String ANCESTORS = "ancestors"; public static final String PARENT = "parent"; public static final String CHILDREN = "contentChildren"; public static final String CONTENT_TYPE = "type"; public static final String VERSION = "version"; public static final String NAME = "name"; public static final String LANGUAGE = "language"; public static final String AUDITING = "auditing"; public static final String DRAFT_VERSION = "masterVersion"; public static final String VERSIONS = "slaveVersions"; public static final String CONTENT_SECTION = "section"; private static final String PUBLISH_LISTENER_CLASS = PublishLifecycleListener.class.getName(); private VersionCache m_pending; private VersionCache m_live; private boolean m_wasNew; private Reporter m_reporter; private BasicAuditTrail m_audit_trail; /** * Default constructor. This creates a new content item. */ public ContentItem() { this(BASE_DATA_OBJECT_TYPE); s_log.debug("Undergoing creation"); } /** * 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 ContentItem(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 * ContentItem.BASE_DATA_OBJECT_TYPE. * * @param id The * id for the retrieved * DataObject */ public ContentItem(final BigDecimal id) throws DataObjectNotFoundException { this(new OID(BASE_DATA_OBJECT_TYPE, id)); } /** * Constructor. Retrieves or creates a content item using the * DataObject argument. * * @param obj The * DataObject with which to create or load a content item */ public ContentItem(final DataObject obj) { super(obj); } /** * Constructor. Creates a new content item using the given data object type. * Such items are created as draft versions. * * @param type The * String data object type of the item to create */ public ContentItem(final String type) { super(type); if (s_log.isDebugEnabled()) { s_log.debug("Content item " + this + " created with type " + type); } } private static DomainObjectObserver s_parentObs = new AbstractDomainObjectObserver() { public void set(DomainObject dobj, String name, Object old, Object newVal) { if (PARENT.equals(name)) { ContentItem ci = (ContentItem) dobj; if (newVal != null) { PermissionService.setContext(ci.getOID(), ((DataObject) newVal).getOID()); } } } }; /** * Called from the base class ( * DomainObject) constructors. */ protected void initialize() { super.initialize(); addObserver(s_parentObs); DataObject dataObj = (DataObject) get(AUDITING); if (dataObj != null) { m_audit_trail = new BasicAuditTrail(dataObj); } else { // creates a new one when one doesn't already exist m_audit_trail = BasicAuditTrail.retrieveForACSObject(this); } addObserver(new AuditingObserver(m_audit_trail)); m_pending = new VersionCache(); m_live = new VersionCache(); m_reporter = new Reporter(s_log, this, ContentItem.class); if (isNew()) { s_log.debug(this + " is being newly created; " + "marking it as a draft version"); m_wasNew = true; set(VERSION, DRAFT); setMaster(this); try { final ContentType type = ContentType.findByAssociatedObjectType( getSpecificObjectType()); if (s_log.isDebugEnabled()) { s_log.debug("Set content type for " + this + " to " + type); } setContentType(type); } catch (DataObjectNotFoundException donfe) { if (s_log.isDebugEnabled()) { s_log.debug("No content type found for " + this); } // Do nothing. There's no associated content // type. } } else { if (Assert.isEnabled()) { Assert.exists(getVersion(), String.class); } } } /** * @return the base PDL object type for this item. Child classes should * override this method to return the correct value */ @Override public String getBaseDataObjectType() { return this.BASE_DATA_OBJECT_TYPE; } /** * Publicized getter method for use by metadata forms. * * @param key * * @return */ @Override public Object get(final String key) { return super.get(key); } /** * Public setter method for use by metadata forms. * * @param key param value */ @Override public void set(final String key, final Object value) { super.set(key, value); } /** * Public add for use by metadata forms. * * @param propertyName param dobj * * @return */ @Override public DataObject add(String propertyName, DomainObject dobj) { return super.add(propertyName, dobj); } /** * Public remove for use by metadata forms * * @param propertyName param dobj */ @Override public void remove(String propertyName, DomainObject dobj) { super.remove(propertyName, dobj); } /** * For new content items, sets the associated content type if it has not * been already set. */ @Override protected void beforeSave() { m_wasNew = isNew(); super.beforeSave(); if (m_wasNew) { // Set the default content section. if (getContentSection() == null) { setDefaultContentSection(); } } } /* * * * removed cg - object observer sets context based on parent whenever parent * is updated * * protected void afterSave() { super.afterSave(); s_log.info("******After * Save of object " + getOID()); // Set the object's context to its parent * object for // permissioning. if (m_wasNew) { final ACSObject parent = * getParent(); if (parent == null) { s_log.info("parent is null - set * context to content section"); PermissionService.setContext(this, * getContentSection()); } else { s_log.info("parent is " + * parent.getOID()); PermissionService.setContext(this, parent); } } } */ private void setDefaultContentSection() { s_log.debug("Setting the default content section"); final String version = getVersion(); if (version != null && version.equals(ContentItem.DRAFT)) { // If the parent is not a folder, the content section of // the child (this item) should not be set. final ACSObject parent = getParent(); if (parent != null && parent instanceof Folder) { setContentSection(((ContentItem) parent).getContentSection()); } else { s_log.debug("The item's parent is not a folder; I am " + "not setting the default content section"); } } else { s_log.debug("The item's version is null or it is not draft; " + "doing nothing"); } } /** * Fetch the display name of the content item. The display name for a {@link com.arsdigita.cms.ContentItem} * is the name property. * * @return The name of the content item */ public String getDisplayName() { return getName(); } /** * Fetches the name of the content item. * * @return The name of the content item */ public String getName() { return (String) get(NAME); } /** * Sets the name of the content item. * * @param value The name of the content item */ public void setName(final String value) { Assert.exists(value, String.class); set(NAME, value); m_reporter.mutated("name"); } /** * Get the parent object. */ public ACSObject getParent() { return (ACSObject) DomainObjectFactory.newInstance((DataObject) get( PARENT)); } /** * Set the parent object. * * @param object The * ACSObject parent */ public final void setParent(final ACSObject object) { setAssociation(PARENT, object); m_reporter.mutated("parent"); } /** * Fetches all the child items of this item. * * @return an * ItemCollection of children */ public final ItemCollection getChildren() { final DataAssociationCursor cursor = ((DataAssociation) super.get(CHILDREN)). cursor(); return new ItemCollection(cursor); } /** * Gets the content type of this content item. */ public ContentType getContentType() { DataObject type = (DataObject) get(CONTENT_TYPE); if (type == null) { return null; } else { return new ContentType(type); } } /** * Sets the content type of this content item. * * @param type The content type */ public void setContentType(ContentType type) { setAssociation(CONTENT_TYPE, type); m_reporter.mutated("contentType"); } public boolean isContentType(ContentType type) { try { // Try to cast this contentItem to the desired content type // This will succeed if this ci is of the type or a subclass Class.forName(type.getClassName()).cast(this); return true; } catch (Exception ex) { return false; } } /** * Returns the content section to which this item belongs. Fetches the * denormalized content section of an item. If one is not found, this method * returns null. * * Since * cms_items.section_id is a denormalization, this method may * return null even if the item "belongs" to a content section. For example, * calling * getContentSection() on an Article's * ImageAsset will return null even though the image asset * should belong to the same section as the article. * * @return The content section to which this item belongs */ public ContentSection getContentSection() { return (ContentSection) DomainObjectFactory.newInstance((DataObject) get( CONTENT_SECTION)); } /** * Set the content section of an item. * * @param section The content section */ public final void setContentSection(final ContentSection section) { setAssociation(CONTENT_SECTION, section); m_reporter.mutated("contentSection"); } /** * Return the path to the item starting at its root. The path is absolute, * of the form /x/y/z where x and y are the names * of the item's grandparent and parent respectively, and z is the * name of the item itself. * * The item's root is the ancestor reachable through repeated * getParent() calls whose parent is * null. This is usually a folder, but may be any { * * @see com.arsdigita.kernel.ACSObject}. * * Note that the name of the root folder of the content section where the * item resides is not included in the path. * * @see #getPathInfo(boolean) * @return the path from the item's root to the item */ public String getPath() { return getPathNoJsp(); } /** * * @see #getPathInfo(boolean) * * @return the path from the item's root to the item */ public String getPathNoJsp() { StringBuffer result = new StringBuffer(400); ItemCollection coll = getPathInfo(true); coll.next(); s_log.debug("Get item path not jsp"); boolean first = true; while (coll.next()) { if (!first) { result.append('/'); } else { first = false; } s_log.debug("Add " + coll.getName()); result.append(coll.getName()); s_log.debug("Now " + result); } return result.toString(); } /** * Return a collection of ancestors starting from the item's root to the * item's parent item. For items contained in folders this is similar to a * directory path to the item. The collection starts with the root item and * ends with the item's direct parent. * *

The item's root is the ancestor reachable through repeated * getParent() calls whose parent is * null. This is usually a folder, but may be any { * * @see com.arsdigita.kernel.ACSObject}. * * @see #getPathInfo(boolean) * * @return the collection of the item's ancestors. */ public ItemCollection getPathInfo() { return getPathInfo(false); } /** * Return a collection of ancestors starting from the item's root to the * item's parent item (if * includeSelf is * false) or to the item itself otherwise. For items contained * in folders this is similar to a directory path to the item. The * collection starts with the root item and ends with the item's direct * parent. * *

The item's root is the ancestor reachable through repeated * getParent() calls whose parent is * null. This is usually a folder, but may be any { * * @see com.arsdigita.kernel.ACSObject}. * * @param includeSelf a * boolean value. * * @return the items on the path to the root folder. */ public ItemCollection getPathInfo(boolean includeSelf) { DataCollection collection = SessionManager.getSession().retrieve( BASE_DATA_OBJECT_TYPE); String ids = (String) get(ANCESTORS); if (ids == null) { // this should not happen if (includeSelf) { // there are no ancestors so we only return this item collection.addEqualsFilter(ID, getID()); return new ItemCollection(collection); } else { // there are no ancestors and we want want to return this // it so we want an empty collection...but, this should // never happen collection.addFilter("1=2"); return new ItemCollection(collection); } } //add list of ancestors split by "/" character ArrayList ancestors = new ArrayList(); int iIndex = 0; for (int i = ids.indexOf("/", 0); i != -1; i = ids.indexOf("/", iIndex)) { ancestors.add(ids.substring(0, i + 1)); iIndex = i + 1; } Filter filter = collection.addFilter(ANCESTORS + " in :ancestors"); filter.set("ancestors", ancestors); collection.addOrder(ANCESTORS); if (!includeSelf) { collection.addNotEqualsFilter(ID, getID()); } return new ItemCollection(collection); } // // Methods for accessing and linking content item versions // /** * Gets the version tag. */ public String getVersion() { return (String) get(VERSION); } /** * Sets the version tag. * * @param version A version tag, {@link #LIVE} or {@link #DRAFT} or {@link #PENDING} */ protected void setVersion(final String version) { set(VERSION, version); m_reporter.mutated("version"); } /** * Returns * true if this item is a * DRAFT version. * * @return < code>true if this item is a * DRAFT version */ public boolean isDraftVersion() { return DRAFT.equals(getVersion()); } /** * Returns the * DRAFT version of this content item. * * @return the draft version */ public ContentItem getDraftVersion() { if (s_log.isDebugEnabled()) { s_log.debug("Getting the draft version of " + this); } final DataObject draft = (DataObject) get(DRAFT_VERSION); if (draft == null) { // XXX I would like to put this here, but publishing // prevents us. //assertDraft(); return this; } else { return (ContentItem) DomainObjectFactory.newInstance(draft); } } /** * Fetches the draft (aka, "master" or "working") version of this content * item. * * @return the working version representation of the * ContentItem, possibly this item * * @deprecated use {@link #getDraftVersion()} instead */ public ContentItem getWorkingVersion() { return getDraftVersion(); } /** * Returns * true if this item is a * PENDING version. * * @return < code>true if * this is one of the pending versions */ public boolean isPendingVersion() { return PENDING.equals(getVersion()); } /** * Returns one * PENDING version of this content item. * * @return one of the pending versions */ ContentItem getPendingVersion() { if (s_log.isDebugEnabled()) { s_log.debug("getPendingVersion: " + getOID()); } if (m_pending.isCached()) { return m_pending.get(); } return m_pending.set(getUncachedPendingVersion()); } private ContentItem getUncachedPendingVersion() { if (s_log.isDebugEnabled()) { s_log.debug("getUncachedPendingVersion: " + getOID()); } ItemCollection versions = getPendingVersions(); try { if (versions.next()) { return versions.getContentItem(); } return null; } finally { versions.close(); } } /** *

Fetches the pending versions, if any, of this content item. The * versions are returned in chronological order, sorted by their respective * lifecycle's start date.

* * @return the collection of pending versions for this item */ public ItemCollection getPendingVersions() { if (s_log.isDebugEnabled()) { s_log.debug("getPendingVersions: " + getOID()); } DataQuery versions = getSession().retrieveQuery(QUERY_PENDING_ITEMS); versions.setParameter("itemID", getDraftVersion().getID()); return new ItemCollection(new DataQueryDataCollectionAdapter(versions, "item")); } /** * Adds a pending version to the item. */ protected void addPendingVersion(final ContentItem version) { if (s_log.isDebugEnabled()) { s_log.debug("Adding pending version " + version); } if (Assert.isEnabled()) { Assert.exists(version, ContentItem.class); assertDraft(); version.assertPending(); } add(VERSIONS, version); m_pending.clear(); } /** * Removes a pending version from the item. * * @param version the version to remove */ public void removePendingVersion(final ContentItem version) { if (s_log.isDebugEnabled()) { s_log.debug("Removing pending version " + version); } if (Assert.isEnabled()) { Assert.exists(version, ContentItem.class); assertDraft(); version.assertPending(); } remove(VERSIONS, version); m_pending.clear(); version.delete(); } /** * Returns * true if this item is a * LIVE version. * * @return < code>true if * this is the live version */ public boolean isLiveVersion() { return LIVE.equals(getVersion()); } /** * Fetches the live version of this content item. Returns null if there is * none. * * @return a * ContentItem representing the live version */ public ContentItem getLiveVersion() { if (s_log.isDebugEnabled()) { s_log.debug("Getting the live version of " + this); } if (LIVE.equals(getVersion())) { return this; } if (m_live.isCached()) { return m_live.get(); } s_log.debug("m_live miss"); final DataAssociationCursor versions = ((DataAssociation) get(VERSIONS)).cursor(); versions.addEqualsFilter(VERSION, LIVE); try { if (versions.next()) { ContentItem item = (ContentItem) DomainObjectFactory.newInstance(versions. getDataObject()); return m_live.set(item); } return m_live.set(null); } finally { versions.close(); } } /** * Sets the live version. * * @param version The * ContentItem to set live */ protected void setLiveVersion(final ContentItem version) { if (s_log.isDebugEnabled()) { s_log.debug("Setting live version to " + version); } if (Assert.isEnabled()) { assertDraft(); } final ContentItem live = getLiveVersion(); if (live != null) { remove(VERSIONS, live); } if (version == null) { m_live.set(null); } else { add(VERSIONS, version); m_live.set(version); } } /** * Get the live version for the item. If no live version exists, return the * latest pending version, if any. * * @return the public version for this item, or null if none */ public ContentItem getPublicVersion() { if (s_log.isDebugEnabled()) { s_log.debug("getPublicVersion: " + getOID()); } final ContentItem live = getLiveVersion(); if (live == null) { if (s_log.isDebugEnabled()) { s_log.debug("getPublicVersion: no live version " + getOID()); } return getPendingVersion(); } if (s_log.isDebugEnabled()) { s_log.debug("getPublicVersion: returning live version " + getOID()); } return live; } // // Publishing methods // /** * Method to determine whether this ContentItem should be automatically * published to the file system. */ protected boolean canPublishToFS() { return true; } /** * Publish this item to the filesystem; can only be called on a live * version. */ protected void publishToFS() { if (!canPublishToFS()) { return; } if (s_log.isDebugEnabled()) { s_log.debug("Publishing item " + this + " to the file system"); } assertLive(); QueueManager.queuePublish(this); } protected void unpublishFromFS() { if (!canPublishToFS()) { return; } if (s_log.isDebugEnabled()) { s_log.debug("Unpublishing item " + this + " to the file system"); } assertLive(); QueueManager.queueUnpublish(this); } /** * Returns true if this language version of this item has a publicly viewable * version. This item is not necessarily the live version nor is this * method to be confused with isPublished. * * @return < code>true if this content item has a live * version, or if it is the live version */ public boolean isLive() { return getLiveVersion() != null; } // Added by: Quasimodo /** * Returns true if this item has a publicly viewable version in any language. * This item is not necessarily the live version nor is this method * to be confused with isPublished. * * @return < code>true if this content bundle item has a live * version */ public boolean hasLiveInstance() { return getBundle().getLiveVersion() != null; } /** * Makes an item live or not live. * * @param version the version which should become live, null to make the * item non-live */ public void setLive(final ContentItem version) { if (s_log.isDebugEnabled()) { s_log.debug("Setting item " + this + " live with version " + version); } if (Assert.isEnabled()) { Assert.isTrue(version == null || LIVE.equals(version.getVersion()), "Item version " + version + " must be null or " + "the live version"); } if (isLive()) { s_log.debug("The item is already live; getting the current " + "live version"); final ContentItem oldVersion = getLiveVersion(); if (s_log.isDebugEnabled()) { s_log.debug("The current live version is " + oldVersion); } ACSObject parent = null; if (version == null) { // Find all live items with the same parent as this // item, other than this item. // XXX We don't need to use a custom query here // anymore. final DataQuery items = SessionManager.getSession().retrieveQuery( "com.arsdigita.cms.getLiveItemsWithSameParent"); items.addNotEqualsFilter("id", oldVersion.getID()); items.setParameter("itemId", oldVersion.getID()); // If there aren't any, unpublish the parent. Don't // get the parent of the live version, because it all // breaks. if (!items.next()) { parent = getParent(); if (s_log.isDebugEnabled()) { s_log.debug(oldVersion + " is the last child of " + parent); } } items.close(); } // Queue task to delete any files written for this item. if (oldVersion.canPublishToFS()) { oldVersion.unpublishFromFS(); } if (version == null || !version.equals(oldVersion)) { s_log.debug("Deleting old live version"); oldVersion.delete(); PublishedLink.refreshOnUnpublish(this); } if (parent instanceof ContentBundle || parent instanceof Folder) { if (s_log.isDebugEnabled()) { s_log.debug("Parent of " + oldVersion + " is " + parent + "; unpublishing the parent"); } ((ContentItem) parent).setLive(null); } s_log.debug("Setting the live version association to null and " + "saving"); setLiveVersion(null); save(); } if (version != null) { s_log.debug("The new version is not null; setting the live " + "version association"); setLiveVersion(version); save(); PublishedLink.updateLiveLinks(version); save(); // publish item (as template or html pages) to the file // system if appropriate if (version.canPublishToFS()) { version.publishToFS(); } } } /** * Schedules an item for publication. * * @param cycleDef The lifecycle definition * @param startDate The time to schedule the start of the lifecycle. If * null, use the current time as the start date. * * @return the new pending version */ public ContentItem publish(final LifecycleDefinition cycleDef, final Date startDate) { applyTag("Published"); Versions.suspendVersioning(); if (s_log.isDebugEnabled()) { s_log.debug("Publishing item " + this + " with lifecycle " + "definition " + cycleDef + " and start date " + startDate); } /* * amended Chris Gilbert * * Some content types may have their own lifecycles with their own * default listeners. Previous implementation just enforced the listener * retrieved from getPublisherClassName. This amendment looks for a * default listener in the cycle definition first * */ String listener = cycleDef.getDefaultListener(); if (listener == null) { listener = getPublishListenerClassName(); } final Lifecycle cycle = cycleDef.createFullLifecycle(startDate, listener); if (s_log.isDebugEnabled()) { s_log.debug("Instantiated lifecycle " + cycle); } // Create the pending version for the item final ContentItem pending = createPendingVersion(cycle); if (s_log.isDebugEnabled()) { s_log.debug("Created pending content item " + pending); } if (Assert.isEnabled()) { Assert.exists(pending, ContentItem.class); Assert.isTrue(PENDING.equals(pending.getVersion()) || LIVE.equals(pending.getVersion()), "The new pending item must be pending or live; " + "instead it is " + pending.getVersion()); } return pending; } public String getPublishListenerClassName() { String className = ContentSection.getConfig(). getPublishLifecycleListenerClass(); if (className != null && !"".equals(className)) { return className; } else { return PUBLISH_LISTENER_CLASS; } } /** * Unpublishes an item. This method removes the item's lifecycle and removes * all pending versions. It is intended for use in UI code, and it should * not be used for making items go "unlive". Instead, use * setLive(null). */ public void unpublish() { if (s_log.isDebugEnabled()) { s_log.debug("Unpublishing item " + this); } Versions.suspendVersioning(); if (isLive()) { if (s_log.isDebugEnabled()) { s_log.debug("The item is currently live; removing the " + "lifecycle of the public version, " + getPublicVersion()); } removeLifecycle(getPublicVersion()); setLive(null); } else { s_log.debug("The item is not live; removing its lifecycle"); removeLifecycle(this); } s_log.debug("Removing all pending versions"); final ItemCollection pending = getPendingVersions(); while (pending.next()) { final ContentItem item = pending.getContentItem(); removePendingVersion(item); } save(); } /** * Republish the item using its existing lifecycle */ public void republish() { republish(false); } /** * Republish the item @parameter reset - if true create a new lifecycle, if * false use existing Called from ui.lifecycle.ItemLifecycleItemPane.java */ public void republish(boolean reset) { if (s_log.isDebugEnabled()) { s_log.debug("Republishing item " + getOID().toString()); } applyTag("Republished"); Versions.suspendVersioning(); Assert.isTrue(isLive(), "Attempt to republish non live item " + getOID()); Lifecycle cycle = getLifecycle(); Assert.exists(cycle, Lifecycle.class); //resets lifecycle if opted if (reset) { cycle.reset(); } if (s_log.isDebugEnabled()) { s_log.debug("Reusing lifecycle " + cycle.getOID()); } ContentItem newLive = createPendingVersion(cycle); setLive(null); promotePendingVersion(newLive); } /** * Fetches the publication lifecycle. * * @return The associated lifecycle, null if there is none */ public Lifecycle getLifecycle() { s_log.debug("Resolving the item's lifecycle"); final Lifecycle lifecycle = LifecycleService.getLifecycle(this); if (lifecycle == null) { if (s_log.isDebugEnabled()) { s_log.debug("The item has no lifecycle; checking if the " + "public version has a lifecycle"); } final ContentItem pub = getPublicVersion(); if (pub == null) { s_log.debug("There is no public version; returning null"); return null; } else { final Lifecycle cyclelife = LifecycleService.getLifecycle(pub); if (s_log.isDebugEnabled()) { s_log.debug("The public version has a lifecycle; " + "returning " + cyclelife); } return cyclelife; } } else { if (s_log.isDebugEnabled()) { s_log.debug("Found " + lifecycle); } return lifecycle; } } /** * Return true if this item has been published. * * @return true if this item has a lifecycle, false otherwise */ public boolean isPublished() { return getLifecycle() != null; } /** * Apply a lifecycle to this content item. * * @param lifecycle The lifecycle */ public void setLifecycle(final Lifecycle lifecycle) { if (s_log.isDebugEnabled()) { s_log.debug("Setting lifecycle to " + lifecycle + " on " + this); } Assert.exists(lifecycle, Lifecycle.class); LifecycleService.setLifecycle(this, lifecycle); } // XXX domlay What is the relation of setLifecycle(Lifecycle) and // publish(LifecycleDefinition ...)? It doesn't seem coherent. /** * Remove the associated lifecycle. */ public void removeLifecycle(final ContentItem itemToRemove) { // XXX Should this method be static? Why does it take an // item? if (s_log.isDebugEnabled()) { s_log.debug("Removing lifecycle instance from item " + itemToRemove); } LifecycleService.removeLifecycle(itemToRemove); } private ContentBundle getBundle() { final ACSObject parent = getParent(); if (parent instanceof ContentBundle) { return (ContentBundle) parent; } else { return null; } } private void setBundle(final ContentBundle bundle) { setParent(bundle); } // // Category stuff // /** * @return all categories to which this item belongs */ public CategoryCollection getCategoryCollection() { final ContentBundle bundle = getBundle(); if (bundle != null) { return bundle.getCategoryCollection(); } return new CategorizedObject(this).getParents(); } /** * Returns an iterator over the categories associated with this content item * which is associated with the given use context * * @param useContext the category use context * * @return An iterator over all Categories to which this item belongs */ public Iterator getCategories(String useContext) { final ContentBundle bundle = getBundle(); if (bundle != null) { return bundle.getCategories(useContext); } Category root = Category.getRootForObject(getContentSection(), useContext); if (null == root) { s_log.warn("No root category for " + getContentSection().getOID().toString() + " with context " + useContext); return Collections.EMPTY_LIST.iterator(); } CategoryCollection cats = root.getDescendants(); cats.addEqualsFilter("childObjects.id", getID()); Collection categories = new LinkedList(); while (cats.next()) { categories.add(cats.getCategory()); } return categories.iterator(); } /** * Sets a category as the default/primary category for this item. Actual * default assignment is performed on the bundle if one exists. * * If this category is not already assigned to this item, then this method * also adds the category to the item. * * @param category The category to set as the default. * */ public void setDefaultCategory(Category category) { ContentBundle bundle = getBundle(); if (bundle != null) { bundle.setDefaultCategory(category); return; } CategorizedObject cObj = new CategorizedObject(this); cObj.setDefaultParentCategory(category); category.save(); return; } /** * Adds a category to this content item (or its bundle if one exists) * * @param category The category to add this item to * */ public void addCategory(Category category) { ContentBundle bundle = getBundle(); if (bundle != null) { bundle.addCategory(category); return; } category.addChild(this); category.save(); return; } /** * Removes a category from this content item (or its bundle if one exists) * * @param category The category to remove this item from * */ public void removeCategory(Category category) { ContentBundle bundle = getBundle(); if (bundle != null) { bundle.removeCategory(category); return; } category.removeChild(this); category.save(); return; } // // Versioning stuff // /** * Recursively copy this item, creating a clone. Reassign composite * associations from the copy to point to the copies of original items. This * method will not automatically transfer services (such as categories) to * the copy; the {@link #copyServicesFrom(ContentItem)} method should be * called to accomplish this.

NOTE: This method will also save the item * and all of its unpublished subitems. * * NOTE: This method should be final with the addition of makeCopy, but is * not just in case there are extensions in some PS code. The * 'non-finalness' of this method should be considered deprecated. * * @return the live version for this item * * @see #copyServicesFrom(ContentItem) */ public ContentItem copy() { return copy(null, false); } public ContentItem copy(String lang) { return copy(null, false, lang); } /** * Recursively copy this item, creating a clone. Reassign composite * associations from the copy to point to the copies of original items.

* NOTE: This method will save the item and all of its unpublished subitems. * * @param newParent The new parent item for this item * @param copyServices Copy services if true * * @return the new copy of the item * * @see #copyServicesFrom(ContentItem) */ final public ContentItem copy(final ContentItem newParent, final boolean copyServices) { ContentItem newItem = makeCopy(); if (newParent != null) { newItem.setParent(newParent); } if (copyServices) { newItem.copyServicesFrom(this); } return newItem; } final public ContentItem copy(final ContentItem newParent, final boolean copyServices, final String lang) { ContentItem newItem = makeCopy(lang); if (newParent != null) { newItem.setParent(newParent); } if (copyServices) { newItem.copyServicesFrom(this); } return newItem; } /** * Performs the actual mechanics of copying a content item. Non-final so * that subtypes can extend copying behavior. * * @return A new copy of the item */ protected ContentItem makeCopy() { if (s_log.isDebugEnabled()) { s_log.debug("Copy taking place", new Throwable("trace")); } final ContentItem newItem = new ObjectCopier().copyItem(this); // Doesn't seem like I should have to do this, but what the hell newItem.setContentSection(getContentSection()); newItem.save(); return newItem; } /** * Variant of {@link ACSObject#makeCopy} which allows to pass the (further) * language of the copy. * * @param language * * @return */ protected ContentItem makeCopy(String language) { if (s_log.isDebugEnabled()) { s_log.debug("Copy taking place", new Throwable("trace")); } LanguageAwareObjectCopier copier = new LanguageAwareObjectCopier( language); final ContentItem newItem = copier.copyItem(this); // Doesn't seem like I should have to do this, but what the hell newItem.setContentSection(getContentSection()); newItem.save(); return newItem; } /** * Transfer services, such as categories, from the passed-in item to this * item. This method should be called immediately after {@link ItemCopier#copy}, * as follows:

 Article newArticle = (Article)oldArticle.copyItem();
     * newArticle.copyServicesFrom(oldArticle);

* WARNING: This method will most likely crash if you call it twice in a * row. * * @param source the * ContentItem whose services will be copied * * @see #copy() */ public void copyServicesFrom(final ContentItem source) { ObjectCopier.copyServices(this, source); } /** * Recursively copy this item, creating a pending version. Reassign * composite associations from the pending version to point to the * pending/live versions of other items. * * NOTE: This method will also save the item and all of its unpublished * subitems. * * @param cycle the lifecycle to use. A null cycle implies that a live * version should be created. * * @return the new pending version for this item */ protected ContentItem createPendingVersion(final Lifecycle cycle) { if (s_log.isDebugEnabled()) { s_log.debug("Creating a pending version with lifecycle " + cycle); } return new VersionCopier(cycle).copyItem(this); } /** * Promote the specified pending version to live. Delete the old live * version, if any. * * @param pending The pending item to promote */ public void promotePendingVersion(final ContentItem pending) { if (s_log.isDebugEnabled()) { s_log.debug("Promoting pending version " + pending + " to live"); } assertDraft(); final boolean isNewVersionLive = pending.isLiveVersion(); final ContentItem live = getLiveVersion(); if (live != null) { if (live.equals(pending) && isNewVersionLive) { return; } // Remove the previous live version, if any setLive(null); } if (!isNewVersionLive) { // Recursively set the pending version's tag to "live" // and update the master's live_version_id pending.setVersionRecursively(LIVE); } setLive(pending); ContentBundle draftBundle = getBundle(); ContentBundle liveBundle = pending.getBundle(); if ((draftBundle != null) && (liveBundle != null) && (!liveBundle.isLiveVersion())) { draftBundle.promotePendingVersion(liveBundle); } save(); } /** * Recursively update the version attribute of the current content item to * the new value. Used by the lifecycle listener to promote a pending * version to live. * * @param version The new Version to set */ protected void setVersionRecursively(final String version) { s_log.debug("Recursively updating the version attribute of the " + "item"); new VersionUpdater(version).updateItemVersion(this); } /** * Recursively copy this item, creating a live version. Reassign component * associations from the live version to point to the live versions of other * items. * * @return the live version for this item */ public ContentItem createLiveVersion() { s_log.debug("Creating live version"); ContentItem pending = createPendingVersion(null); promotePendingVersion(pending); return pending; } /** * Copy the specified property (attribute or association) from the specified * source item. This method almost completely overrides the metadata-driven * methods in * ObjectCopier. ... * * ObjectCopier will no longer call it, so existing implementations need to * update to the new signature * * @deprecated use {@link #copyProperty(CustomCopy, Property, ItemCopier)} * instead */ protected final boolean copyProperty(final ContentItem source, final String attribute, final ItemCopier copier) { throw new UnsupportedOperationException( "use copyProperty(CustomCopy, Property, ItemCopier) for copying"); } /** * Copy the specified property (attribute or association) from the specified * source item. 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.copy(srcItem, this, * riginalChild, property); An attempt to call any other method in * order to copy the child will most likely have disastrous consequences. In * fact, this copier method should generally be called for any DomainObject * copies, later making custom changes, unless the copying behavior itself * is different from the default (or the item should not be copied at all at * this point). * * * If a subclass of a class which implements CustomCopy overrides this * method, it should return * super.copyProperty for properties which do not need custom * behavior in order to indicate that it is not interested in handling the * property in any special way. * * As a hypothetical example (no longer reflected in Article itself), the {@link Article} * class extends * ContentItem. It defines an association to 0..n * {@link ImageAsset}. Unfortunately, the association has "order_n" and * "caption" link attributes, which cannot be copied automatically, since * the persistence system doesn't know enough about them. The following * sample code from the {@link Article} class ensures that images are copied * correctly: * *


     * public boolean copyProperty(CustomCopy srcItem, Property property, ItemCopier copier) {
     *
     *  String attrName = property.getname()
     *   // We only care about copying images; all other properties should
     *   // be handled in a default manner
     *   if (!attrName.equals(IMAGES))
     *     return super.copyProperty(srcItem, property, copier);
     *
     *   // The source item is guaranteed to be of the correct type
     *   Article src = (Article)srcItem;
     *
     *   // Retrieve images from the source
     *   ImageAssetCollection srcImages = src.getImages();
     *
     *   // Copy each image using the passed-in copier
     *   while(srcImages.next()) {
     *     ImageAsset srcImage = srcImages.getImage();
     *
     *     // Images may be shared between items, and so they are not
     *     // composite. Thus, we are going to pass false to the object
     *     // copier in the second parameter
     *     ImageAsset newImage = (ImageAsset)copier.copy(srcItem, this, srcImage, property);
     *
     *     // Add the new image to the new item
     *     addImage(newImage, src.getCaption(srcImage));
     *   }
     *
     *   // Tell the automated copying service that we have handled this
     *   // property
     *   return true;
     * }
     * 
* * Note that for top-level item associations, * VersionCopier will return * null since the actual associatons are only created at "go * live" time, so the ability to override behavior for top-level item * associations is somewhat limited. A common case for needing to override * copyProperty to handle these associations would be to auto-publish the * target of the association but still handle the association updating * normally. In this case, copyProperty would call publish() separately on * the associated object, and then return * false to indicate that the copier should continue to handle * the association normally. * * @param source the source CustomCopy item * @param property the property to copy * @param copier a temporary class that is able to copy a child item * correctly. * * @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 source, final Property property, final ItemCopier copier) { String attribute = property.getName(); if (CHILDREN.equals(attribute)) { return true; } // Ignore live and pending versions. if (VERSIONS.equals(attribute)) { return true; } // Don't copy path denormalization. if (ANCESTORS.equals(attribute)) { return true; } //don't copy BasicAuditingTrail if (AUDITING.equals(attribute)) { return true; } if ("categories".equals(attribute)) { return true; } // If live Bundle already exists, recategorize. if (PARENT.equals(attribute)) { ACSObject parent = ((ContentItem) source).getParent(); if (parent != null && copier.getCopyType() == ItemCopier.VERSION_COPY) { if (parent instanceof ContentBundle) { final ContentBundle bundle = (ContentBundle) parent; final ContentBundle oldLiveBundle = (ContentBundle) bundle.getPublicVersion(); //jensp 2012-03-07 Changes to the ContentBundle were not //published because the ContentBundle was not republished. //Moved the next lines out of the if below to enable //republishing of the ContentBundle final ContentBundle liveBundle = (ContentBundle) bundle. createPendingVersion(null); /* * if (liveBundle == null) { } else { Set liveCatSet = new * HashSet(); Set draftCatSet = new HashSet(); * * CategoryCollection liveCategories = * liveBundle.getCategoryCollection(); while * (liveCategories.next()) { * liveCatSet.add(liveCategories.getCategory()); } * liveCategories.close(); * * CategoryCollection draftCategories = * bundle.getCategoryCollection(); while * (draftCategories.next()) { * draftCatSet.add(draftCategories.getCategory()); } * draftCategories.close(); * * Set catsToRemove = new HashSet(liveCatSet); * catsToRemove.removeAll(draftCatSet); Set catsToAdd = new * HashSet(draftCatSet); catsToAdd.removeAll(liveCatSet); * * Iterator removeIter = catsToRemove.iterator(); while * (removeIter.hasNext()) { liveBundle.removeCategory( * (Category) removeIter.next()); } Iterator addIter = * catsToAdd.iterator(); while (addIter.hasNext()) { * liveBundle.addCategory((Category) addIter.next()); } * * } */ if (oldLiveBundle != null) { final ItemCollection instances = oldLiveBundle. getInstances(); while (instances.next()) { liveBundle.addInstance( instances.getContentItem()); } } setBundle(liveBundle); return true; } else if (parent instanceof Folder) { Folder folder = (Folder) parent; Folder liveFolder = (Folder) folder.getLiveVersion(); if (liveFolder == null) { liveFolder = (Folder) folder.createLiveVersion(); } setParent(liveFolder); return true; } } } /*final AssociationCopierLoader assocCopierLoader = AssociationCopierLoader.getInstance(); final AssociationCopier assocCopier = assocCopierLoader. getAssociationCopierFor(property, source); if (assocCopier != null) { return assocCopier.copyProperty(source, property, copier); }*/ if (source instanceof ContentItem) { final ContentItem sourceItem = (ContentItem) source; final Object value = sourceItem.get(property.getName()); if (value instanceof DataCollection) { final DataCollection collection = (DataCollection) value; while(collection.next()) { DomainObject obj = DomainObjectFactory.newInstance(collection.getDataObject()); if (obj instanceof ContentItem) { final ContentItem item = (ContentItem) obj; collection.close(); return item.copyReverseProperty(source, this, property, copier); } } } } return false; } public boolean copyReverseProperty(final CustomCopy source, final ContentItem liveItem, final Property property, final ItemCopier copier) { return false; } /** * Copy services from the source item. This method is the analogue of the {@link #copyProperty} * method above. The object copier will call this method whenever an item * has been successfully published, in order to transfer services such as * categorization or permissions to the live version.

This method is * requied to return false to signal the object copier to transfer default * services from the source item; or true in order to abort further * processing of services. * * @return true to tell the object copier to stop copying services for this * item, false otherwise */ public boolean copyServices(ContentItem srcItem) { return false; } // // Multilingual content // /** * Language of the content item. * * @return ISO639 2-letter language code */ public String getLanguage() { return (String) get(LANGUAGE); } /** * Set the language of the content item. * * @param language ISO639 2-letter language code */ public void setLanguage(String language) { set(LANGUAGE, language); m_reporter.mutated("language"); } /** * Get the locale for this content item. * * @return The locale of the item @post return != null */ public com.arsdigita.globalization.Locale getLocale() { Locale locale = null; // SystemLocaleProvider slp = new SystemLocaleProvider(); // slp.getLocale() try { locale = Locale.fromJavaLocale(new java.util.Locale(getLanguage(), "")); } catch (GlobalizationException e) { s_log.warn("GlobalizationException thrown in getLocale()", e); throw new UncheckedWrapperException(e.getMessage()); } Assert.exists(locale, Locale.class); return locale; } /** * Assert that this item is a draft version */ public final void assertDraft() { Assert.isEqual(DRAFT, getVersion()); } /** * Assert that this item is a pending version */ public final void assertPending() { Assert.isEqual(PENDING, getVersion()); } /** * Assert that this item is a live version */ public final void assertLive() { Assert.isEqual(LIVE, getVersion()); } // // Deprecated methods and classes // /** * Assert that this item is a top-level master object * * @deprecated with no replacement */ public final void assertMaster() { Assert.isTrue(isMaster(), "Item " + getOID() + " is a top-level item"); } // // Private utility methods and classes // /** * Caches a version of this item. */ private class VersionCache { private ContentItem m_version; private boolean m_cached; VersionCache() { m_version = null; m_cached = false; } boolean isCached() { return m_cached; } ContentItem get() { Assert.isTrue(m_cached); return m_version; } ContentItem set(final ContentItem version) { m_version = version; m_cached = true; return m_version; } void clear() { m_version = null; m_cached = false; } } /** * Remove any Links pointing to this item before deletion. XXX This should * go away when one-way association targets can specify the equivalent of on * delete set null */ @Override protected void beforeDelete() { super.beforeDelete(); // remove Link associations to this DataCollection dc = SessionManager.getSession().retrieve( Link.BASE_DATA_OBJECT_TYPE); dc.addEqualsFilter(Link.TARGET_ITEM + "." + ACSObject.ID, getID()); while (dc.next()) { Link link = (Link) DomainObjectFactory.newInstance( dc.getDataObject()); link.setTargetItem(null); } // remove ContentGroup associations to this dc = SessionManager.getSession().retrieve( ContentGroupAssociation.BASE_DATA_OBJECT_TYPE); dc.addEqualsFilter(ContentGroupAssociation.CONTENT_ITEM + "." + ACSObject.ID, getID()); while (dc.next()) { ContentGroupAssociation groupAssoc = (ContentGroupAssociation) DomainObjectFactory. newInstance(dc.getDataObject()); groupAssoc.setContentItem(null); } } /** * Overriding the Auditing interface in order to use the denormalized * information */ /** * Gets the user who created the object. May be null. * * @return the user who created the object. */ @Override public User getCreationUser() { return m_audit_trail.getCreationUser(); } /** * Gets the creation date of the object. * * @return the creation date. */ @Override public Date getCreationDate() { return m_audit_trail.getCreationDate(); } /** * Gets the IP address associated with creating an object. May be null. * * @return the creation IP address. */ @Override public String getCreationIP() { return m_audit_trail.getCreationIP(); } /** * Gets the user who last modified the object. May be null. * * @return the last modifying user. */ @Override public User getLastModifiedUser() { return m_audit_trail.getLastModifiedUser(); } /** * Gets the last modified date. * * @return the last modified date. */ @Override public Date getLastModifiedDate() { return m_audit_trail.getLastModifiedDate(); } /** * Gets the last modified IP address. May be null. * * @return the IP address associated with the last modification. */ @Override public String getLastModifiedIP() { return m_audit_trail.getLastModifiedIP(); } /** *

Override this to explicit that your content items have extra XML to * generate. An overriding implementation should call the super method, and * append its generators to the list. Example:

*
     * {@code
     * @Override
     * public List getExtraXMLGenerators() {
     *   final List generators =
     *     super.getExtraXMLGenerators();
     *
     *   generators.add(new YourExtraXMLGenerator());
     *
     *   return generators;
     * }
     * }
     * 
* * @return A list of all extra XML Generators for this content item. */ public List getExtraXMLGenerators() { return new ArrayList(); } /** *

Override this method if your content items have extra XML for list * views. You may return the same XML generators as in * {@link #getExtraXMLGenerators()}. But beware: The page state passed to * generators returned by this method will may be null.

* * @return A list of all extra XML Generators for lists views of this * content item. */ public List getExtraListXMLGenerators() { return new ArrayList(); } }