/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the
 * License at http://www.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
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.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
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 ---&gt; I &lt;--- B
 * </pre></blockquote>
 *
 * When A is published, creating a pending version A', I will be
 * published as well:
 *
 * <blockquote><pre>
 * A ---&gt; I &lt;--- B
 * A'---&gt; I'
 * </pre></blockquote>
 *
 * When B is later published as B', B' will reference I':
 *
 * <blockquote><pre>
 * A ---&gt; I &lt;--- B
 * A'---&gt; I'&lt;--- 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 &lt;sfreidin@redhat.com&gt;
 *
 * @version $Id: //cms/dev/src/com/arsdigita/cms/ContentItem.java#106 $
 */
public class ContentItem extends VersionedACSObject implements CustomCopy {
    public static final String versionId =
        "$Id: //cms/dev/src/com/arsdigita/cms/ContentItem.java#106 $" +
        "$Author: dennis $" +
        "$DateTime: 2004/04/07 16:07:11 $";

    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, ci.getContentSection());
                    } else {
                        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
     */
    public String getBaseDataObjectType() {
        return BASE_DATA_OBJECT_TYPE;
    }

    /**
     * Publicized getter method for use by metadata forms.
     */
    public Object get(final String key) {
        return super.get(key);
    }

    /**
     * Public setter method for use by metadata forms.
     */
    public void set(final String key, final Object value) {
        super.set(key, value);
    }


    /**
     * For new content items, sets the associated content type if it
     * has not been already set.
     */
    protected void beforeSave() {
        m_wasNew = isNew();

        super.beforeSave();

        if (m_wasNew) {
            // Set the default content section.
            if (getContentSection() == null) {
                setDefaultContentSection();
            }
        }
    }

    protected void afterSave() {
        super.afterSave();

        // Set the object's context to its parent object for
        // permissioning.
        if (m_wasNew) {
            final ACSObject parent = getParent();

            if (parent == null) {
                PermissionService.setContext(this, getContentSection());
            } else {
                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");
    }

    /**
     * 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 filter for ancestor check
        int iLength;
        if (includeSelf) {
            iLength = ids.length();
        } else {
            iLength = ids.length() - 1;
        }

        //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.truth(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).unpublish();
            }

            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);
        }

        final Lifecycle cycle = cycleDef.createFullLifecycle
            (startDate, PUBLISH_LISTENER_CLASS);

        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.truth(PENDING.equals(pending.getVersion()) ||
                         LIVE.equals(pending.getVersion()),
                         "The new pending item must be pending or live; " +
                         "instead it is " + pending.getVersion());
        }
        return pending;
    }

    /**
     * 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();
    }

    /**
     * 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) {
            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);
        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.equal(DRAFT, getVersion());
    }

    /**
     * Assert that this item is a pending version
     */
    public final void assertPending() {
        Assert.equal(PENDING, getVersion());
    }

    /**
     * Assert that this item is a live version
     */
    public final void assertLive() {
        Assert.equal(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.truth(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.truth(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
     */
    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.
     */

    public User getCreationUser() {
        return m_audit_trail.getCreationUser();
    }

    /**
     * Gets the creation date of the object.
     * @return the creation date.
     */

    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.
     */

    public String getCreationIP() {
        return m_audit_trail.getCreationIP();
    }

    /**
     * Gets the user who last modified the object. May be null.
     * @return the last modifying user.
     */

    public User getLastModifiedUser() {
        return m_audit_trail.getLastModifiedUser();
    }

    /**
     * Gets the last modified date.
     * @return the last modified date.
     */

    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.
     */

    public String getLastModifiedIP() {
        return m_audit_trail.getLastModifiedIP();
    }
}
