KickJava   Java API By Example, From Geeks To Geeks.

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


1 /*****************************************************************
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements. See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership. The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License. You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied. See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  ****************************************************************/

19
20 package org.apache.cayenne.map;
21
22 import java.util.ArrayList JavaDoc;
23 import java.util.Collections JavaDoc;
24 import java.util.EventListener JavaDoc;
25 import java.util.Iterator JavaDoc;
26 import java.util.List JavaDoc;
27 import java.util.ListIterator JavaDoc;
28
29 import org.apache.commons.lang.builder.ToStringBuilder;
30 import org.apache.cayenne.CayenneRuntimeException;
31 import org.apache.cayenne.event.EventManager;
32 import org.apache.cayenne.exp.ExpressionException;
33 import org.apache.cayenne.exp.parser.ASTDbPath;
34 import org.apache.cayenne.map.event.RelationshipEvent;
35 import org.apache.cayenne.util.Util;
36 import org.apache.cayenne.util.XMLEncoder;
37
38 /**
39  * Describes navigational association between two Java classes, represented as source and
40  * target ObjEntity. Maps to a path of DbRelationships.
41  *
42  * @author Andrus Adamchik
43  */

44 public class ObjRelationship extends Relationship implements EventListener JavaDoc {
45
46     boolean readOnly;
47     boolean dbRelationshipsRefreshNeeded = true;
48
49     protected int deleteRule = DeleteRule.NO_ACTION;
50     protected boolean usedForLocking;
51     protected String JavaDoc dbRelationshipPath;
52
53     protected List JavaDoc dbRelationships = new ArrayList JavaDoc();
54
55     public ObjRelationship() {
56         this(null);
57     }
58
59     public ObjRelationship(String JavaDoc name) {
60         super(name);
61     }
62
63     /**
64      * Prints itself as XML to the provided XMLEncoder.
65      *
66      * @since 1.1
67      */

68     public void encodeAsXML(XMLEncoder encoder) {
69         ObjEntity source = (ObjEntity) getSourceEntity();
70         if (source == null) {
71             return;
72         }
73
74         encoder.print("<obj-relationship name=\"" + getName());
75         encoder.print("\" source=\"" + source.getName());
76
77         ObjEntity target = (ObjEntity) getTargetEntity();
78         if (target != null) {
79             encoder.print("\" target=\"" + target.getName());
80         }
81
82         if (isUsedForLocking()) {
83             encoder.print("\" lock=\"true");
84         }
85
86         String JavaDoc deleteRule = DeleteRule.deleteRuleName(getDeleteRule());
87         if (getDeleteRule() != DeleteRule.NO_ACTION && deleteRule != null) {
88             encoder.print("\" deleteRule=\"" + deleteRule);
89         }
90
91         // quietly get rid of invalid path... this is not the best way of doing things,
92
// but it is consistent across map package
93
String JavaDoc path = getValidRelationshipPath();
94         if (path != null) {
95             encoder.print("\" db-relationship-path=\"" + path);
96         }
97
98         encoder.println("\"/>");
99     }
100
101     /**
102      * Returns a target ObjEntity of this relationship. Entity is looked up in the parent
103      * DataMap using "targetEntityName".
104      */

105     public Entity getTargetEntity() {
106         String JavaDoc targetName = getTargetEntityName();
107         if (targetName == null) {
108             return null;
109         }
110
111         return getNonNullNamespace().getObjEntity(targetName);
112     }
113
114     /**
115      * Returns the name of a complimentary relationship going in the opposite direction or
116      * null if it doesn't exist.
117      *
118      * @since 1.2
119      */

120     public String JavaDoc getReverseRelationshipName() {
121         ObjRelationship reverse = getReverseRelationship();
122         return (reverse != null) ? reverse.getName() : null;
123     }
124
125     /**
126      * Returns a "complimentary" ObjRelationship going in the opposite direction. Returns
127      * null if no such relationship is found.
128      */

129     public ObjRelationship getReverseRelationship() {
130         // reverse the list
131
List JavaDoc reversed = new ArrayList JavaDoc();
132         Iterator JavaDoc rit = this.getDbRelationships().iterator();
133         while (rit.hasNext()) {
134             DbRelationship rel = (DbRelationship) rit.next();
135             DbRelationship reverse = rel.getReverseRelationship();
136             if (reverse == null)
137                 return null;
138
139             reversed.add(0, reverse);
140         }
141
142         Entity target = this.getTargetEntity();
143         if (target == null) {
144             return null;
145         }
146
147         Entity src = this.getSourceEntity();
148
149         Iterator JavaDoc it = target.getRelationships().iterator();
150         while (it.hasNext()) {
151             ObjRelationship rel = (ObjRelationship) it.next();
152             if (rel.getTargetEntity() != src)
153                 continue;
154
155             List JavaDoc otherRels = rel.getDbRelationships();
156             if (reversed.size() != otherRels.size())
157                 continue;
158
159             int len = reversed.size();
160             boolean relsMatch = true;
161             for (int i = 0; i < len; i++) {
162                 if (otherRels.get(i) != reversed.get(i)) {
163                     relsMatch = false;
164                     break;
165                 }
166             }
167
168             if (relsMatch)
169                 return rel;
170         }
171
172         return null;
173     }
174
175     /**
176      * Returns an immutable list of underlying DbRelationships.
177      */

178     public List JavaDoc getDbRelationships() {
179         refreshFromPath(true);
180         return Collections.unmodifiableList(dbRelationships);
181     }
182
183     /** Appends a DbRelationship to the existing list of DbRelationships. */
184     public void addDbRelationship(DbRelationship dbRel) {
185         refreshFromPath(true);
186
187         // Adding a second is creating a flattened relationship.
188
// Ensure that the new relationship properly continues
189
// on the flattened path
190
int numDbRelationships = dbRelationships.size();
191         if (numDbRelationships > 0) {
192             DbRelationship lastRel = (DbRelationship) dbRelationships
193                     .get(numDbRelationships - 1);
194             if (!lastRel.getTargetEntityName().equals(dbRel.getSourceEntity().getName())) {
195                 throw new CayenneRuntimeException("Error adding db relationship "
196                         + dbRel
197                         + " to ObjRelationship "
198                         + this
199                         + " because the source of the newly added relationship "
200                         + "is not the target of the previous relationship "
201                         + "in the chain");
202             }
203         }
204
205         EventManager.getDefaultManager().addListener(
206                 this,
207                 "dbRelationshipDidChange",
208                 RelationshipEvent.class,
209                 DbRelationship.PROPERTY_DID_CHANGE,
210                 dbRel);
211
212         dbRelationships.add(dbRel);
213
214         this.calculateReadOnlyValue();
215         this.calculateToManyValue();
216     }
217
218     /**
219      * Removes the relationship <code>dbRel</code> from the list of relationships.
220      */

221     public void removeDbRelationship(DbRelationship dbRel) {
222         refreshFromPath(true);
223
224         dbRelationships.remove(dbRel);
225         // Do not listen any more
226
EventManager.getDefaultManager().removeListener(
227                 this,
228                 DbRelationship.PROPERTY_DID_CHANGE,
229                 dbRel);
230
231         this.calculateReadOnlyValue();
232         this.calculateToManyValue();
233     }
234
235     public void clearDbRelationships() {
236         this.dbRelationshipPath = null;
237         this.dbRelationshipsRefreshNeeded = false;
238         this.dbRelationships.clear();
239         this.readOnly = false;
240         this.toMany = false;
241     }
242
243     /**
244      * Returns a boolean indicating whether modifying a target of such relationship in any
245      * way will not change the underlying table row of the source.
246      *
247      * @since 1.1
248      */

249     public boolean isSourceIndependentFromTargetChange() {
250         // note - call "isToPK" at the end of the chain, since
251
// if it is to a dependent PK, we still should return true...
252
return isToMany() || isFlattened() || isToDependentEntity() || !isToPK();
253     }
254
255     /**
256      * Returns true if underlying DbRelationships point to dependent entity.
257      */

258     public boolean isToDependentEntity() {
259         return ((DbRelationship) getDbRelationships().get(0)).isToDependentPK();
260     }
261
262     /**
263      * Returns true if the underlying DbRelationships point to a at least one of the
264      * columns of the target entity.
265      *
266      * @since 1.1
267      */

268     public boolean isToPK() {
269         return ((DbRelationship) getDbRelationships().get(0)).isToPK();
270     }
271
272     /**
273      * Returns true if the relationship is a "flattened" relationship. A relationship is
274      * considered "flattened" if it maps to more than one DbRelationship. Such chain of
275      * DbRelationships is also called "relationship path". All flattened relationships are
276      * at least readable, but only those formed across a many-many join table (with no
277      * custom attributes other than foreign keys) can be automatically written.
278      *
279      * @see #isReadOnly
280      * @return flag indicating if the relationship is flattened or not.
281      */

282     public boolean isFlattened() {
283         return getDbRelationships().size() > 1;
284     }
285
286     /**
287      * Returns true if the relationship is flattened, but is not of the single case that
288      * can have automatic write support. Otherwise, it returns false.
289      *
290      * @return flag indicating if the relationship is read only or not
291      */

292     public boolean isReadOnly() {
293         refreshFromPath(true);
294         return readOnly;
295     }
296
297     public boolean isToMany() {
298         refreshFromPath(true);
299         return super.isToMany();
300     }
301
302     /**
303      * Returns the deleteRule. The delete rule is a constant from the DeleteRule class,
304      * and specifies what should happen to the destination object when the source object
305      * is deleted.
306      *
307      * @return int a constant from DeleteRule
308      * @see #setDeleteRule
309      */

310     public int getDeleteRule() {
311         return deleteRule;
312     }
313
314     /**
315      * Sets the delete rule of the relationship.
316      *
317      * @param value New delete rule. Must be one of the constants defined in DeleteRule
318      * class.
319      * @see DeleteRule
320      * @throws IllegalArgumentException if the value is not a valid delete rule.
321      */

322     public void setDeleteRule(int value) {
323         if ((value != DeleteRule.CASCADE)
324                 && (value != DeleteRule.DENY)
325                 && (value != DeleteRule.NULLIFY)
326                 && (value != DeleteRule.NO_ACTION)) {
327
328             throw new IllegalArgumentException JavaDoc("Delete rule value "
329                     + value
330                     + " is not a constant from the DeleteRule class");
331         }
332
333         this.deleteRule = value;
334     }
335
336     public void dbRelationshipDidChange(RelationshipEvent event) {
337         calculateToManyValue();
338         calculateReadOnlyValue();
339     }
340
341     /**
342      * Returns whether this attribute should be used for locking.
343      *
344      * @since 1.1
345      */

346     public boolean isUsedForLocking() {
347         return usedForLocking;
348     }
349
350     /**
351      * Sets whether this attribute should be used for locking.
352      *
353      * @since 1.1
354      */

355     public void setUsedForLocking(boolean usedForLocking) {
356         this.usedForLocking = usedForLocking;
357     }
358
359     /**
360      * Returns a dot-separated path over mapped DbRelationships.
361      *
362      * @since 1.1
363      */

364     public String JavaDoc getDbRelationshipPath() {
365         if (dbRelationshipsRefreshNeeded) {
366             return dbRelationshipPath;
367         }
368         else {
369             // build path on the fly
370
if (getDbRelationships().isEmpty()) {
371                 return null;
372             }
373
374             StringBuffer JavaDoc path = new StringBuffer JavaDoc();
375             Iterator JavaDoc it = getDbRelationships().iterator();
376             while (it.hasNext()) {
377                 DbRelationship next = (DbRelationship) it.next();
378                 path.append(next.getName());
379                 if (it.hasNext()) {
380                     path.append(Entity.PATH_SEPARATOR);
381                 }
382             }
383
384             return path.toString();
385         }
386     }
387
388     /**
389      * Returns a reversed dbRelationship path.
390      *
391      * @since 1.2
392      */

393     public String JavaDoc getReverseDbRelationshipPath() throws ExpressionException {
394
395         List JavaDoc relationships = getDbRelationships();
396         if (relationships == null || relationships.isEmpty()) {
397             return null;
398         }
399
400         StringBuffer JavaDoc buffer = new StringBuffer JavaDoc();
401
402         // iterate in reverse order
403
ListIterator JavaDoc it = relationships.listIterator(relationships.size());
404         while (it.hasPrevious()) {
405
406             DbRelationship relationship = (DbRelationship) it.previous();
407             DbRelationship reverse = relationship.getReverseRelationship();
408
409             // another sanity check
410
if (reverse == null) {
411                 throw new CayenneRuntimeException("No reverse relationship exist for "
412                         + relationship);
413             }
414
415             if (buffer.length() > 0) {
416                 buffer.append(Entity.PATH_SEPARATOR);
417             }
418
419             buffer.append(reverse.getName());
420         }
421
422         return buffer.toString();
423     }
424
425     /**
426      * Sets mapped DbRelationships as a dot-separated path.
427      */

428     public void setDbRelationshipPath(String JavaDoc relationshipPath) {
429         if (!Util.nullSafeEquals(this.dbRelationshipPath, relationshipPath)) {
430             this.dbRelationshipPath = relationshipPath;
431             this.dbRelationshipsRefreshNeeded = true;
432         }
433     }
434
435     /**
436      * Returns dot-separated path over DbRelationships, only including components that
437      * have valid DbRelationships.
438      */

439     String JavaDoc getValidRelationshipPath() {
440         String JavaDoc path = getDbRelationshipPath();
441         if (path == null) {
442             return null;
443         }
444
445         ObjEntity entity = (ObjEntity) getSourceEntity();
446         if (entity == null) {
447             throw new CayenneRuntimeException(
448                     "Can't resolve DbRelationships, null source ObjEntity");
449         }
450
451         StringBuffer JavaDoc validPath = new StringBuffer JavaDoc();
452
453         try {
454             Iterator JavaDoc it = entity.resolvePathComponents(new ASTDbPath(path));
455             while (it.hasNext()) {
456                 DbRelationship relationship = (DbRelationship) it.next();
457
458                 if (validPath.length() > 0) {
459                     validPath.append(Entity.PATH_SEPARATOR);
460                 }
461                 validPath.append(relationship.getName());
462             }
463         }
464         catch (ExpressionException ex) {
465
466         }
467
468         return validPath.toString();
469     }
470
471     /**
472      * Rebuild a list of relationships if String relationshipPath has changed.
473      */

474     final void refreshFromPath(boolean stripInvalid) {
475         if (!dbRelationshipsRefreshNeeded) {
476             return;
477         }
478
479         synchronized (this) {
480             // check flag again in the synced block...
481
if (!dbRelationshipsRefreshNeeded) {
482                 return;
483             }
484
485             EventManager eventLoop = EventManager.getDefaultManager();
486
487             // remove existing relationships
488
Iterator JavaDoc removeIt = dbRelationships.iterator();
489             while (removeIt.hasNext()) {
490                 DbRelationship relationship = (DbRelationship) removeIt.next();
491                 eventLoop.removeListener(
492                         this,
493                         DbRelationship.PROPERTY_DID_CHANGE,
494                         relationship);
495
496                 removeIt.remove();
497             }
498
499             if (this.dbRelationshipPath != null) {
500
501                 ObjEntity entity = (ObjEntity) getSourceEntity();
502                 if (entity == null) {
503                     throw new CayenneRuntimeException(
504                             "Can't resolve DbRelationships, null source ObjEntity");
505                 }
506
507                 try {
508                     // add new relationships from path
509
Iterator JavaDoc it = entity.resolvePathComponents(new ASTDbPath(
510                             this.dbRelationshipPath));
511
512                     while (it.hasNext()) {
513                         DbRelationship relationship = (DbRelationship) it.next();
514
515                         // listen for changes
516
eventLoop.addListener(
517                                 this,
518                                 "dbRelationshipDidChange",
519                                 RelationshipEvent.class,
520                                 DbRelationship.PROPERTY_DID_CHANGE,
521                                 relationship);
522
523                         dbRelationships.add(relationship);
524                     }
525                 }
526                 catch (ExpressionException ex) {
527                     if (!stripInvalid) {
528                         throw ex;
529                     }
530                 }
531             }
532
533             calculateToManyValue();
534             calculateReadOnlyValue();
535
536             dbRelationshipsRefreshNeeded = false;
537         }
538     }
539
540     /**
541      * Recalculates whether a relationship is toMany or toOne, based on the underlying db
542      * relationships.
543      */

544     final void calculateToManyValue() {
545         // If there is a single toMany along the path, then the flattend
546
// rel is toMany. If all are toOne, then the rel is toOne.
547
// Simple (non-flattened) relationships form the degenerate case
548
// taking the value of the single underlying dbrel.
549
Iterator JavaDoc dbRelIterator = this.dbRelationships.iterator();
550         while (dbRelIterator.hasNext()) {
551             DbRelationship thisRel = (DbRelationship) dbRelIterator.next();
552             if (thisRel.isToMany()) {
553                 this.toMany = true;
554                 return;
555             }
556         }
557
558         this.toMany = false;
559     }
560
561     /**
562      * Recalculates a new readonly value based on the underlying DbRelationships.
563      */

564     final void calculateReadOnlyValue() {
565         // not flattened, always read/write
566
if (dbRelationships.size() < 2) {
567             this.readOnly = false;
568             return;
569         }
570
571         // too long, can't handle this yet
572
if (dbRelationships.size() > 2) {
573             this.readOnly = true;
574             return;
575         }
576
577         DbRelationship firstRel = (DbRelationship) dbRelationships.get(0);
578         DbRelationship secondRel = (DbRelationship) dbRelationships.get(1);
579
580         // only support many-to-many with single-step join
581
if (!firstRel.isToMany() || secondRel.isToMany()) {
582             this.readOnly = true;
583             return;
584         }
585
586         DataMap map = firstRel.getTargetEntity().getDataMap();
587         if (map == null) {
588             throw new CayenneRuntimeException(this.getClass().getName()
589                     + " could not obtain a DataMap for the destination of "
590                     + firstRel.getName());
591         }
592
593         // allow modifications if the joins are from FKs
594
if (!secondRel.isToPK()) {
595             this.readOnly = true;
596             return;
597         }
598
599         DbRelationship firstReverseRel = firstRel.getReverseRelationship();
600         if (firstReverseRel == null || !firstReverseRel.isToPK()) {
601             this.readOnly = true;
602             return;
603         }
604
605         this.readOnly = false;
606     }
607
608     public String JavaDoc toString() {
609         return new ToStringBuilder(this).append("name", getName()).append(
610                 "dbRelationshipPath",
611                 dbRelationshipPath).toString();
612     }
613
614     /**
615      * Returns an ObjAttribute stripped of any server-side information, such as
616      * DbAttribute mapping.
617      *
618      * @since 1.2
619      */

620     public ObjRelationship getClientRelationship() {
621         ObjRelationship reverse = getReverseRelationship();
622         String JavaDoc reverseName = reverse != null ? reverse.getName() : null;
623
624         ObjRelationship relationship = new ClientObjRelationship(
625                 getName(),
626                 reverseName,
627                 isToMany(),
628                 isReadOnly());
629
630         relationship.setTargetEntityName(getTargetEntityName());
631         relationship.setDeleteRule(getDeleteRule());
632
633         // TODO: copy locking flag...
634

635         return relationship;
636     }
637 }
638
Popular Tags