KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > objectstyle > cayenne > map > ObjRelationship


1 /* ====================================================================
2  *
3  * The ObjectStyle Group Software License, version 1.1
4  * ObjectStyle Group - http://objectstyle.org/
5  *
6  * Copyright (c) 2002-2005, Andrei (Andrus) Adamchik and individual authors
7  * of the software. All rights reserved.
8  *
9  * Redistribution and use in source and binary forms, with or without
10  * modification, are permitted provided that the following conditions
11  * are met:
12  *
13  * 1. Redistributions of source code must retain the above copyright
14  * notice, this list of conditions and the following disclaimer.
15  *
16  * 2. Redistributions in binary form must reproduce the above copyright
17  * notice, this list of conditions and the following disclaimer in
18  * the documentation and/or other materials provided with the
19  * distribution.
20  *
21  * 3. The end-user documentation included with the redistribution, if any,
22  * must include the following acknowlegement:
23  * "This product includes software developed by independent contributors
24  * and hosted on ObjectStyle Group web site (http://objectstyle.org/)."
25  * Alternately, this acknowlegement may appear in the software itself,
26  * if and wherever such third-party acknowlegements normally appear.
27  *
28  * 4. The names "ObjectStyle Group" and "Cayenne" must not be used to endorse
29  * or promote products derived from this software without prior written
30  * permission. For written permission, email
31  * "andrus at objectstyle dot org".
32  *
33  * 5. Products derived from this software may not be called "ObjectStyle"
34  * or "Cayenne", nor may "ObjectStyle" or "Cayenne" appear in their
35  * names without prior written permission.
36  *
37  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
38  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
39  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
40  * DISCLAIMED. IN NO EVENT SHALL THE OBJECTSTYLE GROUP OR
41  * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
42  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
43  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
44  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
46  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
47  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
48  * SUCH DAMAGE.
49  * ====================================================================
50  *
51  * This software consists of voluntary contributions made by many
52  * individuals and hosted on ObjectStyle Group web site. For more
53  * information on the ObjectStyle Group, please see
54  * <http://objectstyle.org/>.
55  */

56 package org.objectstyle.cayenne.map;
57
58 import java.util.ArrayList JavaDoc;
59 import java.util.Collections JavaDoc;
60 import java.util.EventListener JavaDoc;
61 import java.util.Iterator JavaDoc;
62 import java.util.List JavaDoc;
63 import java.util.ListIterator JavaDoc;
64
65 import org.apache.commons.lang.builder.ToStringBuilder;
66 import org.apache.log4j.Logger;
67 import org.objectstyle.cayenne.CayenneRuntimeException;
68 import org.objectstyle.cayenne.event.EventManager;
69 import org.objectstyle.cayenne.exp.ExpressionException;
70 import org.objectstyle.cayenne.exp.parser.ASTDbPath;
71 import org.objectstyle.cayenne.map.event.RelationshipEvent;
72 import org.objectstyle.cayenne.util.Util;
73 import org.objectstyle.cayenne.util.XMLEncoder;
74
75 /**
76  * Describes navigational association between two Java classes, represented as source and
77  * target ObjEntity. Maps to a path of DbRelationships.
78  *
79  * @author Andrei Adamchik
80  */

