503 lines
17 KiB
Java
Executable File
503 lines
17 KiB
Java
Executable File
/*
|
|
* 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 <code>DomainCopier</code>
|
|
*/
|
|
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
|
|
* <code>source</code> and then traverses its associations and
|
|
* repeats the process.
|
|
*
|
|
* @param source the <code>DomainObject</code> 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 <code>DomainObject</code> being copied
|
|
* @param target the new copy
|
|
* @param prop the <code>Property</code> 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 <code>prop</code> on
|
|
* <code>source</code> and sets it on <code>target</code>.
|
|
*
|
|
* @param source the <code>DomainObject</code> being copied
|
|
* @param target the new <code>DomainObject</code> copy
|
|
* @param prop the <code>Property</code> of <code>source</code>
|
|
* 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
|
|
* <code>0..n</code> association. Otherwise it calls {@link
|
|
* #copy(DomainObject, DomainObject, DomainObject, Property)} for
|
|
* the value on <code>source</code> and sets the result on
|
|
* <code>target</code>.
|
|
*
|
|
* If the role belongs to a link that has already been traversed,
|
|
* this method skips it.
|
|
*
|
|
* @param source the <code>DomainObject</code> being copied
|
|
* @param target the new <code>DomainObject</code> copy
|
|
* @param prop the <code>Property</code> of <code>source</code>
|
|
* 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 <code>0..n</code>
|
|
* 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 <code>prop</code> on <code>source</code> and sets the
|
|
* result on <code>target</code>.
|
|
*
|
|
* @param source the <code>DomainObject</code> being copied
|
|
* @param target the new <code>DomainObject</code> copy
|
|
* @param prop the <code>Property</code> of <code>source</code>
|
|
* 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 <code>object</code>.
|
|
*
|
|
* This implementation returns the result of {@link
|
|
* #copy(DomainObject)} if the property is a component and simply
|
|
* returns <code>object</code> if it is not.
|
|
*
|
|
* @param source the <code>DomainObject</code> source (original)
|
|
* object to which this property belongs
|
|
* @param target the new <code>DomainObject</code> copy to which
|
|
* the return value of this method will be attached
|
|
* @param object the <code>DomainObject</code> property being
|
|
* copied
|
|
* @param prop a <code>Property</code> representing
|
|
* <code>object</code>
|
|
* @return <code>object</code> if <code>prop</code> is not a
|
|
* component or a copy of <code>object</code> 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());
|
|
}
|
|
}
|
|
|
|
}
|