/*
$Header: /cvsroot/xorm/xorm/src/org/xorm/RelationshipProxy.java,v 1.33 2004/02/05 23:04:55 seifertd Exp $
This file is part of XORM.
XORM is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
XORM 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with XORM; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.xorm;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Logger;
import javax.jdo.JDOFatalUserException;
import org.xorm.datastore.Column;
import org.xorm.datastore.DataFetchGroup;
import org.xorm.datastore.Row;
import org.xorm.datastore.Table;
import org.xorm.query.BoundExpression;
import org.xorm.query.AndCondition;
import org.xorm.query.Condition;
import org.xorm.query.Operator;
import org.xorm.query.QueryImpl;
import org.xorm.query.Selector;
import org.xorm.query.SimpleCondition;
/**
* Represents a one-to-many or many-to-many relationship. An instance
* of RelationshipProxy is constructed in
* InterfaceInvocationHandler.invokeCollectionGet().
*/
public class RelationshipProxy extends CollectionProxy {
protected static Logger logger = Logger.getLogger("org.xorm.RelationshipProxy");
// The field on the owner object that references this collection
protected boolean txnManaged;
protected RelationshipMapping mapping;
protected InterfaceInvocationHandler owner;
// Additional fields used for many-to-many tracking
protected Collection deletedRows;
protected Collection newRows;
// Arguments that need to be translated into parameters in a filtered
// relationship query
protected Object[] args = null;
/** Returns the underlying relationship mapping this represents. */
public RelationshipMapping getRelationshipMapping() { return mapping; }
/** Returns the owner of this directional relationship. */
public InterfaceInvocationHandler getOwner() { return owner; }
/** Creates a new proxy backed by the data in the rows collection. */
public RelationshipProxy(InterfaceManager mgr,
RelationshipMapping mapping,
InterfaceInvocationHandler owner,
ClassMapping classMapping,
Object[] args) {
super(mgr, classMapping);
this.mapping = mapping;
this.owner = owner;
this.txnManaged = owner.isTransactional();
if (mapping.isMToN()) {
this.newRows = new ArrayList();
this.deletedRows = new ArrayList();
}
this.args = args;
this.selector = getSelector();
}
/** Overrides CollectionProxy implementation. */
public Class getElementType() {
return mapping.getSource().getElementClass();
}
protected Selector getSelector() {
Object ownerPKey = owner.getObjectId();
if (ownerPKey instanceof TransientKey) {
return null;
}
Column columnToUse = null;
if (mapping.getSource().getColumn() != null) {
columnToUse = mapping.getSource().getColumn();
}
else if (mapping.getFilter() != null) {
// In this case, source wasn't specified, but target
// is always specified...and in this case should be
// the primary key column of the owner class (see
// ModelMapping.parseRelationship).
columnToUse = mapping.getTarget().getColumn();
}
else {
// How did we get here? An exception should have been
// thrown in ModelMapping.parseRelationship
throw new JDOFatalUserException(I18N.msg("E_collection_no_source"));
}
Selector selector = new Selector
(columnToUse.getTable(),
new SimpleCondition(columnToUse, Operator.EQUAL, ownerPKey));
if (mapping.getFilter() != null) {
logger.fine("Filter: " + mapping.getFilter());
logger.fine("Imports: " + mapping.getImports());
logger.fine("Parameters: " + mapping.getParameters());
logger.fine("Variables: " + mapping.getVariables());
if (mapping.getParameters() != null &&
!mapping.getParameters().equals("") &&
(args == null || args.length == 0)) {
throw new NullPointerException(I18N.msg("E_missing_arguments"));
}
//logger.fine("Class Mapping Table: " + classMapping.getTable().getName());
Class filterTargetClass = classMapping.getMappedClass();
//logger.fine("Filter Target Class: " + filterTargetClass.getName());
QueryImpl query = new QueryImpl(mgr);
query.setClass(filterTargetClass);
query.setFilter(mapping.getFilter());
// We support the implicit "owner" parameter in filters.
// The owner refers to the object instance off which the
// collection field getter is being called. We need to add
// it to the parameters that we set on the query.
String parameters = mapping.getParameters();
ClassMapping ownerClassMapping = owner.getClassMapping();
Class ownerClass = ownerClassMapping.getMappedClass();
String ownerClassName = ownerClass.getName();
//ownerClassName = ownerClassName.substring(ownerClassName.lastIndexOf('.') + 1);
//logger.fine("OwnerClassName: " + ownerClassName);
if (mapping.getImports() != null) {
query.declareImports(mapping.getImports());
}
if (parameters == null || parameters.equals("")) {
// No parameters, just this owner param
parameters = ownerClassName + " owner";
}
else {
// Append this owner param to the existing parameters
parameters += ", " + ownerClassName + " owner";
}
logger.fine("Modified Parameters: " + parameters);
query.declareParameters(parameters);
if (mapping.getVariables() != null) {
query.declareVariables(mapping.getVariables());
}
if (mapping.getOrdering() != null) {
query.setOrdering(mapping.getOrdering());
}
query.compile();
//logger.fine("Filter Query: " + query);
BoundExpression bound = query.getBoundExpression();
int paramCount = 0;
if (args != null) {
while (paramCount < args.length) {
bound.bindParameter(paramCount, args[paramCount]);
++paramCount;
}
}
// Always bind the implicit "owner" parameter
bound.bindParameter(paramCount++, owner.getProxy());
//logger.fine(paramCount + " parameter(s) bound");
/*
logger.info("Main Selector Table Before: " +
selector.getTable().getName());
logger.info("Main Selector Before: " + selector);
*/
Selector filterSelector = bound.getSelector();
/*
logger.info("Filter Selector Table: " +
filterSelector.getTable().getName());
logger.info("Filter Selector: " + filterSelector);
*/
if (mapping.getSource().getColumn() != null) {
//logger.fine("Joining filter selector with running selector");
/*
Condition join = new SimpleCondition(mapping.getTarget().getColumn(), Operator.EQUAL, filterSelector);
selector.setCondition(new AndCondition(join, selector.getCondition()));
*/
selector.merge(filterSelector, Operator.ANDC);
}
else {
// This is a free-floating collection getter query.
// There is no link table, there is no source.
// Just use the filter as the selector.
//logger.fine("Replacing selector with filter selector");
selector = filterSelector;
}
/*
logger.info("Main Selector Table After: " +
selector.getTable().getName());
logger.info("Main Selector After: " + selector);
*/
}
// This is a hack until there is real OrderedSet/List support.
// key="order-by" can be specified in JDO metadata extensions
String orderBy = mapping.getOrderBy();
if (orderBy != null) {
int spacePos = orderBy.indexOf(' ');
String colName;
int orderingType = Selector.Ordering.ASCENDING;
if (spacePos != -1) {
colName = orderBy.substring(0, spacePos);
if ((spacePos + 1) < orderBy.length()) {
String type = orderBy.substring(spacePos+1);
orderingType = "ASC".equalsIgnoreCase(type) ? Selector.Ordering.ASCENDING : Selector.Ordering.DESCENDING;
}
}
else {
colName = orderBy;
}
Selector.Ordering[] ordering = new Selector.Ordering[1];
ordering[0] = new Selector.Ordering(selector.getTable().getColumnByName(colName), orderingType);
selector.setOrdering(ordering);
}
DataFetchGroup dfg = null;
FetchGroupManager fgm = mgr.getInterfaceManagerFactory()
.getFetchGroupManager();
if (mapping.isMToN()) {
// For many-to-many mappings, select all from the join table,
// and then the default fetch group of the target table
dfg = new DataFetchGroup(selector.getTable().getColumns());
dfg.addSubgroup(mapping.getTarget().getColumn(),
fgm.getDataFetchGroup(classMapping));
} else {
dfg = fgm.getDataFetchGroup(classMapping);
}
selector.require(dfg);
return selector;
}
/**
* Ensures that the proxy is registered in a transaction if necessary.
*/
protected void forceTransaction() {
if (owner.isPersistent()) {
TransactionImpl txn = (TransactionImpl) owner.getInterfaceManager().currentTransaction();
if (txn.isActive()) {
txn.attachRelationship(this);
txnManaged = true;
} else {
throw new JDOFatalUserException(I18N.msg("E_modify_collection"));
}
}
}
/** Adds the given object (proxy object) to the collection. */
public boolean add(Object o) {
// Sanity checking
if (o == null) throw new NullPointerException();
if (!getElementType().isInstance(o)) {
throw new IllegalArgumentException(I18N.msg("E_element_type", getElementType().getName()));
}
if (!txnManaged) {
forceTransaction();
}
InterfaceInvocationHandler handler = InterfaceInvocationHandler.getHandler(o);
Row row = new Row(mapping.getSource().getColumn().getTable());
row.setValue(mapping.getSource().getColumn(), owner.getObjectId());
row.setValue(mapping.getTarget().getColumn(), handler.getObjectId());
getRows().add(row);
if (!owner.isDirty()) {
owner.makeDirty();
}
rowToProxy.put(row, o);
if (mapping.isMToN()) {
markAsNew(row);
}
// Transient objects that are added to a persistent relationship
// become persistent.
if (owner.isPersistent() && !handler.isPersistent()) {
handler.makePersistent(mgr);
}
return true;
}
/** Removes the given object (proxy object) from the collection. */
public boolean remove(Object o) {
if (!txnManaged) {
forceTransaction();
}
InterfaceInvocationHandler handler = InterfaceInvocationHandler.getHandler(o);
Object targetKey = handler.getObjectId();
// Find the row that matches the targetKey
Iterator i = getRows().iterator();
while (i.hasNext()) {
Row row = (Row) i.next();
if (targetKey.equals(row.getValue(mapping.getTarget().getColumn()))) {
if (mapping.isMToN()) {
markAsDeleted(row);
}
i.remove();
rowToProxy.remove(row);
if (!owner.isDirty()) {
owner.makeDirty();
}
return true;
}
}
return false; // not found
}
/**
* Removes all rows. This is slightly more straightforward than
* iterating through and removing each element (but that works too).
*/
public void clear() {
if (!txnManaged) {
forceTransaction();
}
Iterator i = getRows().iterator();
if (i.hasNext()) {
if (!owner.isDirty()) {
owner.makeDirty();
}
while (i.hasNext()) {
Row row = (Row) i.next();
if (mapping.isMToN()) {
markAsDeleted(row);
}
i.remove();
}
}
rowToProxy.clear();
}
/**
* Removes any transactional context associated with this proxy,
* clearing the list of new and deleted rows.
*/
void exitTransaction(boolean commit) {
txnManaged = false;
if (mapping.isMToN()) {
if (!commit && rows != null) {
rows.removeAll(newRows);
rows.addAll(deletedRows);
}
newRows.clear();
deletedRows.clear();
}
}
/** Called when an object ID changes. */
public void notifyIDChanged(Object oldID, Object newID) {
// Go through Rows and look for references to oldID
Iterator i = getRows().iterator();
Column c1 = mapping.getTarget().getColumn();
Column c2 = mapping.getSource().getColumn();
while (i.hasNext()) {
Row row = (Row) i.next();
if (oldID.equals(row.getValue(c1))) {
row.setValue(c1, newID);
}
if (oldID.equals(row.getValue(c2))) {
row.setValue(c2, newID);
}
}
// TODO do we need to tell all contained items?
}
public boolean dependsOn(InterfaceInvocationHandler other) {
// See if any of the contents match
Iterator i = new ProxyIterator();
while (i.hasNext()) {
Object obj = i.next();
// Check if its key is new
InterfaceInvocationHandler handler = InterfaceInvocationHandler.getHandler(obj);
Object idObj = handler.getObjectId();
if (idObj instanceof TransientKey) {
// check depends on
if (handler.dependsOn(other)) {
return true;
}
}
}
return false;
}
protected Column getKeyColumn() {
return mapping.getTarget().getColumn();
}
protected class RelationshipProxyIterator extends ProxyIterator {
public void remove() {
if (!txnManaged) {
forceTransaction();
}
if (mapping.isMToN()) {
markAsDeleted(lastRow);
}
inner.remove();
if (!owner.isDirty()) {
owner.makeDirty();
}
rowToProxy.remove(lastRow);
}
}
public Iterator iterator() {
return iterator = new RelationshipProxyIterator();
}
// Methods for Many-to-Many support
/** Marks a row for removal. */
void markAsDeleted(Row row) {
if (newRows.contains(row)) {
// NEW_DELETED in this case means just pretend it never happened
newRows.remove(row);
} else {
deletedRows.add(row);
}
}
/** Marks a row as newly created. */
void markAsNew(Row row) {
if (deletedRows.contains(row)) {
deletedRows.remove(row);
} else {
newRows.add(row);
}
}
/**
* Retrieve the collection of rows that were created in the
* current transaction.
*/
Collection getNewRows() { return newRows; }
/**
* Retrieve the collection of rows that were removed in the
* current transaction.
*/
Collection getDeletedRows() { return deletedRows; }
}