81 public class ObjRelationship extends Relationship implements EventListener JavaDoc {
82
83     private static Logger logObj = Logger.getLogger(ObjRelationship.class);
84
85     int deleteRule = DeleteRule.NO_ACTION;
86     boolean readOnly;
87     boolean dbRelationshipsRefreshNeeded = true;
88
89     // Whether optimstic locking should consider this relationship
90
protected boolean usedForLocking;
91     protected String JavaDoc dbRelationshipPath;
92
93     List JavaDoc dbRelationships = new ArrayList JavaDoc();
94     List JavaDoc dbRelationshipsRef = Collections.unmodifiableList(dbRelationships);
95
96     public ObjRelationship() {
97     }
98
99     public ObjRelationship(String JavaDoc name) {
100         super(name);
101     }
102
103     /**
104      * Prints itself as XML to the provided XMLEncoder.
105      *
106      * @since 1.1
107      */

108     public void encodeAsXML(XMLEncoder encoder) {
109         ObjEntity source = (ObjEntity) getSourceEntity();
110         if (source == null) {
111             logObj
112                     .warn("No source entity, will not encode ObjRelationship: "
113                             + getName());
114             return;
115         }
116
117         encoder.print("<obj-relationship name=\"" + getName());
118         encoder.print("\" source=\"" + source.getName());
119
120         ObjEntity target = (ObjEntity) getTargetEntity();
121         if (target != null) {
122             encoder.print("\" target=\"" + target.getName());
123         }
124
125         if (isUsedForLocking()) {
126             encoder.print("\" lock=\"true");
127         }
128
129         String JavaDoc deleteRule = DeleteRule.deleteRuleName(getDeleteRule());
130         if (getDeleteRule() != DeleteRule.NO_ACTION && deleteRule != null) {
131             encoder.print("\" deleteRule=\"" + deleteRule);
132         }
133
134         // quietly get rid of invalid path... this is not the best way of doing things,
135
// but it is consistent across map package
136
String JavaDoc path = getValidRelationshipPath();
137         if (path != null) {
138             encoder.print("\" db-relationship-path=\"" + path);
139         }
140
141         encoder.println("\"/>");
142     }
143
144     public Entity getTargetEntity() {
145         String JavaDoc targetName = getTargetEntityName();
146         if (targetName == null) {
147             return null;
148         }
149
150         return getNonNullNamespace().getObjEntity(targetName);
151     }
152
153     /**
154      * Returns a "complimentary" ObjRelationship going in the opposite direction. Returns
155      * null if no such relationship is found.
156      */

157     public ObjRelationship getReverseRelationship() {
158         // reverse the list
159
List JavaDoc reversed = new ArrayList JavaDoc();
160         Iterator JavaDoc rit = this.getDbRelationships().iterator();
161         while (rit.hasNext()) {
162             DbRelationship rel = (DbRelationship) rit.next();
163             DbRelationship reverse = rel.getReverseRelationship();
164             if (reverse == null)
165                 return null;
166
167             reversed.add(0, reverse);
168         }
169
170         Entity target = this.getTargetEntity();
171         Entity src = this.getSourceEntity();
172
173         Iterator JavaDoc it = target.getRelationships().iterator();
174         while (it.hasNext()) {
175             ObjRelationship rel = (ObjRelationship) it.next();
176             if (rel.getTargetEntity() != src)
177                 continue;
178
179             List JavaDoc otherRels = rel.getDbRelationships();
180             if (reversed.size() != otherRels.size())
181                 continue;
182
183             int len = reversed.size();
184             boolean relsMatch = true;
185             for (int i = 0; i < len; i++) {
186                 if (otherRels.get(i) != reversed.get(i)) {
187                     relsMatch = false;
188                     break;
189                 }
190             }
191
192             if (relsMatch)
193                 return rel;
194         }
195
196         return null;
197     }
198
199     /**
200      * Returns an immutable list of underlying DbRelationships.
201      */

202     public List JavaDoc getDbRelationships() {
203         refreshFromPath(true);
204         return dbRelationshipsRef;
205     }
206
207     /** Appends a DbRelationship to the existing list of DbRelationships. */
208     public void addDbRelationship(DbRelationship dbRel) {
209         refreshFromPath(true);
210
211         // Adding a second is creating a flattened relationship.
212
// Ensure that the new relationship properly continues
213
// on the flattened path
214
int numDbRelationships = dbRelationships.size();
215         if (numDbRelationships > 0) {
216             DbRelationship lastRel = (DbRelationship) dbRelationships
217                     .get(numDbRelationships - 1);
218             if (!lastRel.getTargetEntityName().equals(dbRel.getSourceEntity().getName())) {
219                 throw new CayenneRuntimeException("Error adding db relationship "
220                         + dbRel
221                         + " to ObjRelationship "
222                         + this
223                         + " because the source of the newly added relationship "
224                         + "is not the target of the previous relationship "
225                         + "in the chain");
226             }
227         }
228
229         EventManager.getDefaultManager().addListener(this,
230                 "dbRelationshipDidChange",
231                 RelationshipEvent.class,
232                 DbRelationship.PROPERTY_DID_CHANGE,
233                 dbRel);
234
235         dbRelationships.add(dbRel);
236
237         this.calculateReadOnlyValue();
238         this.calculateToManyValue();
239     }
240
241     /**
242      * Removes the relationship <code>dbRel</code> from the list of relationships.
243      */

244     public void removeDbRelationship(DbRelationship dbRel) {
245         refreshFromPath(true);
246
247         dbRelationships.remove(dbRel);
248         //Do not listen any more
249
EventManager.getDefaultManager().removeListener(this,
250                 DbRelationship.PROPERTY_DID_CHANGE,
251                 dbRel);
252
253         this.calculateReadOnlyValue();
254         this.calculateToManyValue();
255     }
256
257     public void clearDbRelationships() {
258         this.dbRelationshipPath = null;
259         this.dbRelationshipsRefreshNeeded = false;
260         this.dbRelationships.clear();
261         this.readOnly = false;
262         this.toMany = false;
263     }
264
265     /**
266      * Returns a boolean indicating whether modifying a target of such relationship in any
267      * way will not change the underlying table row of the source.
268      *
269      * @since 1.1
270      */

271     public boolean isSourceIndependentFromTargetChange() {
272         // note - call "isToPK" at the end of the chain, since
273
// if it is to a dependent PK, we still should return true...
274
return isToMany() || isFlattened() || isToDependentEntity() || !isToPK();
275     }
276
277     /**
278      * Returns true if underlying DbRelationships point to dependent entity.
279      */

280     public boolean isToDependentEntity() {
281         return ((DbRelationship) getDbRelationships().get(0)).isToDependentPK();
282     }
283
284     /**
285      * Returns true if the underlying DbRelationships point to a at least one of the
286      * columns of the target entity.
287      *
288      * @since 1.1
289      */

290     public boolean isToPK() {
291         return ((DbRelationship) getDbRelationships().get(0)).isToPK();
292     }
293
294     /**
295      * Returns true if the relationship is a "flattened" relationship. A relationship is
296      * considered "flattened" if it maps to more than one DbRelationship. Such chain of
297      * DbRelationships is also called "relationship path". All flattened relationships are
298      * at least readable, but only those formed across a many-many join table (with no
299      * custom attributes other than foreign keys) can be automatically written.
300      *
301      * @see #isReadOnly
302      * @return flag indicating if the relationship is flattened or not.
303      */

304     public boolean isFlattened() {
305         return getDbRelationships().size() > 1;
306     }
307
308     /**
309      * Returns true if the relationship is flattened, but is not of the single case that
310      * can have automatic write support. Otherwise, it returns false.
311      *
312      * @return flag indicating if the relationship is read only or not
313      */

314     public boolean isReadOnly() {
315         refreshFromPath(true);
316         return readOnly;
317     }
318
319     public boolean isToMany() {
320         refreshFromPath(true);
321         return super.isToMany();
322     }
323
324     /**
325      * Returns the deleteRule. The delete rule is a constant from the DeleteRule class,
326      * and specifies what should happen to the destination object when the source object
327      * is deleted.
328      *
329      * @return int a constant from DeleteRule
330      * @see #setDeleteRule
331      */

332     public int getDeleteRule() {
333         return deleteRule;
334     }
335
336     /**
337      * Sets the delete rule of the relationship.
338      *
339      * @param value New delete rule. Must be one of the constants defined in DeleteRule
340      * class.
341      * @see DeleteRule
342      * @throws IllegalArgumentException if the value is not a valid delete rule.
343      */

344     public void setDeleteRule(int value) {
345         if ((value != DeleteRule.CASCADE)
346                 && (value != DeleteRule.DENY)
347                 && (value != DeleteRule.NULLIFY)
348                 && (value != DeleteRule.NO_ACTION)) {
349
350             throw new IllegalArgumentException JavaDoc("Delete rule value "
351                     + value
352                     + " is not a constant from the DeleteRule class");
353         }
354
355         this.deleteRule = value;
356     }
357
358     public void dbRelationshipDidChange(RelationshipEvent event) {
359         this.calculateToManyValue();
360     }
361
362     /**
363      * Returns whether this attribute should be used for locking.
364      *
365      * @since 1.1
366      */

367     public boolean isUsedForLocking() {
368         return usedForLocking;
369     }
370
371     /**
372      * Sets whether this attribute should be used for locking.
373      *
374      * @since 1.1
375      */

376     public void setUsedForLocking(boolean usedForLocking) {
377         this.usedForLocking = usedForLocking;
378     }
379
380     /**
381      * Returns a dot-separated path over mapped DbRelationships.
382      *
383      * @since 1.1
384      */

385     public String JavaDoc getDbRelationshipPath() {
386         if (dbRelationshipsRefreshNeeded) {
387             return dbRelationshipPath;
388         }
389         else {
390             // build path on the fly
391
if (getDbRelationships().isEmpty()) {
392                 return null;
393             }
394
395             StringBuffer JavaDoc path = new StringBuffer JavaDoc();
396             Iterator JavaDoc it = getDbRelationships().iterator();
397             while (it.hasNext()) {
398                 DbRelationship next = (DbRelationship) it.next();
399                 path.append(next.getName());
400                 if (it.hasNext()) {
401                     path.append(Entity.PATH_SEPARATOR);
402                 }
403             }
404
405             return path.toString();
406         }
407     }
408
409     /**
410      * Reverses dbRelationship path.
411      *
412      * @since 1.2
413      */

414     public String JavaDoc getReverseDbRelationshipPath() throws ExpressionException {
415
416         List JavaDoc relationships = getDbRelationships();
417         if (relationships == null || relationships.isEmpty()) {
418             return null;
419         }
420
421         StringBuffer JavaDoc buffer = new StringBuffer JavaDoc();
422
423         // iterate in reverse order
424
ListIterator JavaDoc it = relationships.listIterator(relationships.size());
425         while (it.hasPrevious()) {
426
427             DbRelationship relationship = (DbRelationship) it.previous();
428             DbRelationship reverse = relationship.getReverseRelationship();
429
430             // another sanity check
431
if (reverse == null) {
432                 throw new CayenneRuntimeException("No reverse relationship exist for "
433                         + relationship);
434             }
435
436             if (buffer.length() > 0) {
437                 buffer.append(Entity.PATH_SEPARATOR);
438             }
439
440             buffer.append(reverse.getName());
441         }
442
443         return buffer.toString();
444     }
445
446     /**
447      * Sets mapped DbRelationships as a dot-separated path.
448      */

449     public void setDbRelationshipPath(String JavaDoc relationshipPath) {
450         if (!Util.nullSafeEquals(this.dbRelationshipPath, relationshipPath)) {
451             this.dbRelationshipPath = relationshipPath;
452             this.dbRelationshipsRefreshNeeded = true;
453         }
454     }
455
456     /**
457      * Returns dot-separated path over DbRelationships, only including components that
458      * have valid DbRelationships.
459      */

460     String JavaDoc getValidRelationshipPath() {
461         String JavaDoc path = getDbRelationshipPath();
462         if (path == null) {
463             return null;
464         }
465
466         ObjEntity entity = (ObjEntity) getSourceEntity();
467         if (entity == null) {
468             throw new CayenneRuntimeException(
469                     "Can't resolve DbRelationships, null source ObjEntity");
470         }
471
472         StringBuffer JavaDoc validPath = new StringBuffer JavaDoc();
473
474         try {
475             Iterator JavaDoc it = entity.resolvePathComponents(new ASTDbPath(path));
476             while (it.hasNext()) {
477                 DbRelationship relationship = (DbRelationship) it.next();
478
479                 if (validPath.length() > 0) {
480                     validPath.append(Entity.PATH_SEPARATOR);
481                 }
482                 validPath.append(relationship.getName());
483             }
484         }
485         catch (ExpressionException ex) {
486
487         }
488
489         return validPath.toString();
490     }
491
492     /**
493      * Rebuild a list of relationships if String relationshipPath has changed.
494      */

495     final void refreshFromPath(boolean stripInvalid) {
496         if (!dbRelationshipsRefreshNeeded) {
497             return;
498         }
499
500         synchronized (this) {
501             // check flag again in the synced block...
502
if (!dbRelationshipsRefreshNeeded) {
503                 return;
504             }
505
506             EventManager eventLoop = EventManager.getDefaultManager();
507
508             // remove existing relationships
509
Iterator JavaDoc removeIt = dbRelationships.iterator();
510             while (removeIt.hasNext()) {
511                 DbRelationship relationship = (DbRelationship) removeIt.next();
512                 eventLoop.removeListener(this,
513                         DbRelationship.PROPERTY_DID_CHANGE,
514                         relationship);
515
516                 removeIt.remove();
517             }
518
519             if (this.dbRelationshipPath != null) {
520
521                 ObjEntity entity = (ObjEntity) getSourceEntity();
522                 if (entity == null) {
523                     throw new CayenneRuntimeException(
524                             "Can't resolve DbRelationships, null source ObjEntity");
525                 }
526
527                 try {
528                     // add new relationships from path
529
Iterator JavaDoc it = entity.resolvePathComponents(new ASTDbPath(
530                             this.dbRelationshipPath));
531
532                     while (it.hasNext()) {
533                         DbRelationship relationship = (DbRelationship) it.next();
534
535                         // listen for changes
536
eventLoop.addListener(this,
537                                 "dbRelationshipDidChange",
538                                 RelationshipEvent.class,
539                                 DbRelationship.PROPERTY_DID_CHANGE,
540                                 relationship);
541
542                         dbRelationships.add(relationship);
543                     }
544                 }
545                 catch (ExpressionException ex) {
546                     if (!stripInvalid) {
547                         throw ex;
548                     }
549                 }
550             }
551
552             calculateToManyValue();
553             calculateReadOnlyValue();
554
555             dbRelationshipsRefreshNeeded = false;
556         }
557     }
558
559     /**
560      * Recalculates whether a relationship is toMany or toOne, based on the underlying db
561      * relationships.
562      */

563     final void calculateToManyValue() {
564         //If there is a single toMany along the path, then the flattend
565
// rel is toMany. If all are toOne, then the rel is toOne.
566
// Simple (non-flattened) relationships form the degenerate case
567
// taking the value of the single underlying dbrel.
568
Iterator JavaDoc dbRelIterator = this.dbRelationships.iterator();
569         while (dbRelIterator.hasNext()) {
570             DbRelationship thisRel = (DbRelationship) dbRelIterator.next();
571             if (thisRel.isToMany()) {
572                 this.toMany = true;
573                 return;
574             }
575         }
576
577         this.toMany = false;
578     }
579
580     /**
581      * Recalculates a new readonly value based on the underlying DbRelationships.
582      */

583     final void calculateReadOnlyValue() {
584         // not flattened, always read/write
585
if (dbRelationships.size() < 2) {
586             this.readOnly = false;
587             return;
588         }
589
590         // too long, can't handle this yet
591
if (dbRelationships.size() > 2) {
592             this.readOnly = true;
593             return;
594         }
595
596         DbRelationship firstRel = (DbRelationship) dbRelationships.get(0);
597         DbRelationship secondRel = (DbRelationship) dbRelationships.get(1);
598
599         // only support many-to-many with single-step join
600
if (!firstRel.isToMany() || secondRel.isToMany()) {
601             this.readOnly = true;
602             return;
603         }
604
605         DataMap map = firstRel.getTargetEntity().getDataMap();
606         if (map == null) {
607             throw new CayenneRuntimeException(this.getClass().getName()
608                     + " could not obtain a DataMap for the destination of "
609                     + firstRel.getName());
610         }
611
612         // allow modifications if the joins are from FKs
613
if (!secondRel.isToPK()) {
614             this.readOnly = true;
615             return;
616         }
617
618         DbRelationship firstReverseRel = firstRel.getReverseRelationship();
619         if (firstReverseRel == null || !firstReverseRel.isToPK()) {
620             this.readOnly = true;
621             return;
622         }
623
624         this.readOnly = false;
625     }
626
627     public String JavaDoc toString() {
628         return new ToStringBuilder(this)
629                 .append("name", getName())
630                 .append("dbRelationshipPath", dbRelationshipPath)
631                 .toString();
632     }
633 }
Popular Tags