libreccm-legacy/ccm-core/src/com/arsdigita/domain/DomainObjectTraversal.java

544 lines
20 KiB
Java
Executable File

/*
* Copyright (C) 2002-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.domain;
import com.arsdigita.util.Assert;
import com.arsdigita.persistence.metadata.ObjectType;
import com.arsdigita.persistence.metadata.Property;
import com.arsdigita.persistence.DataCollection;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.DataAssociation;
import com.arsdigita.persistence.DataAssociationCursor;
import com.arsdigita.persistence.DataQuery;
import com.arsdigita.persistence.DataQueryDataCollectionAdapter;
import com.arsdigita.persistence.OID;
import com.arsdigita.persistence.SessionManager;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.HashSet;
import org.apache.log4j.Logger;
/**
* <p>This class provides a general purpose framework for iterating
* over a domain object's properties, processing attributes and
* traversing associations as required.</p>
*
* <p>Subclasses should implement the startXXX and endXXX methods to
* provide whatever processing logic they require upon encountering
* attributes, roles, associations and objects.</p>
*
* <p>The {@link com.arsdigita.domain.DomainObjectTraversalAdapter}
* provides a means to control which properties are processed and,
* most importantly, which associations are traversed. When
* registering an adapter, a 'use context' is supplied allowing
* different adapters to be used according to the requirements of any
* implementing subclass. It is recommended that the use context be
* based on the fully qualified name of the class using
* DomainObjectTraversal, e.g.,
* com.arsdigita.cms.ui.DomainObjectRenderer.</p>
*
* <p>The path argument provided to the adapter and the startXXX ad
* endXXX methods indicates which associations are currently being
* traversed. The first element in the path is always '/object'. If it
* then starts to traverse the 'rootCategory' association, the path
* will become '/object/rootCategory'. For self-recursive
* associations, rather than building up a long repeating string, the
* path will be shortened by adding a '+' for each element that is
* repeated. For example, '/object/container+' indicates that the
* container association has been followed two or more times.</p>
*/
public abstract class DomainObjectTraversal {
private Set m_visited = new HashSet();
private static Map s_adapters = new HashMap();
private static final Logger s_log = Logger.getLogger(
DomainObjectTraversal.class);
public final static String LINK_NAME = "link";
/**
* Registers a traversal adapter for an object type in a given
* context.
*
* @param type the object type whose items will be traversed
* @param adapter the adapter for controlling object traversal
* @param context the context in which the adapter should be used
*/
public static void registerAdapter(final ObjectType type,
final DomainObjectTraversalAdapter adapter,
final String context) {
Assert.exists(adapter,
"The DomainObjectTraversalAdapter is null for context '"
+ context + "' and object type '" + type);
Assert.exists(type, "The ObjectType for context '" + context
+ "' and adapter '" + adapter + "' is null");
Assert.exists(context, String.class);
if (s_log.isDebugEnabled()) {
s_log.debug("Registering adapter " + adapter.getClass()
+ " for object type " + type.getQualifiedName()
+ " in context " + context);
}
s_adapters.put(new AdapterKey(type, context), adapter);
}
/**
* Unregisteres a traversal adapter for an object type in a
* given context
*
* @param type the object type whose items will be traversed
* @param context the context in which the adapter should be used
*/
public static void unregisterAdapter(final ObjectType type,
final String context) {
Assert.exists(type, ObjectType.class);
Assert.exists(context, String.class);
if (s_log.isDebugEnabled()) {
s_log.debug("Removing adapter " + " for object type " + type.
getQualifiedName() + " in context " + context);
}
s_adapters.remove(new AdapterKey(type, context));
}
/**
* Registers a traversal adapter for an object type in a given
* context.
*
* @param type the object type whose items will be traversed
* @param adapter the adapter for controlling object traversal
* @param context the context in which the adapter should be used
*/
public static void registerAdapter(final String type,
final DomainObjectTraversalAdapter adapter,
final String context) {
registerAdapter(SessionManager.getMetadataRoot().getObjectType(type),
adapter,
context);
}
/**
* Unregisteres a traversal adapter for an object type in a
* given context
*
* @param type the object type whose items will be traversed
* @param context the context in which the adapter should be used
*/
public static void unregisterAdapter(final String type,
final String context) {
unregisterAdapter(SessionManager.getMetadataRoot().getObjectType(type),
context);
}
/**
* Retrieves the traversal adapter for an object type in a given
* context.
*
* @param type the object type to lookup
* @param context the adapter context
*/
public static DomainObjectTraversalAdapter lookupAdapter(
final ObjectType type,
final String context) {
Assert.exists(type, ObjectType.class);
Assert.exists(context, String.class);
if (s_log.isDebugEnabled()) {
s_log.debug("lookupAdapter for type " + type.getQualifiedName()
+ " in context " + context);
}
return (DomainObjectTraversalAdapter) s_adapters.get(
new AdapterKey(type, context));
}
/**
* Retrieves the closest matching traversal adapter for an object type
* in a given context. The algorithm looks for an exact match, then
* considers the supertype, and the supertype's supertype. If no match
* could be found at all, returns null
*
* @param type the object type to search for
* @param context the adapter context
*/
public static DomainObjectTraversalAdapter findAdapter(ObjectType type,
final String context) {
Assert.exists(type, ObjectType.class);
Assert.exists(context, String.class);
if (s_log.isDebugEnabled()) {
s_log.debug("findAdapter for type " + type.getQualifiedName()
+ " in context " + context);
StringBuffer buf = new StringBuffer();
buf.append("Adapters contain:\n");
Iterator keys = s_adapters.keySet().iterator();
while (keys.hasNext()) {
Object key = keys.next();
buf.append(key.toString()).append(": ");
buf.append(s_adapters.get(key).toString()).append('\n');
}
s_log.debug(buf.toString());
}
DomainObjectTraversalAdapter adapter = null;
ObjectType tmpType = type;
while (adapter == null && tmpType != null) {
adapter = lookupAdapter(tmpType, context);
tmpType = tmpType.getSupertype();
}
if (adapter == null) {
s_log.warn("Could not find adapter for object type " + type.
getQualifiedName() + " in context " + context);
}
return adapter;
}
/**
* Walks over properties of a domain object, invoking
* methods to handle associations, roles and attributes.
*
* @param obj the domain object to traverse
* @param context the context for the traversal adapter
*/
public void walk(final DomainObject obj,
final String context) {
final DomainObjectTraversalAdapter adapter = findAdapter(obj.
getObjectType(),
context);
if (adapter == null) {
final String errorMsg = "No adapter for object " + obj.getOID()
+ " in context " + context;
s_log.error(errorMsg);
throw new IllegalArgumentException(errorMsg);
}
walk(obj, context, adapter);
}
protected void walk(final DomainObject obj,
final String context,
final DomainObjectTraversalAdapter adapter) {
Assert.exists(adapter, DomainObjectTraversalAdapter.class);
walk(adapter, obj, "/object", context, null);
}
// Changed from private to protected because I needed to have access to
// the class ContentBundle from package com.arsdigita.cms. The problem was
// to change RelatedLinks and therefore Link to always link to the corresponding
// ContentBundle instead of the content item. To get the corresponding
// content item during XML generation, I have to test for ContentBundle and
// negotiate the language version. This is not possible in com.arsdigita.ccm.
protected void walk(final DomainObjectTraversalAdapter adapter,
final DomainObject obj,
final String path,
final String context,
final DomainObject linkObject) {
s_log.debug(String.format("Walking with path %s and context %s...", path, context));
OID oid = obj.getOID();
OID linkOid = null;
if (linkObject != null) {
linkOid = linkObject.getOID();
}
OID[] visitedKey = {oid, linkOid};
// Prevent infinite recursion
if (m_visited.contains(visitedKey)) {
revisitObject(obj, path);
return;
} else {
m_visited.add(visitedKey);
}
// If needed, open a surrounding tag
beginObject(obj, path);
if (linkObject != null) {
beginLink(linkObject, path);
walk(adapter,
linkObject,
appendToPath(path, LINK_NAME),
context,
null);
endLink(linkObject, path);
}
ObjectType type = obj.getObjectType();
// Test all properties against the traversal xml
for (Iterator i = type.getProperties(); i.hasNext();) {
Property prop = (Property) i.next();
String propName = prop.getName();
if (!adapter.processProperty(obj,
appendToPath(path, prop.getName()),
prop,
context)) {
if (s_log.isDebugEnabled()) {
s_log.debug("Not processing " + appendToPath(path, prop.
getName()) + " in object " + oid + " and context "
+ context + " with adapter " + adapter.getClass().
getName());
}
continue;
}
Object propValue = obj.get(propName);
if (propValue == null) {
if (s_log.isDebugEnabled()) {
s_log.debug("Object " + oid.toString() + " doesn't "
+ "contain property " + propName);
}
continue;
}
if (prop.isAttribute()) {
handleAttribute(obj, path, prop);
// Property is a DataObject, so start recursion
} else if (propValue instanceof DataObject) {
if (s_log.isDebugEnabled()) {
s_log.debug(prop.getName() + " is a DataObject");
}
beginRole(obj, path, prop);
walk(adapter,
DomainObjectFactory.newInstance((DataObject) propValue),
appendToPath(path, propName),
context,
null);
endRole(obj, path, prop);
} else if (propValue instanceof DataAssociation) {
// see #25808 - this hack prevents the content field of cms_files
// (which is a blob) from being queried when all we need is a
// list of the files on an item..
if (prop.getName().equals("fileAttachments") && !Domain.
getConfig().queryBlobContentForFileAttachments()) {
// make true a config
DataQuery fileAttachmentsQuery =
SessionManager.getSession().retrieveQuery(
"com.arsdigita.cms.contentassets.fileAttachmentsQuery");
fileAttachmentsQuery.setParameter("item_id",
obj.getOID().get("id"));
DataCollection files = new DataQueryDataCollectionAdapter(
fileAttachmentsQuery, "file");
while (files.next()) {
DataObject file = files.getDataObject();
walk(adapter,
DomainObjectFactory.newInstance(file),
appendToPath(path, propName),
context,
null);
}
} else {
if (s_log.isDebugEnabled()) {
s_log.debug(prop.getName() + " is a DataAssociation");
}
beginAssociation(obj, path, prop);
DataAssociationCursor daCursor =
((DataAssociation) propValue).
getDataAssociationCursor();
while (daCursor.next()) {
s_log.debug("Processing data assoication cursor...");
DataObject link = daCursor.getLink();
DomainObject linkObj = null;
if (link != null) {
linkObj = new LinkDomainObject(link);
}
walk(adapter,
DomainObjectFactory.newInstance(daCursor.
getDataObject()),
appendToPath(path, propName),
context,
linkObj);
}
endAssociation(obj, path, prop);
}
} else {
// Unknown property value type - do nothing
}
}
// If needed, close a surrounding tag
endObject(obj, path);
}
/**
* Method called when the processing of an object
* starts
*/
protected abstract void beginObject(DomainObject obj,
String path);
/**
* Method called when the procesing of an object
* completes
*/
protected abstract void endObject(DomainObject obj,
String path);
/**
* Method called when the processing of a Link Object
* starts
*/
protected void beginLink(DomainObject obj, String path) {
s_log.debug(String.format("Starting link with path = %s", path));
}
/**
* Method called when the procesing of a Link Object
* completes
*/
protected void endLink(DomainObject obj, String path) {
s_log.debug(String.format("Finished link with path = %s", path));
}
/**
* Method called when a previously visited object
* is encountered for a second time.
*/
protected abstract void revisitObject(DomainObject obj,
String path);
/**
* Method called when an attribute is encountered
*/
protected abstract void handleAttribute(DomainObject obj,
String path,
Property property);
/**
* Method called when the processing of a role
* starts
*/
protected abstract void beginRole(DomainObject obj,
String path,
Property property);
/**
* Method called when the procesing of a role
* completes
*/
protected abstract void endRole(DomainObject obj,
String path,
Property property);
/**
* Method called when the processing of an association
* starts
*/
protected abstract void beginAssociation(DomainObject obj,
String path,
Property property);
/**
* Method called when the procesing of an association
* completes
*/
protected abstract void endAssociation(DomainObject obj,
String path,
Property property);
protected String appendToPath(String path,
String name) {
if (path.endsWith("/" + name)) {
path = path + "+";
} else if (!path.endsWith("/" + name + "+")) {
path = path + "/" + name;
}
return path;
}
protected String nameFromPath(String path) {
int index = path.lastIndexOf("/");
Assert.isTrue(index >= 0, "Path starts with /");
if (path.endsWith("+")) {
return path.substring(index + 1, path.length() - 1);
} else {
return path.substring(index + 1);
}
}
protected String parentFromPath(String path) {
int index = path.lastIndexOf("/");
Assert.isTrue(index >= 0, "Path starts with /");
if (index == 0) {
return null;
} else {
return path.substring(0, index - 1);
}
}
protected static class AdapterKey {
private final ObjectType m_type;
private final String m_context;
public AdapterKey(ObjectType type,
String context) {
Assert.exists(type, ObjectType.class);
Assert.exists(context, String.class);
m_type = type;
m_context = context;
}
public boolean equals(Object o) {
if (o instanceof AdapterKey) {
AdapterKey k = (AdapterKey) o;
return k.m_type.equals(m_type) && k.m_context.equals(m_context);
} else {
return false;
}
}
public int hashCode() {
return m_type.hashCode() + m_context.hashCode();
}
public String toString() {
return m_type.getQualifiedName() + ',' + m_context;
}
}
/**
* this is simply a subclass since DomainObject is abstract
* but we don't have any other domain object to use.
*/
private class LinkDomainObject extends DomainObject {
LinkDomainObject(DataObject object) {
super(object);
}
}
}