/* * Content.java * * This work is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * * This work 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 * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * * Copyright (c) 2004-2005 Per Cederberg. All rights reserved. */ package org.liquidsite.core.content; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import org.liquidsite.core.data.AttributeData; import org.liquidsite.core.data.AttributePeer; import org.liquidsite.core.data.ContentData; import org.liquidsite.core.data.ContentPeer; import org.liquidsite.core.data.DataObjectException; import org.liquidsite.core.data.DataSource; import org.liquidsite.util.log.Log; /** * The base class for all content objects. This class should NOT be * instantiated directly unless in a backup/restore scenario. * Otherwise the proper subclass should ALWAYS be created. * * @author Per Cederberg, <per at percederberg dot net> * @version 1.0 */ public class Content extends PersistentObject { /** * The class logger. */ private static final Log LOG = new Log(Content.class); /** * The site content category. */ public static final int SITE_CATEGORY = 1; /** * The translator content category. */ public static final int TRANSLATOR_CATEGORY = 2; /** * The folder content category. */ public static final int FOLDER_CATEGORY = 3; /** * The page content category. */ public static final int PAGE_CATEGORY = 4; /** * The file content category. */ public static final int FILE_CATEGORY = 5; /** * The template content category. */ public static final int TEMPLATE_CATEGORY = 6; /** * The section content category. */ public static final int SECTION_CATEGORY = 11; /** * The document content category. */ public static final int DOCUMENT_CATEGORY = 12; /** * The forum content category. */ public static final int FORUM_CATEGORY = 13; /** * The topic content category. */ public static final int TOPIC_CATEGORY = 14; /** * The post content category. */ public static final int POST_CATEGORY = 15; /** * The permitted content name characters. */ public static final String NAME_CHARS = UPPER_CASE + LOWER_CASE + NUMBERS + BINDERS + "."; /** * The content data object. */ private ContentData data; /** * The previous content revision number. This number is set only * when reading a content data object from the database, and is * used to track changes to the revision number. */ private int oldRevision = 0; /** * The content attribute data objects. The data objects are * indexed by the attribute name. */ private HashMap attributes = new HashMap(); /** * The names of content attributes added. */ private ArrayList attributesAdded = new ArrayList(); /** * The content attribute data objects removed. */ private ArrayList attributesRemoved = new ArrayList(); /** * Creates a new content object with default values. The content * identifier will be set to the next available one after storing * to the database, and the content revision is set to zero (0). * <p> * This constructor should NOT BE CALLED directly unless you know * what you are doing. It is supposed to be called by the * constructors in the subclasses and is public only to simplify * the backup and restore operations. * * @param manager the content manager to use * @param domain the domain * @param category the category */ public Content(ContentManager manager, Domain domain, int category) { super(manager, false); this.data = new ContentData(); this.data.setString(ContentData.DOMAIN, domain.getName()); this.data.setInt(ContentData.CATEGORY, category); this.data.setDate(ContentData.MODIFIED, new Date()); } /** * Creates a new content object. This constructor will also read * all content attributes from the database. * * @param manager the content manager to use * @param data the content data object * @param src the data source to use * * @throws ContentException if the database couldn't be accessed * properly */ protected Content(ContentManager manager, ContentData data, DataSource src) throws ContentException { super(manager, true); this.data = data; this.oldRevision = data.getInt(ContentData.REVISION); try { doReadAttributes(src); } catch (DataObjectException e) { LOG.error(e.getMessage()); throw new ContentException(e); } } /** * Checks if this content object equals another object. This * method will only return true if the other object is a content * object with the same id. * * @param obj the object to compare with * * @return true if the other object is identical, or * false otherwise */ public boolean equals(Object obj) { if (obj instanceof Content) { return equals((Content) obj); } else { return false; } } /** * Checks if this content object equals another object. This * method will only return true if the other object is a content * object with the same id. * * @param obj the object to compare with * * @return true if the other object is identical, or * false otherwise */ public boolean equals(Content obj) { return obj != null && getId() == obj.getId(); } /** * Returns a string representation of this object. * * @return a string representation of this object */ public String toString() { return getName(); } /** * Checks if this content object revision is online. Note that * this method does NOT take other revisions into account. A * later revision may set different online and offline dates, * causing the content object to actually be offline. * * @return true if the content object revision is online, or * false otherwise */ public boolean isOnline() { Date online = getOnlineDate(); Date offline = getOfflineDate(); Date now = new Date(); return online != null && online.before(now) && (offline == null || offline.after(now)); } /** * Checks if this content object revision is the latest one. The * revision is considered the latest if it is a work revision, or * if no work revision exists and it is the latest published * revision. * * @return true if this content revision is the latest one, or * false otherwise */ public boolean isLatestRevision() { int status = data.getInt(ContentData.STATUS); return (status & ContentPeer.LATEST_STATUS) > 0; } /** * Checks if this content object revision is the published one. * The revision is considered the published one if it is the * revision with the highest revision number. Note that working * revision always have a revision number of zero. * * @return true if this content revision is the published one, or * false otherwise */ public boolean isPublishedRevision() { int status = data.getInt(ContentData.STATUS); return (status & ContentPeer.PUBLISHED_STATUS) > 0; } /** * Returns the content domain. * * @return the content domain * * @throws ContentException if no content manager is available */ public Domain getDomain() throws ContentException { return getContentManager().getDomain(getDomainName()); } /** * Returns the content domain name. * * @return the content domain name */ public String getDomainName() { return data.getString(ContentData.DOMAIN); } /** * Returns the content identifier. * * @return the content identifier */ public int getId() { return data.getInt(ContentData.ID); } /** * Sets the content identifier (RESTORE ONLY). This method should * NOT BE CALLED unless you know what you are doing. Changing the * content identifier may cause irrepairable harm to the content * database which is why content identfiers are normally assigned * automatically. This method only exists to simplify the backup * and restore operations. * * @param id the new content identifier */ public void setId(int id) { data.setInt(ContentData.ID, id); } /** * Returns the content revision number. * * @return the content revision number */ public int getRevisionNumber() { return data.getInt(ContentData.REVISION); } /** * Sets the content revision number. Note that the revision zero * (0) is treated specially in several ways. First, when moving * from revision zero, the old zero revision will be deleted from * the database (corresponding to a revision promotion). Also, * when storing a non-zero revision, permissions to publish are * required by save(). * * @param revision the new content revision number */ public void setRevisionNumber(int revision) { AttributeData attr; Iterator iter; data.setInt(ContentData.REVISION, revision); iter = attributes.values().iterator(); while (iter.hasNext()) { attr = (AttributeData) iter.next(); attr.setInt(AttributeData.REVISION, revision); } } /** * Returns the content category. * * @return the content category */ public int getCategory() { return data.getInt(ContentData.CATEGORY); } /** * Returns the content name. * * @return the content name */ public String getName() { return data.getString(ContentData.NAME); } /** * Sets the content name. * * @param name the new name */ public void setName(String name) { data.setString(ContentData.NAME, name); } /** * Returns the content parent. * * @return the content parent * * @throws ContentException if the database couldn't be accessed * properly */ public Content getParent() throws ContentException { return getParent(getContentManager()); } /** * Returns the content parent. * * @param manager the content manager to use * * @return the content parent, or * null if the object has no parent * * @throws ContentException if the database couldn't be accessed * properly */ public Content getParent(ContentManager manager) throws ContentException { int parent = getParentId(); if (parent <= 0) { return null; } else { return manager.getContent(parent); } } /** * Sets the content parent. * * @param parent the new parent, or null for none */ public void setParent(Content parent) { if (parent == null) { setParentId(0); } else { setParentId(parent.getId()); } } /** * Returns the content parent identifier. * * @return the content parent identifier */ public int getParentId() { return data.getInt(ContentData.PARENT); } /** * Sets the content parent identifier. * * @param parent the new parent identifier */ public void setParentId(int parent) { data.setInt(ContentData.PARENT, parent); } /** * Returns the content publishing online date. * * @return the content publishing online date */ public Date getOnlineDate() { return data.getDate(ContentData.ONLINE); } /** * Sets the content publishing online date. * * @param online the new publishing online date, or null */ public void setOnlineDate(Date online) { if (online != null && online.getTime() == 0) { online = null; } data.setDate(ContentData.ONLINE, online); } /** * Returns the content publishing offline date. * * @return the content publishing offline date */ public Date getOfflineDate() { return data.getDate(ContentData.OFFLINE); } /** * Sets the content publishing offline date. * * @param offline the new publishing offline date, or null */ public void setOfflineDate(Date offline) { if (offline != null && offline.getTime() == 0) { offline = null; } data.setDate(ContentData.OFFLINE, offline); } /** * Returns the content last modification date. * * @return the content last modification date */ public Date getModifiedDate() { return data.getDate(ContentData.MODIFIED); } /** * Sets the content last modification date (RESTORE ONLY). This * method should NOT BE CALLED unless you know what you are * doing. The date set here will always be overwritten by the * save method. This method only exists to simplify the backup * and restore operations. * * @param modified the new last modification date */ public void setModifiedDate(Date modified) { data.setDate(ContentData.MODIFIED, modified); } /** * Returns the content last modification author. The author name * is set automatically by the save method. * * @return the content last modification author * * @throws ContentException if the database couldn't be accessed * properly */ public User getAuthor() throws ContentException { return getContentManager().getUser(getDomain(), getAuthorName()); } /** * Returns the content last modification author. The author name * is set automatically by the save method. * * @return the content last modification author */ public String getAuthorName() { return data.getString(ContentData.AUTHOR); } /** * Sets the content last modification author (RESTORE ONLY). This * method should NOT BE CALLED unless you know what you are * doing. The author name set here will always be overwritten by * the save method. This method only exists to simplify the * backup and restore operations. * * @param author the content last modification author */ public void setAuthorName(String author) { data.setString(ContentData.AUTHOR, author); } /** * Returns the content revision comment. * * @return the content revision comment */ public String getComment() { return data.getString(ContentData.COMMENT); } /** * Sets the content revision comment. * * @param comment the content revision comment */ public void setComment(String comment) { data.setString(ContentData.COMMENT, comment); } /** * Returns an iterator for all the attribute names (BACKUP ONLY). * This method should NOT BE CALLED unless you know what you are * doing. It provides direct access to the content attributes * that should normally be accessed through the various helper * methods in each subclass. This method is only public to * simplify the backup and restore operations. * * @return an iterator for all the attribute names */ public Iterator getAttributeNames() { return attributes.keySet().iterator(); } /** * Returns a content attribute value (BACKUP ONLY). This method * should NOT BE CALLED unless you know what you are doing. It * provides direct access to the content attributes that should * normally be accessed through the various helper methods in * each subclass. This method is only public to simplify the * backup and restore operations. * * @param name the content attribute name * * @return the content attribute value, or * null if not found */ public String getAttribute(String name) { AttributeData attr; attr = (AttributeData) attributes.get(name); if (attr == null) { return null; } else { return attr.getString(AttributeData.DATA); } } /** * Sets a content attribute value (RESTORE ONLY). If the * attribute does not exist it will be created. This method * should NOT BE CALLED unless you know what you are doing. It * provides direct access to the content attributes that should * normally be accessed through the various helper methods in * each subclass. This method is only public to simplify the * backup and restore operations. * * @param name the content attribute name * @param value the content attribute value */ public void setAttribute(String name, String value) { AttributeData attr; attr = (AttributeData) attributes.get(name); if (value == null) { if (attr != null) { attributesRemoved.add(attr); attributes.remove(name); } } else { if (attr == null) { attr = new AttributeData(); attr.setString(AttributeData.DOMAIN, getDomainName()); attr.setInt(AttributeData.CONTENT, getId()); attr.setInt(AttributeData.REVISION, getRevisionNumber()); attr.setString(AttributeData.NAME, name); attributes.put(name, attr); attributesAdded.add(name); } attr.setString(AttributeData.DATA, value); } } /** * Returns the highest content object revision number available. * Note that this may not be the most recent revision, as a * working revision (zero) may exist. * * @return the highest revision number in the database, or * -1 if no revisions are in the database * * @throws ContentException if the database couldn't be accessed * properly */ public int getMaxRevisionNumber() throws ContentException { Content[] revisions = getAllRevisions(); int max = -1; for (int i = 0; i < revisions.length; i++) { if (max < revisions[i].getRevisionNumber()) { max = revisions[i].getRevisionNumber(); } } return max; } /** * Returns the specified content object revision. * * @param revision the content revision * * @return the content object revision found, or * null if no matching content existed * * @throws ContentException if the database couldn't be accessed * properly */ public Content getRevision(int revision) throws ContentException { return InternalContent.findByRevision(getContentManager(), getId(), revision); } /** * Returns an array of all content object revisions. * * @return an array of the content object revisions found * * @throws ContentException if the database couldn't be accessed * properly */ public Content[] getAllRevisions() throws ContentException { return InternalContent.findById(getContentManager(), getId()); } /** * Returns the lock applicable to this content object. If no lock * has been set on this object, null will be returned. * * @return the content lock object found, or * null if this object is not locked * * @throws ContentException if the database couldn't be accessed * properly */ public Lock getLock() throws ContentException { return Lock.findByContent(getContentManager(), this); } /** * Returns the permission list applicable to this content object. * If this object has no permissions either an empty list or the * inherited permission list will be returned. * * @param inherit the search inherited permissions flag * * @return the permission list for this object * * @throws ContentException if the database couldn't be accessed * properly */ public PermissionList getPermissions(boolean inherit) throws ContentException { return getContentManager().getPermissions(this, inherit); } /** * Deletes this content revision from the database. * * @param user the user performing the operation * * @throws ContentException if the database couldn't be accessed * properly * @throws ContentSecurityException if the user specified didn't * have write permissions */ public void deleteRevision(User user) throws ContentException, ContentSecurityException { DataSource src = getDataSource(getContentManager()); // Delete from database try { SecurityManager.getInstance().checkDelete(user, this); ContentPeer.doDeleteRevision(src, getId(), getRevisionNumber()); ContentPeer.doStatusUpdate(src, getId()); } catch (DataObjectException e) { LOG.error(e.getMessage()); throw new ContentException(e); } finally { src.close(); } // Remove from cache CacheManager.getInstance().remove(this); } /** * Validates the object data before writing to the database. * * @throws ContentException if the object data wasn't valid */ protected void doValidate() throws ContentException { if (!isPersistent()) { if (getDomain().equals("")) { throw new ContentException("no domain set for content object"); } else if (getDomain() == null) { throw new ContentException("domain '" + getDomainName() + "'does not exist"); } validateSize("content name", getName(), 1, 200); if (getCategory() != SITE_CATEGORY) { validateChars("content name", getName(), NAME_CHARS); } } validateSize("content comment", getComment(), 0, 200); } /** * Inserts the object data into the database. If the restore flag * is set, no automatic changes should be made to the data before * writing to the database. * * @param src the data source to use * @param user the user performing the operation * @param restore the restore flag * * @throws ContentException if the database couldn't be accessed * properly */ protected void doInsert(DataSource src, User user, boolean restore) throws ContentException { if (!restore) { data.setString(ContentData.AUTHOR, user.getName()); data.setDate(ContentData.MODIFIED, new Date()); } try { ContentPeer.doInsert(src, data); doWriteAttributes(src, true); oldRevision = getRevisionNumber(); ContentPeer.doStatusUpdate(src, getId()); } catch (DataObjectException e) { LOG.error(e.getMessage()); throw new ContentException(e); } } /** * Updates the object data in the database. * * @param src the data source to use * @param user the user performing the operation * * @throws ContentException if the database couldn't be accessed * properly */ protected void doUpdate(DataSource src, User user) throws ContentException { data.setString(ContentData.AUTHOR, user.getName()); data.setDate(ContentData.MODIFIED, new Date()); try { if (oldRevision != getRevisionNumber()) { ContentPeer.doInsert(src, data); doWriteAttributes(src, true); if (oldRevision == 0) { ContentPeer.doDeleteRevision(src, getId(), 0); } oldRevision = getRevisionNumber(); } else { ContentPeer.doUpdate(src, data); doWriteAttributes(src, false); } ContentPeer.doStatusUpdate(src, getId()); } catch (DataObjectException e) { LOG.error(e.getMessage()); throw new ContentException(e); } } /** * Deletes the object data from the database. This method will * also delete any child content object recursively. * * @param src the data source to use * @param user the user performing the operation * * @throws ContentException if the database couldn't be accessed * properly */ protected void doDelete(DataSource src, User user) throws ContentException { Content[] children; children = InternalContent.findByParent(getContentManager(), this); try { for (int i = 0; i < children.length; i++) { children[i].delete(src, user); } ContentPeer.doDelete(src, data); } catch (DataObjectException e) { LOG.error(e.getMessage()); throw new ContentException(e); } catch (ContentSecurityException e) { LOG.warning(e.getMessage()); throw new ContentException("couldn't delete child object", e); } } /** * Reads the content attributes from the database. This method * will add all found attributes to the attributes map. * * @param src the data source to use * * @throws DataObjectException if the data source couldn't be * accessed properly */ private void doReadAttributes(DataSource src) throws DataObjectException { ArrayList list; AttributeData attr; list = AttributePeer.doSelectByRevision(src, getId(), getRevisionNumber()); for (int i = 0; i < list.size(); i++) { attr = (AttributeData) list.get(i); attributes.put(attr.getString(AttributeData.NAME), attr); } } /** * Writes the content attributes to the database. This method * will either insert of update each attribute depending on * whether it is present in the added attributes list or not. * * @param src the data source to use * @param insert the force insert flag * * @throws DataObjectException if the data source couldn't be * accessed properly */ private void doWriteAttributes(DataSource src, boolean insert) throws DataObjectException { Iterator iter = attributes.keySet().iterator(); AttributeData attr; String name; while (iter.hasNext()) { name = (String) iter.next(); attr = (AttributeData) attributes.get(name); if (attr.getInt(AttributeData.CONTENT) <= 0) { attr.setInt(AttributeData.CONTENT, getId()); } if (insert || attributesAdded.contains(name)) { AttributePeer.doInsert(src, attr); } else { AttributePeer.doUpdate(src, attr); } } if (!insert) { for (int i = 0; i < attributesRemoved.size(); i++) { attr = (AttributeData) attributesRemoved.get(i); AttributePeer.doDelete(src, attr); } } attributesAdded.clear(); attributesRemoved.clear(); } }