2115 lines
66 KiB
Java
Executable File
2115 lines
66 KiB
Java
Executable File
/*
|
|
* 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.
|
|
*
|
|
* <h4>Publishing Items</h4>
|
|
*
|
|
* 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
|
|
* <code>createPendingVersion</code> 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
|
|
* <strong>composite</strong> relations. After that, the item will
|
|
* copy all of its non-composite associations "by reference". If a
|
|
* target of any association is a <code>ContentItem</code>, the cloned
|
|
* item will reference the live or pending version of the target item.
|
|
*
|
|
* For example, consider <code>Articles</code> A and B, both of which
|
|
* reference an <code>ImageAsset</code> I:
|
|
*
|
|
* <blockquote><pre>
|
|
* A ---> I <--- B
|
|
* </pre></blockquote>
|
|
*
|
|
* When A is published, creating a pending version A', I will be
|
|
* published as well:
|
|
*
|
|
* <blockquote><pre>
|
|
* A ---> I <--- B
|
|
* A'---> I'
|
|
* </pre></blockquote>
|
|
*
|
|
* When B is later published as B', B' will reference I':
|
|
*
|
|
* <blockquote><pre>
|
|
* A ---> I <--- B
|
|
* A'---> I'<--- B'
|
|
* </pre></blockquote>
|
|
*
|
|
* In order to work correctly with the automatic publishing code,
|
|
* every subclass of <code>ContentItem</code> (such as "FooItem
|
|
* extends ContentItem") <b>must</b> adhere to the following
|
|
* guidelines:
|
|
*
|
|
* <ul>
|
|
* <li>The subclass must have a constructor of the form
|
|
* <blockquote><pre>
|
|
* public FooItem(DataObject obj) {
|
|
* super(obj);
|
|
* }
|
|
* </pre></blockquote>
|
|
* </li>
|
|
*
|
|
* <li>The subclass must have a constructor of the form
|
|
* <blockquote><pre>
|
|
* public FooItem(String type) {
|
|
* super(type);
|
|
*
|
|
* // Do more stuff here.
|
|
* }
|
|
* </pre></blockquote>
|
|
* </li>
|
|
*
|
|
* <li>If the PDL file for the subclass contains any link
|
|
* attributes, read-only associations, or in general any
|
|
* associations that are not standard, the subclass must implement
|
|
* the {@link #copyProperty(ContentItem, String, ItemCopier)}
|
|
* method. For examples on how to implement it, see the methods's
|
|
* javadoc and the sample implementation in the {@link Article}
|
|
* class.</li>
|
|
* </ul>
|
|
*
|
|
* 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 <em>only</em>
|
|
* 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 <em>critically important</em> 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 <code>publish</code> 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 <em>requirement</em> 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.
|
|
*
|
|
* <h4>Copying Items</h4>
|
|
*
|
|
* 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>
|
|
*
|
|
* @version $Id: ContentItem.java 1621 2007-09-13 12:43:12Z chrisg23 $
|
|
*/
|
|
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 <code>DataObject</code> is retrieved
|
|
* from the persistent storage mechanism with an <code>OID</code>
|
|
* specified by <code>oid</code>.
|
|
*
|
|
* @param oid The <code>OID</code> for the retrieved
|
|
* <code>DataObject</code>
|
|
*/
|
|
public ContentItem(final OID oid) throws DataObjectNotFoundException {
|
|
super(oid);
|
|
}
|
|
|
|
/**
|
|
* Constructor. The contained <code>DataObject</code> is retrieved
|
|
* from the persistent storage mechanism with an <code>OID</code>
|
|
* specified by <code>id</code> and
|
|
* <code>ContentItem.BASE_DATA_OBJECT_TYPE</code>.
|
|
*
|
|
* @param id The <code>id</code> for the retrieved
|
|
* <code>DataObject</code>
|
|
*/
|
|
public ContentItem(final BigDecimal id)
|
|
throws DataObjectNotFoundException {
|
|
this(new OID(BASE_DATA_OBJECT_TYPE, id));
|
|
}
|
|
|
|
/**
|
|
* Constructor. Retrieves or creates a content item using the
|
|
* <code>DataObject</code> argument.
|
|
*
|
|
* @param obj The <code>DataObject</code> 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 <code>String</code> 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 (<code>DomainObject</code>)
|
|
* 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 <code>ACSObject</code> 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 <code>ItemCollection</code> 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 <code>cms_items.section_id</code> is a denormalization,
|
|
* this method may return null even if the item "belongs" to a
|
|
* content section. For example, calling
|
|
* <code>getContentSection()</code> on an Article's
|
|
* <code>ImageAsset</code> 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 <tt>/x/y/z</tt> where <tt>x</tt> and
|
|
* <tt>y</tt> are the names of the item's grandparent and parent
|
|
* respectively, and <tt>z</tt> is the name of the item itself.
|
|
*
|
|
* The item's root is the ancestor reachable through repeated
|
|
* <code>getParent()</code> calls whose parent is
|
|
* <code>null</code>. 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.
|
|
*
|
|
* <p> The item's root is the ancestor reachable through repeated
|
|
* <code>getParent()</code> calls whose parent is <code>null</code>. 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 <code>includeSelf</code> is <code>false</code>)
|
|
* 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.
|
|
*
|
|
* <p> The item's root is the ancestor reachable through repeated
|
|
* <code>getParent()</code> calls whose parent is <code>null</code>. This
|
|
* is usually a folder, but may be any {@see
|
|
* com.arsdigita.kernel.ACSObject}.
|
|
*
|
|
* @param includeSelf a <code>boolean</code> 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 <code>true</code> if this item is a <code>DRAFT</code>
|
|
* version.
|
|
*
|
|
* @return <code>true</code> if this item is a <code>DRAFT</code>
|
|
* version
|
|
*/
|
|
public boolean isDraftVersion() {
|
|
return DRAFT.equals(getVersion());
|
|
}
|
|
|
|
/**
|
|
* Returns the <code>DRAFT</code> 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
|
|
* <code>ContentItem</code>, possibly this item
|
|
* @deprecated use {@link #getDraftVersion()} instead
|
|
*/
|
|
public ContentItem getWorkingVersion() {
|
|
return getDraftVersion();
|
|
}
|
|
|
|
/**
|
|
* Returns <code>true</code> if this item is a
|
|
* <code>PENDING</code> version.
|
|
*
|
|
* @return <code>true</code> if <code>this</code> is one of the
|
|
* pending versions
|
|
*/
|
|
public boolean isPendingVersion() {
|
|
return PENDING.equals(getVersion());
|
|
}
|
|
|
|
/**
|
|
* Returns one <code>PENDING</code> 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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <p>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.</p>
|
|
*
|
|
* @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 <code>true</code> if this item is a <code>LIVE</code>
|
|
* version.
|
|
*
|
|
* @return <code>true</code> if <code>this</code> 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 <code>ContentItem</code> 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 <code>ContentItem</code> 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 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<code> if this content item has a live
|
|
* version, or if it <em>is</em> the live version
|
|
*/
|
|
public boolean isLive() {
|
|
return 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 <code>setLive(null)</code>.
|
|
*/
|
|
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.
|
|
* <p>
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* Recursively copy this item, creating a clone.
|
|
* Reassign composite associations from the copy to point
|
|
* to the copies of original items.
|
|
* <p>
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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:
|
|
* <blockquote><pre><code> Article newArticle = (Article)oldArticle.copyItem();
|
|
* newArticle.copyServicesFrom(oldArticle);</code></pre></blockquote>
|
|
* <p>
|
|
* WARNING: This method will most likely crash if you call it twice
|
|
* in a row.
|
|
*
|
|
* @param source the <code>ContentItem</code> 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 <code>ObjectCopier</code>. ...
|
|
*
|
|
* 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 <code>ObjectCopier</code>. If
|
|
* the property in question is an association to
|
|
* <code>ContentItem</code>(s), this method should <b>only</b>
|
|
* call <code>FooContentItem newChild = copier.copy(srcItem, this,
|
|
* riginalChild, property);</code> 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 <code>super.copyProperty</code>
|
|
* 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
|
|
* <code>ContentItem</code>. 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:
|
|
*
|
|
* <blockquote><pre><code>
|
|
* 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;
|
|
* }
|
|
* </code></pre></blockquote>
|
|
*
|
|
* Note that for top-level item associations,
|
|
* <code>VersionCopier</code> will return <code>null</code> 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 <code>false</code> 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) {
|
|
|
|
ContentBundle bundle = (ContentBundle) parent;
|
|
ContentBundle liveBundle = (ContentBundle) bundle.getPublicVersion();
|
|
if (liveBundle == null) {
|
|
liveBundle = (ContentBundle) bundle.createPendingVersion(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());
|
|
}
|
|
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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.
|
|
* <p>
|
|
* 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();
|
|
}
|
|
protected static List extraXMLGenerators = new ArrayList();
|
|
|
|
/**
|
|
* Override this to explicit that your content items
|
|
* have extra XML to generate.
|
|
* ATM This is used in navigation for GreetingItems.
|
|
*/
|
|
public List getExtraXMLGenerators() {
|
|
return extraXMLGenerators;
|
|
}
|
|
|
|
}
|