001: /*
002: $Header: /cvsroot/xorm/xorm/src/org/xorm/RelationshipProxy.java,v 1.33 2004/02/05 23:04:55 seifertd Exp $
003:
004: This file is part of XORM.
005:
006: XORM is free software; you can redistribute it and/or modify
007: it under the terms of the GNU General Public License as published by
008: the Free Software Foundation; either version 2 of the License, or
009: (at your option) any later version.
010:
011: XORM is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU General Public License for more details.
015:
016: You should have received a copy of the GNU General Public License
017: along with XORM; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package org.xorm;
021:
022: import java.util.AbstractCollection;
023: import java.util.ArrayList;
024: import java.util.Collection;
025: import java.util.HashMap;
026: import java.util.Iterator;
027: import java.util.logging.Logger;
028: import javax.jdo.JDOFatalUserException;
029:
030: import org.xorm.datastore.Column;
031: import org.xorm.datastore.DataFetchGroup;
032: import org.xorm.datastore.Row;
033: import org.xorm.datastore.Table;
034: import org.xorm.query.BoundExpression;
035: import org.xorm.query.AndCondition;
036: import org.xorm.query.Condition;
037: import org.xorm.query.Operator;
038: import org.xorm.query.QueryImpl;
039: import org.xorm.query.Selector;
040: import org.xorm.query.SimpleCondition;
041:
042: /**
043: * Represents a one-to-many or many-to-many relationship. An instance
044: * of RelationshipProxy is constructed in
045: * InterfaceInvocationHandler.invokeCollectionGet().
046: */
047: public class RelationshipProxy extends CollectionProxy {
048: protected static Logger logger = Logger
049: .getLogger("org.xorm.RelationshipProxy");
050:
051: // The field on the owner object that references this collection
052: protected boolean txnManaged;
053: protected RelationshipMapping mapping;
054: protected InterfaceInvocationHandler owner;
055:
056: // Additional fields used for many-to-many tracking
057: protected Collection deletedRows;
058: protected Collection newRows;
059:
060: // Arguments that need to be translated into parameters in a filtered
061: // relationship query
062: protected Object[] args = null;
063:
064: /** Returns the underlying relationship mapping this represents. */
065: public RelationshipMapping getRelationshipMapping() {
066: return mapping;
067: }
068:
069: /** Returns the owner of this directional relationship. */
070: public InterfaceInvocationHandler getOwner() {
071: return owner;
072: }
073:
074: /** Creates a new proxy backed by the data in the rows collection. */
075: public RelationshipProxy(InterfaceManager mgr,
076: RelationshipMapping mapping,
077: InterfaceInvocationHandler owner,
078: ClassMapping classMapping, Object[] args) {
079: super (mgr, classMapping);
080: this .mapping = mapping;
081: this .owner = owner;
082: this .txnManaged = owner.isTransactional();
083: if (mapping.isMToN()) {
084: this .newRows = new ArrayList();
085: this .deletedRows = new ArrayList();
086: }
087: this .args = args;
088: this .selector = getSelector();
089: }
090:
091: /** Overrides CollectionProxy implementation. */
092: public Class getElementType() {
093: return mapping.getSource().getElementClass();
094: }
095:
096: protected Selector getSelector() {
097: Object ownerPKey = owner.getObjectId();
098: if (ownerPKey instanceof TransientKey) {
099: return null;
100: }
101:
102: Column columnToUse = null;
103:
104: if (mapping.getSource().getColumn() != null) {
105: columnToUse = mapping.getSource().getColumn();
106: } else if (mapping.getFilter() != null) {
107: // In this case, source wasn't specified, but target
108: // is always specified...and in this case should be
109: // the primary key column of the owner class (see
110: // ModelMapping.parseRelationship).
111: columnToUse = mapping.getTarget().getColumn();
112: } else {
113: // How did we get here? An exception should have been
114: // thrown in ModelMapping.parseRelationship
115: throw new JDOFatalUserException(I18N
116: .msg("E_collection_no_source"));
117: }
118:
119: Selector selector = new Selector(columnToUse.getTable(),
120: new SimpleCondition(columnToUse, Operator.EQUAL,
121: ownerPKey));
122:
123: if (mapping.getFilter() != null) {
124: logger.fine("Filter: " + mapping.getFilter());
125: logger.fine("Imports: " + mapping.getImports());
126: logger.fine("Parameters: " + mapping.getParameters());
127: logger.fine("Variables: " + mapping.getVariables());
128: if (mapping.getParameters() != null
129: && !mapping.getParameters().equals("")
130: && (args == null || args.length == 0)) {
131: throw new NullPointerException(I18N
132: .msg("E_missing_arguments"));
133: }
134:
135: //logger.fine("Class Mapping Table: " + classMapping.getTable().getName());
136: Class filterTargetClass = classMapping.getMappedClass();
137: //logger.fine("Filter Target Class: " + filterTargetClass.getName());
138:
139: QueryImpl query = new QueryImpl(mgr);
140: query.setClass(filterTargetClass);
141: query.setFilter(mapping.getFilter());
142:
143: // We support the implicit "owner" parameter in filters.
144: // The owner refers to the object instance off which the
145: // collection field getter is being called. We need to add
146: // it to the parameters that we set on the query.
147: String parameters = mapping.getParameters();
148: ClassMapping ownerClassMapping = owner.getClassMapping();
149: Class ownerClass = ownerClassMapping.getMappedClass();
150: String ownerClassName = ownerClass.getName();
151: //ownerClassName = ownerClassName.substring(ownerClassName.lastIndexOf('.') + 1);
152: //logger.fine("OwnerClassName: " + ownerClassName);
153:
154: if (mapping.getImports() != null) {
155: query.declareImports(mapping.getImports());
156: }
157: if (parameters == null || parameters.equals("")) {
158: // No parameters, just this owner param
159: parameters = ownerClassName + " owner";
160: } else {
161: // Append this owner param to the existing parameters
162: parameters += ", " + ownerClassName + " owner";
163: }
164: logger.fine("Modified Parameters: " + parameters);
165: query.declareParameters(parameters);
166:
167: if (mapping.getVariables() != null) {
168: query.declareVariables(mapping.getVariables());
169: }
170:
171: if (mapping.getOrdering() != null) {
172: query.setOrdering(mapping.getOrdering());
173: }
174:
175: query.compile();
176: //logger.fine("Filter Query: " + query);
177:
178: BoundExpression bound = query.getBoundExpression();
179: int paramCount = 0;
180: if (args != null) {
181: while (paramCount < args.length) {
182: bound.bindParameter(paramCount, args[paramCount]);
183: ++paramCount;
184: }
185: }
186:
187: // Always bind the implicit "owner" parameter
188: bound.bindParameter(paramCount++, owner.getProxy());
189:
190: //logger.fine(paramCount + " parameter(s) bound");
191:
192: /*
193: logger.info("Main Selector Table Before: " +
194: selector.getTable().getName());
195: logger.info("Main Selector Before: " + selector);
196: */
197:
198: Selector filterSelector = bound.getSelector();
199:
200: /*
201: logger.info("Filter Selector Table: " +
202: filterSelector.getTable().getName());
203: logger.info("Filter Selector: " + filterSelector);
204: */
205:
206: if (mapping.getSource().getColumn() != null) {
207: //logger.fine("Joining filter selector with running selector");
208: /*
209: Condition join = new SimpleCondition(mapping.getTarget().getColumn(), Operator.EQUAL, filterSelector);
210: selector.setCondition(new AndCondition(join, selector.getCondition()));
211: */
212: selector.merge(filterSelector, Operator.ANDC);
213: } else {
214: // This is a free-floating collection getter query.
215: // There is no link table, there is no source.
216: // Just use the filter as the selector.
217: //logger.fine("Replacing selector with filter selector");
218: selector = filterSelector;
219: }
220:
221: /*
222: logger.info("Main Selector Table After: " +
223: selector.getTable().getName());
224: logger.info("Main Selector After: " + selector);
225: */
226: }
227:
228: // This is a hack until there is real OrderedSet/List support.
229: // key="order-by" can be specified in JDO metadata extensions
230: String orderBy = mapping.getOrderBy();
231: if (orderBy != null) {
232: int spacePos = orderBy.indexOf(' ');
233: String colName;
234: int orderingType = Selector.Ordering.ASCENDING;
235: if (spacePos != -1) {
236: colName = orderBy.substring(0, spacePos);
237: if ((spacePos + 1) < orderBy.length()) {
238: String type = orderBy.substring(spacePos + 1);
239: orderingType = "ASC".equalsIgnoreCase(type) ? Selector.Ordering.ASCENDING
240: : Selector.Ordering.DESCENDING;
241: }
242: } else {
243: colName = orderBy;
244: }
245: Selector.Ordering[] ordering = new Selector.Ordering[1];
246: ordering[0] = new Selector.Ordering(selector.getTable()
247: .getColumnByName(colName), orderingType);
248: selector.setOrdering(ordering);
249: }
250:
251: DataFetchGroup dfg = null;
252: FetchGroupManager fgm = mgr.getInterfaceManagerFactory()
253: .getFetchGroupManager();
254: if (mapping.isMToN()) {
255: // For many-to-many mappings, select all from the join table,
256: // and then the default fetch group of the target table
257: dfg = new DataFetchGroup(selector.getTable().getColumns());
258: dfg.addSubgroup(mapping.getTarget().getColumn(), fgm
259: .getDataFetchGroup(classMapping));
260: } else {
261: dfg = fgm.getDataFetchGroup(classMapping);
262: }
263: selector.require(dfg);
264:
265: return selector;
266: }
267:
268: /**
269: * Ensures that the proxy is registered in a transaction if necessary.
270: */
271: protected void forceTransaction() {
272: if (owner.isPersistent()) {
273: TransactionImpl txn = (TransactionImpl) owner
274: .getInterfaceManager().currentTransaction();
275: if (txn.isActive()) {
276: txn.attachRelationship(this );
277: txnManaged = true;
278: } else {
279: throw new JDOFatalUserException(I18N
280: .msg("E_modify_collection"));
281: }
282: }
283: }
284:
285: /** Adds the given object (proxy object) to the collection. */
286: public boolean add(Object o) {
287: // Sanity checking
288: if (o == null)
289: throw new NullPointerException();
290: if (!getElementType().isInstance(o)) {
291: throw new IllegalArgumentException(I18N.msg(
292: "E_element_type", getElementType().getName()));
293: }
294:
295: if (!txnManaged) {
296: forceTransaction();
297: }
298:
299: InterfaceInvocationHandler handler = InterfaceInvocationHandler
300: .getHandler(o);
301: Row row = new Row(mapping.getSource().getColumn().getTable());
302: row.setValue(mapping.getSource().getColumn(), owner
303: .getObjectId());
304: row.setValue(mapping.getTarget().getColumn(), handler
305: .getObjectId());
306: getRows().add(row);
307: if (!owner.isDirty()) {
308: owner.makeDirty();
309: }
310: rowToProxy.put(row, o);
311: if (mapping.isMToN()) {
312: markAsNew(row);
313: }
314:
315: // Transient objects that are added to a persistent relationship
316: // become persistent.
317: if (owner.isPersistent() && !handler.isPersistent()) {
318: handler.makePersistent(mgr);
319: }
320: return true;
321: }
322:
323: /** Removes the given object (proxy object) from the collection. */
324: public boolean remove(Object o) {
325: if (!txnManaged) {
326: forceTransaction();
327: }
328:
329: InterfaceInvocationHandler handler = InterfaceInvocationHandler
330: .getHandler(o);
331: Object targetKey = handler.getObjectId();
332:
333: // Find the row that matches the targetKey
334: Iterator i = getRows().iterator();
335: while (i.hasNext()) {
336: Row row = (Row) i.next();
337: if (targetKey.equals(row.getValue(mapping.getTarget()
338: .getColumn()))) {
339: if (mapping.isMToN()) {
340: markAsDeleted(row);
341: }
342: i.remove();
343: rowToProxy.remove(row);
344: if (!owner.isDirty()) {
345: owner.makeDirty();
346: }
347: return true;
348: }
349: }
350: return false; // not found
351: }
352:
353: /**
354: * Removes all rows. This is slightly more straightforward than
355: * iterating through and removing each element (but that works too).
356: */
357: public void clear() {
358: if (!txnManaged) {
359: forceTransaction();
360: }
361:
362: Iterator i = getRows().iterator();
363: if (i.hasNext()) {
364: if (!owner.isDirty()) {
365: owner.makeDirty();
366: }
367:
368: while (i.hasNext()) {
369: Row row = (Row) i.next();
370: if (mapping.isMToN()) {
371: markAsDeleted(row);
372: }
373: i.remove();
374: }
375: }
376: rowToProxy.clear();
377: }
378:
379: /**
380: * Removes any transactional context associated with this proxy,
381: * clearing the list of new and deleted rows.
382: */
383: void exitTransaction(boolean commit) {
384: txnManaged = false;
385: if (mapping.isMToN()) {
386: if (!commit && rows != null) {
387: rows.removeAll(newRows);
388: rows.addAll(deletedRows);
389: }
390: newRows.clear();
391: deletedRows.clear();
392: }
393: }
394:
395: /** Called when an object ID changes. */
396: public void notifyIDChanged(Object oldID, Object newID) {
397: // Go through Rows and look for references to oldID
398: Iterator i = getRows().iterator();
399: Column c1 = mapping.getTarget().getColumn();
400: Column c2 = mapping.getSource().getColumn();
401: while (i.hasNext()) {
402: Row row = (Row) i.next();
403: if (oldID.equals(row.getValue(c1))) {
404: row.setValue(c1, newID);
405: }
406: if (oldID.equals(row.getValue(c2))) {
407: row.setValue(c2, newID);
408: }
409: }
410: // TODO do we need to tell all contained items?
411: }
412:
413: public boolean dependsOn(InterfaceInvocationHandler other) {
414: // See if any of the contents match
415: Iterator i = new ProxyIterator();
416: while (i.hasNext()) {
417: Object obj = i.next();
418: // Check if its key is new
419: InterfaceInvocationHandler handler = InterfaceInvocationHandler
420: .getHandler(obj);
421: Object idObj = handler.getObjectId();
422: if (idObj instanceof TransientKey) {
423: // check depends on
424: if (handler.dependsOn(other)) {
425: return true;
426: }
427: }
428: }
429: return false;
430: }
431:
432: protected Column getKeyColumn() {
433: return mapping.getTarget().getColumn();
434: }
435:
436: protected class RelationshipProxyIterator extends ProxyIterator {
437: public void remove() {
438: if (!txnManaged) {
439: forceTransaction();
440: }
441: if (mapping.isMToN()) {
442: markAsDeleted(lastRow);
443: }
444: inner.remove();
445: if (!owner.isDirty()) {
446: owner.makeDirty();
447: }
448: rowToProxy.remove(lastRow);
449: }
450: }
451:
452: public Iterator iterator() {
453: return iterator = new RelationshipProxyIterator();
454: }
455:
456: // Methods for Many-to-Many support
457: /** Marks a row for removal. */
458: void markAsDeleted(Row row) {
459: if (newRows.contains(row)) {
460: // NEW_DELETED in this case means just pretend it never happened
461: newRows.remove(row);
462: } else {
463: deletedRows.add(row);
464: }
465: }
466:
467: /** Marks a row as newly created. */
468: void markAsNew(Row row) {
469: if (deletedRows.contains(row)) {
470: deletedRows.remove(row);
471: } else {
472: newRows.add(row);
473: }
474: }
475:
476: /**
477: * Retrieve the collection of rows that were created in the
478: * current transaction.
479: */
480: Collection getNewRows() {
481: return newRows;
482: }
483:
484: /**
485: * Retrieve the collection of rows that were removed in the
486: * current transaction.
487: */
488: Collection getDeletedRows() {
489: return deletedRows;
490: }
491:
492: }
|