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