/* * 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; import com.arsdigita.domain.DomainObject; import com.arsdigita.domain.DomainObjectFactory; import com.arsdigita.domain.DomainService; import com.arsdigita.kernel.ACSObject; import com.arsdigita.persistence.DataAssociation; import com.arsdigita.persistence.DataAssociationCursor; import com.arsdigita.persistence.DataObject; import com.arsdigita.persistence.OID; import com.arsdigita.persistence.metadata.ObjectType; import com.arsdigita.persistence.metadata.Property; import com.arsdigita.util.Assert; import com.arsdigita.util.Tracer; import com.arsdigita.util.Classes; import org.apache.log4j.Logger; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; /** * Recursively copies a domain object. * * @author Justin Ross <jross@redhat.com> * @version $Id: DomainCopier.java 2218 2011-06-22 23:55:36Z pboy $ */ class DomainCopier extends DomainService { private static final Logger s_log = Logger.getLogger(DomainCopier.class); // A map of OID => DomainObject private final HashMap m_copied; protected final TraversedSet m_traversed; protected final Tracer m_trace; /** * Constructs a new DomainCopier */ public DomainCopier() { m_copied = new HashMap(); m_traversed = new TraversedSet(); m_trace = new Tracer(DomainCopier.class); } /** * Kicks off the copying process. Creates a copy by value of * source and then traverses its associations and * repeats the process. * * @param source the DomainObject from which to copy */ public DomainObject copy(final DomainObject source) { m_trace.enter("copy", source); final OID sourceOID = source.getOID(); final DomainObject already = (DomainObject) m_copied.get(sourceOID); if (already == null) { if (s_log.isInfoEnabled()) { s_log.info("Copying " + source); } final Class clacc = source.getClass(); if (s_log.isDebugEnabled()) { s_log.debug("Using class " + clacc.getName()); } DomainObject target = null; if (source instanceof ACSObject) { final String type = ((ACSObject) source).getSpecificObjectType(); target = (DomainObject) Classes.newInstance( clacc, new Class[]{String.class}, new Object[]{type}); } else { Assert.fail("Cannot copy " + source + "; it is not an " + "ACSObject"); // XXX Contrary to this assertion, it is reasonable to // copy domain objects, and this code should support // it. } Assert.exists(target, DomainObject.class); if (s_log.isDebugEnabled()) { s_log.debug("Created empty copy shell " + target); } // Prevent infinite recursion m_copied.put(sourceOID, target); copyData(source, target); m_trace.exit("copy", target); return target; } else { if (s_log.isDebugEnabled()) { s_log.debug("There is already a copy; returning it"); } m_trace.exit("copy", already); return already; } } protected void copyData(final DomainObject source, final DomainObject target) { final ObjectType type = source.getObjectType(); if (s_log.isDebugEnabled()) { s_log.debug("Using object type " + type.getName()); } // XXX This is what I would like to do: // //final Iterator iter = type.getProperties(); // //while (iter.hasNext()) { // copyProperty(source, target, (Property) iter.next()); //} // But the beforeSave-on-flush behavior makes it so I have // to do this instead: Iterator iter = type.getProperties(); final ArrayList attributes = new ArrayList(); final ArrayList roles = new ArrayList(); final ArrayList collections = new ArrayList(); while (iter.hasNext()) { final Property prop = (Property) iter.next(); if (prop.isAttribute()) { attributes.add(prop); } else if (prop.isCollection()) { collections.add(prop); } else { roles.add(prop); } } iter = attributes.iterator(); while (iter.hasNext()) { copyProperty(source, target, (Property) iter.next()); } iter = roles.iterator(); while (iter.hasNext()) { copyProperty(source, target, (Property) iter.next()); } iter = collections.iterator(); while (iter.hasNext()) { copyProperty(source, target, (Property) iter.next()); } } /** * Copies properties. This method is called from {@link * #copy(DomainObject)} for each property of the object being * copied. * * This implementation calls {@link #copyAttribute(DomainObject, * DomainObject, Property)} if the property is a scalar attribute * and {@link #copyRole(DomainObject, DomainObject, Property)} if * the property belongs to an association. * * If the property is a key property, this method skips it. * * @param source the DomainObject being copied * @param target the new copy * @param prop the Property currently under * consideration */ protected void copyProperty(final DomainObject source, final DomainObject target, final Property prop) { m_trace.enter("copyProperty", source, target, prop); if (s_log.isDebugEnabled()) { s_log.debug("Considering property " + prop + " for copying"); } if (prop.isKeyProperty()) { s_log.debug("The property is one of the key properties; " + "skipping it"); } else { s_log.debug("Copying is enabled; proceeding"); if (prop.isAttribute()) { copyAttribute(source, target, prop); } else { copyRole(source, target, prop); } } m_trace.exit("copyProperty"); } /** * Copies properties that are scalar attributes, not associations. * * This implementation gets the value of prop on * source and sets it on target. * * @param source the DomainObject being copied * @param target the new DomainObject copy * @param prop the Property of source * being copied */ protected void copyAttribute(final DomainObject source, final DomainObject target, final Property prop) { m_trace.enter("copyAttribute", source, target, prop); if (s_log.isDebugEnabled()) { s_log.debug("Copying attribute " + prop + " by value"); } final String name = prop.getName(); set(target, name, get(source, name)); m_trace.exit("copyAttribute"); } /** * Copies properties that belong to associations. This method is * called from {@link #copyProperty(DomainObject, DomainObject, * Property}. * * This implementation calls {@link #copyCollection(DomainObject, * DomainObject, Property)} if the property belongs to a * 0..n association. Otherwise it calls {@link * #copy(DomainObject, DomainObject, DomainObject, Property)} for * the value on source and sets the result on * target. * * If the role belongs to a link that has already been traversed, * this method skips it. * * @param source the DomainObject being copied * @param target the new DomainObject copy * @param prop the Property of source * being copied */ protected void copyRole(final DomainObject source, final DomainObject target, final Property prop) { m_trace.enter("copyRole", source, target, prop); final String name = prop.getName(); if (m_traversed.contains(source, prop)) { s_log.debug("The role belongs to a link that has " + "already been traversed; skipping it"); } else { // This marks the forward link traversed. Further down, // in this method and in copyCollection, we mark the // reverse link traversed as well. m_traversed.add(source, prop); if (prop.isCollection()) { s_log.debug("The property is a 0..n association"); copyCollection(source, target, prop); } else if (prop.isRequired()) { s_log.debug("The property is a 1..1 association"); final DataObject data = (DataObject) get(source, name); Assert.exists(data, DataObject.class); final DomainObject domain = domain(data); checkXmlCache(domain); m_traversed.add(domain, prop.getAssociatedProperty()); set(target, name, copy(source, target, domain, prop)); } else if (prop.isNullable()) { s_log.debug("The property is a 0..1 association"); final DataObject data = (DataObject) get(source, name); if (data == null) { set(target, name, null); } else { final DomainObject domain = domain(data); checkXmlCache(domain); m_traversed.add(domain, prop.getAssociatedProperty()); set(target, name, copy(source, target, domain(data), prop)); } } else { Assert.fail("Unknown property type"); } } m_trace.exit("copyRole"); } /** * Copies properties that belong to 0..n * associations. This method is called from {@link * #copyRole(DomainObject, DomainObject, Property)}. * * This implementation calls {@link #copy(DomainObject, * DomainObject, DomainObject, Property)} for each value fetched * from prop on source and sets the * result on target. * * @param source the DomainObject being copied * @param target the new DomainObject copy * @param prop the Property of source * being copied */ protected void copyCollection(final DomainObject source, final DomainObject target, final Property prop) { m_trace.enter("copyCollection", source, target, prop); if (s_log.isDebugEnabled()) { s_log.debug("Copying collection " + prop); } final String name = prop.getName(); final DataAssociation sass = (DataAssociation) get(source, name); final DataAssociationCursor scursor = sass.cursor(); final Property reverse = prop.getAssociatedProperty(); while (scursor.next()) { final DomainObject selem = domain(scursor.getDataObject()); m_traversed.add(selem, reverse); final DomainObject telem = copy(source, target, selem, prop); checkXmlCache(telem); DataObject tgtLink = null; // removing this assert since copy will return null in the // case of deferred association creation in VersionCopier //Assert.exists(telem, DomainObject.class); if (telem != null) { tgtLink = add(target, name, telem); } if (tgtLink != null) { // Copy link attributes as well copyData(new WrapperDomainObject(scursor.getLink()), new WrapperDomainObject(tgtLink)); } } m_trace.exit("copyCollection"); } /** * Creates a copy, by reference or by value, of the property * represented in object. * * This implementation returns the result of {@link * #copy(DomainObject)} if the property is a component and simply * returns object if it is not. * * @param source the DomainObject source (original) * object to which this property belongs * @param target the new DomainObject copy to which * the return value of this method will be attached * @param object the DomainObject property being * copied * @param prop a Property representing * object * @return object if prop is not a * component or a copy of object it is a component */ protected DomainObject copy(final DomainObject source, final DomainObject target, final DomainObject object, final Property prop) { m_trace.enter("copy", object, prop); if (prop.isComponent()) { if (s_log.isDebugEnabled()) { s_log.debug("The property is a component; " + "copying by value"); } final DomainObject copy = copy(object); m_trace.exit("copy", copy); return copy; } else { s_log.debug("The property is not a component; " + "copying by reference"); m_trace.exit("copy", object); return object; } } /** * Fetch the copy of the object with the given OID. Return null if * the copy does not exist, or if the copy is not a DomainObject. * * @param oid the OID to look up * @return a copy of the object with the given OID, or null on * failure * @see ItemCopier#getCopy(OID) */ public DomainObject getCopy(final OID oid) { return (DomainObject) m_copied.get(oid); } // Utility methods and classes protected DomainObject domain(final DataObject data) { Assert.exists(data, DataObject.class); final DomainObject domain = DomainObjectFactory.newInstance(data); Assert.exists(domain, DomainObject.class); return domain; } protected static class TraversedSet extends HashSet { void add(final DomainObject object, final Property prop) { Assert.exists(object, DomainObject.class); if (prop != null) { add(object.getOID() + "." + prop.getName()); } } boolean contains(final DomainObject object, final Property prop) { Assert.exists(object, DomainObject.class); Assert.exists(prop, Property.class); return contains(object.getOID() + "." + prop.getName()); } } protected final class WrapperDomainObject extends DomainObject { public WrapperDomainObject(final DataObject dobj) { super(dobj); } public WrapperDomainObject(final OID oid) { super(oid); } } /** * Helper method for invalidating the cached XML of associated objects when an item is (re-)published. * * @param dobj */ private void checkXmlCache(final DomainObject dobj) { if ((dobj instanceof ContentItem) && CMSConfig.getInstanceOf().getEnableXmlCache()) { final ContentItem item = (ContentItem) dobj; XMLDeliveryCache.getInstance().removeFromCache(item.getOID()); } } }