/* * Copyright (C) 2003-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.installer.xml; import com.arsdigita.cms.ContentItem; import com.arsdigita.cms.ContentSection; import com.arsdigita.cms.ContentType; import com.arsdigita.cms.Folder; import com.arsdigita.cms.util.GlobalizationUtil; import com.arsdigita.domain.DataObjectNotFoundException; import com.arsdigita.runtime.ConfigError; import com.arsdigita.persistence.DataAssociation; import com.arsdigita.util.Assert; import com.arsdigita.util.StringUtils; import com.arsdigita.util.UncheckedWrapperException; import java.lang.reflect.Constructor; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.Vector; import org.apache.log4j.Logger; import org.apache.oro.text.perl.Perl5Util; import org.xml.sax.Attributes; import org.xml.sax.helpers.DefaultHandler; /** * Parses and XML file definition of content items in a folder. * the XML configuration should look like this: * *
* <ccm:content-items> <!-- The Document Node --> * <ccm:folder clone="1" * depth="3" * label="testFolder" > * <!-- Folders can be nested to any level. clone, depth and label are * all required properties. Clone and depth minimum values are 1 * which result in no action. The entire nested subtree is cloned * and replicated to the specified depth --> * <ccm:content-item * clone="2" * helperClass="com.arsdigita.cms.installer.xml.GenericArticleHelper"> * <!-- ContentItems can be cloned but cannot have a depth. The * helperClass is used to create the object described here --> * <ccm:content-type * classname="com.arsdigita.cms.contenttypes.Article" * objectType="com.arsdigita.cms.contenttypes.Article"/> * <ccm:item-properties title="ArticleItem" language="en" > * <!-- Item properties are set here. A few tags may be supported * example,Body-text, but the set tags are usually used to * to call appropriate methods through the helper class on * the ContentItem. language is an optional attribute and * defaults to en. --> * <ccm:body-text> * Optional Body Text * </ccm:body-text> * <ccm:item-property method="setSpecies" * argClass="java.lang.String" * argValue="TestSpecies"/> * * <ccm:item-property method="setWingspan" * argClass="java.lang.Double" * argValue="10.0"/> * </ccm:item-properties> * * <ccm:content-item * helperClass="com.arsdigita.cms.installer.xml.ContentBundleHelper" * association-name="birdWatch" > * <!-- Nested ContentItems (nested within another content-item) * are created and associated with the encapsulating item. * These are defined as regular content-items, except for a * few notable differences: * 1) They must have an association-name tag that is the * name of the encapsulating item's association. * 2) They do not have a clone field, they are cloned as * often as the encapsulating item. * * Content-items can be nested any number of times --> * </ccm:content-item> * * </ccm:content-item> * </ccm:folder> * </ccm:content-items> ** */ public class XMLContentItemHandler extends DefaultHandler { private static final Logger s_log = Logger.getLogger(XMLContentItemHandler.class); public static final String CONTENT_ITEMS = "ccm:content-items"; public static final String CONTENT_ITEM = "ccm:content-item"; public static final String ASSOCIATED_ITEM = "ccm:associated-item"; public static final String CONTENT_TYPE = "ccm:content-type"; public static final String ITEM_PROPERTIES = "ccm:item-properties"; public static final String ITEM_PROPERTY = "ccm:item-property"; public static final String FOLDER = "ccm:folder"; public static final String BODY_TEXT = "ccm:body-text"; private xmlContentItem currItem; private Folder m_folder; private FolderTree currFolderTree; private Stack associated_items; boolean isAssociated = false; private ContentSection m_section; private String m_body = "foo!"; /** * @param section the ContentSection where the items will be * created */ public XMLContentItemHandler(ContentSection section) { super(); s_log.debug(XMLContentItemHandler.class.getName()); m_section = section; currFolderTree = new FolderTree ( null ); associated_items = new Stack(); } public void startElement( String uri, String name, String qName, Attributes atts) { if (qName.equals(CONTENT_ITEMS) ) { // Start of document do nothing } else if (qName.equals(FOLDER)) { /* * Creates a new folder and sets currFolderTree to point to it. * Adds existing tree to the FolderTree stack */ FolderHelper new_folderHelper = new FolderHelper ( m_section ); new_folderHelper.setCloneCount( Integer.parseInt(atts.getValue("clone"))); new_folderHelper.setName( validateTitle(atts.getValue("label"))); new_folderHelper.setDepth( Integer.parseInt(atts.getValue("depth"))); FolderTree newFolderTree = new FolderTree ( new_folderHelper ); newFolderTree.setParentTree ( currFolderTree ); currFolderTree.addSubTree ( newFolderTree ); currFolderTree = newFolderTree; m_folder = (Folder) currFolderTree.getFolderHelper().createContentItem ( true ); } else if ( qName.equals(CONTENT_ITEM)) { /* * Creates a new ContentItem and points to it. (does not save * it yet) If the item is nested within another item, it is * created as an association and added to the association stack. * (So that associations can be nested). Otherwise the new * item is added to the main currFolderTree */ xmlContentItem newItem = new xmlContentItem (); newItem.setHelperClass (atts.getValue("helperClass")); newItem.setCloneCount (atts.getValue("clone")); if ( m_folder != null ) { newItem.getHelperClass().setParent ( m_folder ); } if ( currItem != null ) { if ( isAssociated ) { associated_items.push ( currItem ); } isAssociated = true; newItem.setParentItem ( currItem ); final String associationName = atts.getValue ("association-name"); Assert.exists( associationName, String.class ); currItem.setAssociation ( associationName, newItem ); } else { // only add to folderTree if not an association currFolderTree.addContentItem ( newItem ); } currItem = newItem; } else if ( qName.equals(CONTENT_TYPE) ) { String objectType = atts.getValue("objectType"); Assert.exists(objectType, String.class); currItem.setContentType ( objectType ); } else if ( qName.equals(ITEM_PROPERTIES)) { final String itemName = atts.getValue("title"); Assert.exists(itemName, String.class); validateTitle(itemName); currItem.setName ( itemName ); String l_lang = atts.getValue("language"); if ( l_lang != null && !l_lang.equals("") ) { currItem.getHelperClass().setLanguage(l_lang); } currItem.create (false); } else if ( qName.equals(BODY_TEXT)) { s_log.warn("Begin Body text"); // do nothing } else if ( qName.equals(ITEM_PROPERTY)) { s_log.debug("setting property"); currItem.getHelperClass().set( atts.getValue("method"), atts.getValue("argClass"), atts.getValue("argValue") ); } else { s_log.debug("Unknown tag: " + name); } } public void characters(char[] ch, int start, int length) { m_body = new String(ch, start, length); } public void endElement( String uri, String name, String qName) { if ( qName.equals(BODY_TEXT) ) { s_log.warn("Setting body text"); currItem.getHelperClass().setBodyText(m_body); } else if ( qName.equals(ITEM_PROPERTIES)) { } else if ( qName.equals(CONTENT_TYPE)) { } else if ( qName.equals(CONTENT_ITEM) ) { /* * Saves the item. Sets the current Item to null, or * to the encapsulating item if it was an association */ currItem.save(); if ( isAssociated && ! associated_items.isEmpty() ) { currItem = (xmlContentItem) associated_items.pop(); } else if ( isAssociated && associated_items.isEmpty() ) { isAssociated = false; } if ( ! isAssociated ) { currItem = currItem.getParentItem(); } } else if (qName.equals(FOLDER)) { s_log.debug ( "Reached folder end item with folderHelper: " + currFolderTree ); currFolderTree.getFolderHelper().save(); currFolderTree = currFolderTree.getParentTree(); if ( ! currFolderTree.isRoot() ) { m_folder = (Folder) currFolderTree.getFolderHelper().create ( true ); } else { //set this to be the root folder, null acts as default right now m_folder = null; } s_log.debug ( "Done Folder endElement"); } else if (qName.equals(CONTENT_ITEMS)) { /* * Reached the end of the xml document. Traverse the FolderTree and * expand out the items and folders that need to be cloned / * replicated to a certain depth */ expandFolderTree ( currFolderTree, m_section.getRootFolder() ); } else { s_log.debug("Unknown tag: " + name); } } /** * Instantiates the helper class of type
classname
* with a single argument constructor, (section)
*/
private ContentItemHelper getHelperClass(String classname) {
if (!StringUtils.emptyString(classname)) {
try {
s_log.warn("Trying to create " + classname);
Class classDef = Class.forName(classname);
Class[] args = {ContentSection.class};
Object[] argv = { m_section };
Constructor constructor =
classDef.getConstructor(args);
s_log.warn("Got constructor " + constructor.getName());
return (ContentItemHelper)constructor.newInstance(argv);
} catch (java.lang.reflect.InvocationTargetException e) {
throw new UncheckedWrapperException(e);
} catch (InstantiationException e) {
throw new UncheckedWrapperException(e);
} catch (IllegalAccessException e) {
throw new UncheckedWrapperException(e);
} catch (ClassNotFoundException e) {
throw new UncheckedWrapperException(e);
} catch (Exception e ) {
throw new UncheckedWrapperException(e);
}
} else {
s_log.warn("Using default ContentItemHelper");
return new ContentPageHelper(m_section);
}
}
public ContentType getContentType(String typeName)
throws UncheckedWrapperException {
ContentType type = null;
s_log.debug("TypeName " + typeName);
// Look for the contenttype
try {
type = ContentType.findByAssociatedObjectType(typeName);
} catch (DataObjectNotFoundException ex) {
throw new UncheckedWrapperException(
(String) GlobalizationUtil.globalize(
"cms.installer.cannot_find_content_type")
.localize() + typeName, ex);
}
// make sure that the type is added to the section
m_section.addContentType(type);
m_section.save();
s_log.debug("Content type is: " + type.getClassName());
return type;
}
// Utilities
private String validateTitle(String name) {
Perl5Util util = new Perl5Util();
String pattern = "/[^A-Za-z_0-9\\-]+ /";
if (util.match(pattern, name)) {
throw new ConfigError(
"The \"" + name +
"\" name parameter must contain only alpha-numeric " +
"characters, underscores, and/or hyphens.");
}
return name;
}
/**
* This convenience method calls {@link #cloneFolderTree} with the parameters
* required to expand the FolderTree toClone with the parent
* folder toAttachTo.
*
* @see #cloneFolderTree
*/
protected void expandFolderTree ( FolderTree toClone, Folder toAttachTo ) {
cloneFolderTree ( toClone, toAttachTo, toAttachTo, 1, true, true, true, 1 );
}
/**
* This recursive method traverses the FolderTree toClone
* from top to bottom, expanding out nodes to clone, replicating nodes
* to their specified depth, and then expanding out these new trees as
* well. This is a fairly complicated method for a number of reasons, you
* should never need to call it directly, use {@link #expandFolderTree}
*
* @param toClone The FolderTree to clone and expand
* @param toAttachTo The Folder to attach the result of the expansion to
* @param parent The parent of the node to attach to
* @param cloneTime The clone number for this particular folder
* @param firstTime If this is the first time the method is being called
* . ie. not a recursive call. If so it will not attempt
* to clone toClone's root folder.
* @param mainline If the parent node is no the mainlin, ie. the orignal
* folder tree and not along one of the branches created
* via cloning or depth expansion. If it is along the
* mainline, it will not attempt to replicate it's
* subfolders.
* @param clone Whether this Folder should attempt to clone itself. False
* if it is iteslf a clone and thus should not attempt to
* clone itself
* @param depthTime The depth level that this folder is at. If it is at
* it's maximum depth, don't replicate itself as a child
* of itself any further.
*/
protected void cloneFolderTree ( FolderTree toClone,
Folder toAttachTo,
Folder parent,
int cloneTime,
boolean firstTime,
boolean mainline,
boolean clone,
int depthTime ) {
/*
* Clone self as many times as necessary. Folder's created as clones of
* this should not clone themselves.
*/
if ( clone && ! firstTime ) {
int selfNumClone = toClone.getFolderHelper().getCloneCount();
while ( cloneTime < selfNumClone ) {
Folder clonedFolder = (Folder) toClone.getFolderHelper().cloneItem (
cloneTime, parent );
copyFolderItems ( toClone, clonedFolder );
cloneTime++;
cloneFolderTree ( toClone, clonedFolder, parent, cloneTime,
false, false, false, depthTime );
}
}
/*
* Expand yourself to the specified depth. If a depth is specified, this
* folder should replicate and expand itself as a child of itself. The
* children should remember what depth they are at. The entire subTree
* needs to be expanded
*/
if ( ! firstTime && ( depthTime < toClone.getFolderHelper().getDepth() ) ) {
depthTime++;
cloneFolderTree ( toClone, replicateFolder ( toClone, toAttachTo ),
toAttachTo, 1, false, false, true, depthTime );
}
/*
* Expand out the subfolders along all the lines, main and cloned. If on
* the mainline, grab the node to attachTo from the cloneTree itself,
* otherwise replicate the folder to attach to.
*/
List subFolders = toClone.getSubTrees();
for ( int i=0; i < subFolders.size(); i++ ) {
Folder newToAttachTo;
if ( mainline ) {
newToAttachTo = ((Folder)((FolderTree)subFolders.get(i))
.getFolderHelper().getContentItem());
} else {
newToAttachTo = replicateFolder ( (FolderTree)subFolders.get(i), toAttachTo );
}
cloneFolderTree ( (FolderTree) subFolders.get(i), newToAttachTo,
toAttachTo, 1, false, mainline, true, 1 );
}
/*
* Clone all the children for this Folder.
*/
List childItems = toClone.getContentItems();
for ( int i=0; i < childItems.size(); i++ ) {
autoCloneChild ( (xmlContentItem) childItems.get(i), toAttachTo );
}
}
/**
* Clones The child the number of times it needs to be cloned and
* attaches all the children to parent. Cloning an
* xmlContentItem automatically clones all it's associations as well
*
* @param child the xmlContentItem to clone
* @param parent the folder to attach all the new children to
*/
private void autoCloneChild ( xmlContentItem child, Folder parent ) {
final int numClone = child.getHelperClass().getCloneCount();
for ( int i=1; i and replicates
* all content Items (only items, not nested folders etc) of
* toCopy to attach to it.
*
* @param toCopy the FolderTree whose items we wish to copy to the
* new Folder returned.
* @param parent the parent of the new folder returned
* @return A child folder of parent that has replicated copies of
* all of toCopy's children
*/
private Folder replicateFolder ( FolderTree toCopy, Folder parent ) {
Folder newFolder = (Folder) toCopy.getFolderHelper().cloneItem (
toCopy.getFolderHelper().getName(), parent , true );
return copyFolderItems ( toCopy, newFolder );
}
/**
* Same as {@link #replicateFolder(FolderTree, Folder)} except that
* it does not create a new folder, but rather attaches the child
* copies to copyTo
*/
private Folder copyFolderItems ( FolderTree toCopy, Folder copyTo ) {
List childContent = toCopy.getContentItems();
for ( int i=0; i < childContent.size() ; i++ ) {
xmlContentItem child = (xmlContentItem) childContent.get(i);
final String name = child.getHelperClass().getName();
child.replicate(name,
copyTo,
true );
}
return copyTo;
}
private class FolderHelper extends ContentItemHelper {
int m_treeDepth;
public FolderHelper(ContentSection section) {
super(section);
m_treeDepth = 0;
setContentType(Folder.BASE_DATA_OBJECT_TYPE);
}
public void setDepth(int depth) {
m_treeDepth = depth;
s_log.debug("Depth is now " + m_treeDepth);
}
public int getDepth() {
return m_treeDepth;
}
public ContentItem createContentItem ( boolean save ) {
s_log.warn("creating folder");
Folder folder = (Folder)super.createContentItem( false );
folder.setLabel(folder.getName());
setContentItem(folder);
if ( save ) {
save ();
}
return folder;
}
public ContentItem cloneItem ( String name, Folder parent, boolean save ) {
Folder folder= (Folder)super.cloneItem(name, parent, save);
folder.setLabel(folder.getName());
folder.save();
return folder;
}
}
/**
* This helper class acts as a Folder node that can be used to build
* a tree of folders. It maintains a copy of the FolderHelper that
* created this particular Folder, it's parent. As well as a List of
* FolderTrees that represent it's subfolders, and a List of
* xmlContentItems that represent all it's children
*/
private class FolderTree {
private List m_subFolders;
private List m_contentItems;
private FolderTree m_parentTree;
private FolderHelper m_helper;
public FolderTree ( FolderHelper helper ) {
m_subFolders = new Vector();
m_contentItems = new Vector();
m_helper = helper;
}
public FolderHelper getFolderHelper () {
return m_helper;
}
public void setParentTree ( FolderTree parentTree ) {
m_parentTree = parentTree;
if ( parentTree != null && parentTree.getFolderHelper() != null ) {
m_helper.setParent ( (Folder)parentTree.getFolderHelper().create(false) );
}
}
public FolderTree getParentTree () {
return m_parentTree;
}
public void addSubTree ( FolderTree subTree ) {
m_subFolders.add ( subTree );
}
public List getSubTrees () {
return m_subFolders;
}
public void addContentItem ( xmlContentItem item ) {
m_contentItems.add ( item );
}
public List getContentItems () {
return m_contentItems ;
}
public boolean isRoot () {
if ( m_parentTree == null ) {
return true;
}
return false;
}
}
/**
* This helper class is used to maintain information about each
* item As well as all it's associations. It stores the helper class
* that created the item, it's parent and lists of all associated
* xmlContentItems. Cloning this item will automatically created
* and assign clones of all it's associations as well.
*/
private class xmlContentItem {
/** The helper class that created this item */
private ContentItemHelper xml_page_helper;
/** The List of associations. Maps of association names to
* xmlContentItems */
private Vector xml_assocs;
/** The parent Item */
private xmlContentItem xml_parent_item;
public xmlContentItem () {
xml_assocs = new Vector();
}
/**
* Instantiates and sets the specified helper class
*/
public void setHelperClass ( String l_helperClass ) {
xml_page_helper = XMLContentItemHandler.this.getHelperClass (
l_helperClass );
}
public ContentItemHelper getHelperClass () {
return xml_page_helper;
}
public void setParentItem ( xmlContentItem l_parent ) {
xml_parent_item = l_parent;
}
public xmlContentItem getParentItem () {
return xml_parent_item;
}
public void setCloneCount ( String l_count ) {
xml_page_helper.setCloneCount( Integer.parseInt( l_count ) );
}
public void setContentType ( String l_objectType ) {
getHelperClass().setContentType(
(getContentType(l_objectType)).getClassName());
}
public void setName ( String l_name ) {
s_log.debug ( "Setting Name to:" + l_name );
getHelperClass().setName( l_name );
}
public void setAssociation ( String assocName, xmlContentItem assoc ) {
Map newAssoc = new Hashtable();
newAssoc.put ( assocName, assoc );
xml_assocs.add ( newAssoc );
}
public List getAssociations () {
return xml_assocs;
}
public void create ( boolean save ) {
getHelperClass().create(save);
}
/**
* Save this item. This will save all associated Items and set
* the appropriate associations as well.
*/
public void save () {
getHelperClass().save();
s_log.debug("About to save all the associations");
ContentItem theItem = getHelperClass().getContentItem();
for ( Iterator i = xml_assocs.iterator() ; i.hasNext() ; ) {
Map m_assoc = (Map) i.next();
for ( Iterator j = m_assoc.keySet().iterator() ; j.hasNext() ; ) {
String key = (String) j.next();
xmlContentItem value = (xmlContentItem) m_assoc.get(key);
DataAssociation da = (DataAssociation) theItem.get ( key );
value.getHelperClass().getContentItem().addToAssociation ( da );
s_log.debug("Just saved a data association" );
}
}
theItem.save();
}
/**
* Clones and returns the clone for this contentItem. All associated
* items are cloned and associated to the new clone.
*
* @param cloneNumber This integer is used to create the name for the
* clone returned.
* @param parent The parent of the new cloned item
* @param save If true, the new item will be saved before being returned
*/
public ContentItem clone ( int cloneNumber, Folder parent, boolean save ) {
return cloneItem ( "", cloneNumber, parent, save, false );
}
/**
* Clones and returns the clone for this contentItem. All associated
* items are cloned and associated to the new clone.
*
* @param name This will be the name of the clone returned
* @param parent The parent of the new cloned item
* @param save If true, the new item will be saved before being returned
*/
public ContentItem replicate ( String name, Folder parent, boolean save ) {
return cloneItem ( name, 0, parent, save, true );
}
/**
* FIXME: Need to refactor code here
* Clones the item as well as it's associations and associates them
* appropriately.
*
* @param name The name of the new clone, only used if
* replicate is true.
* @param cloneNumber if replicate is false, this will be
* used to form the new name for the clone.
* {@link #clone(int,Folder,boolean)}
* @param parent The parent of the new clone
* @param save If true, the new item will be saved before it is returned
* @param replicate will decide if the name of the clone should be based
* on name or cloneNumber
*/
private ContentItem cloneItem ( String name,
int cloneNumber,
Folder parent,
boolean save,
boolean replicate
)
{
// clone and set associations here as well
if ( replicate ) {
s_log.debug ( "About to replicate: "
+ name + " with parent: " + parent.getLabel() );
} else {
name = getHelperClass().cloneName ( cloneNumber );
s_log.debug ( "About to clone: "
+ name + " with parent: " + parent.getLabel() );
}
ContentItem clone = getHelperClass().cloneItem ( name, parent, save );
//clone each association and associate it
for ( int i=0; i < xml_assocs.size() ; i++ ) {
Map m_assoc = (Map) xml_assocs.get ( i );
for ( Iterator j = m_assoc.keySet().iterator() ; j.hasNext() ; ) {
String key = (String) j.next();
xmlContentItem value = (xmlContentItem) m_assoc.get(key);
String cloneAssocName;
if ( replicate ) {
cloneAssocName = value.getHelperClass().getName();
} else {
cloneAssocName = value.getHelperClass().cloneName(cloneNumber);
}
ContentItem cloneValue = value.getHelperClass().cloneItem (
cloneAssocName, parent, true );
s_log.debug ( "Value of item: "
+ getHelperClass().getContentItem() );
s_log.debug ( "Value of cloned item: " + clone );
s_log.debug ( "Value of association: "
+ value.getHelperClass().getContentItem() );
s_log.debug ( "Value of cloned association : " + cloneValue );
DataAssociation da = (DataAssociation) clone.get ( key );
da.clear();
cloneValue.addToAssociation ( da );
s_log.debug ("Just saved a data association" );
}
}
clone.save();
return clone;
}
}
}