root/helma/helma/trunk/src/helma/objectmodel/db/Relation.java

Revision 9557, 50.3 kB (checked in by hannes, 1 year ago)

Add static getCollection() method on HopObject constructors to generate collections programmatically and on the fly. Implement limit and offset collection properties for databases that support it (Postgresql + Mysql)

  • Property svn:eol-style set to native
  • Property cvs2svn:cvs-rev set to 1.65
  • Property svn:keywords set to Date Revision Author HeadURL Id
Line 
1 /*
2  * Helma License Notice
3  *
4  * The contents of this file are subject to the Helma License
5  * Version 2.0 (the "License"). You may not use this file except in
6  * compliance with the License. A copy of the License is available at
7  * http://adele.helma.org/download/helma/license.txt
8  *
9  * Copyright 1998-2003 Helma Software. All Rights Reserved.
10  *
11  * $RCSfile$
12  * $Author$
13  * $Revision$
14  * $Date$
15  */
16
17 package helma.objectmodel.db;
18
19 import helma.framework.core.Application;
20 import helma.objectmodel.INode;
21 import helma.objectmodel.IProperty;
22 import helma.util.StringUtils;
23 import helma.util.ResourceProperties;
24
25 import java.sql.SQLException;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.Properties;
29 import java.util.Enumeration;
30 import java.util.Vector;
31
32 /**
33  * This describes how a property of a persistent Object is stored in a
34  *  relational database table. This can be either a scalar property (string, date, number etc.)
35  *  or a reference to one or more other objects.
36  */
37 public final class Relation {
38     // these constants define different type of property-to-db-mappings
39     // there is an error in the description of this relation
40     public final static int INVALID = -1;
41
42     // a mapping of a non-object, scalar type
43     public final static int PRIMITIVE = 0;
44
45     // a 1-to-1 relation, i.e. a field in the table is a foreign key to another object
46     public final static int REFERENCE = 1;
47
48     // a 1-to-many relation, a field in another table points to objects of this type
49     public final static int COLLECTION = 2;
50
51     // a 1-to-1 reference with multiple or otherwise not-trivial constraints
52     // this is managed differently than REFERENCE, hence the separate type.
53     public final static int COMPLEX_REFERENCE = 3;
54
55     // constraints linked together by OR or AND if applicable?
56     public final static String AND = " AND ";
57     public final static String OR = " OR ";
58     public final static String XOR = " XOR ";
59     private String logicalOperator = AND;
60
61     // prefix to use for symbolic names of joined tables. The name is composed
62     // from this prefix and the name of the property we're doing the join for
63     final static String JOIN_PREFIX = "JOIN_";
64
65     // direct mapping is a very powerful feature:
66     // objects of some types can be directly accessed
67     // by one of their properties/db fields.
68     // public final static int DIRECT = 3;
69     // the DbMapping of the type we come from
70     DbMapping ownType;
71
72     // the DbMapping of the prototype we link to, unless this is a "primitive" (non-object) relation
73     DbMapping otherType;
74
75     // the column type, as defined in java.sql.Types
76     int columnType;
77
78     //  if this relation defines a virtual node, we need to provide a DbMapping for these virtual nodes
79     DbMapping virtualMapping;
80     String propName;
81     String columnName;
82     int reftype;
83     Constraint[] constraints;
84     boolean virtual;
85     boolean readonly;
86     boolean aggressiveLoading;
87     boolean aggressiveCaching;
88     boolean isPrivate = false;
89     boolean referencesPrimaryKey = false;
90     String updateCriteria;
91     String accessName; // db column used to access objects through this relation
92     String order;
93     boolean autoSorted = false;
94     String groupbyOrder;
95     String groupby;
96     String prototype;
97     String groupbyPrototype;
98     String filter;
99     private String additionalTables;
100     private boolean additionalTablesJoined = false;
101     String queryHints;
102     Vector filterFragments;
103     Vector filterPropertyRefs;
104     int maxSize = 0;
105     int offset = 0;
106
107     /**
108      * This constructor makes a copy of an existing relation. Not all fields are copied, just those
109      * which are needed in groupby- and virtual nodes defined by this relation.
110      */
111     private Relation(Relation rel) {
112         // Note: prototype, groupby, groupbyPrototype and groupbyOrder aren't copied here.
113         // these are set by the individual get*Relation() methods as appropriate.
114         this.ownType =                  rel.ownType;
115         this.otherType =                rel.otherType;
116         this.propName =                 rel.propName;
117         this.columnName =               rel.columnName;
118         this.reftype =                  rel.reftype;
119         this.order =                    rel.order;
120         this.filter =                   rel.filter;
121         this.filterFragments =          rel.filterFragments;
122         this.filterPropertyRefs =       rel.filterPropertyRefs;
123         this.additionalTables =         rel.additionalTables;
124         this.additionalTablesJoined =   rel.additionalTablesJoined;
125         this.queryHints =               rel.queryHints;
126         this.maxSize =                  rel.maxSize;
127         this.offset =                   rel.offset;
128         this.constraints =              rel.constraints;
129         this.accessName =               rel.accessName;
130         this.logicalOperator =          rel.logicalOperator;
131         this.aggressiveLoading =        rel.aggressiveLoading;
132         this.aggressiveCaching =        rel.aggressiveCaching;
133         this.updateCriteria =           rel.updateCriteria;
134         this.autoSorted =               rel.autoSorted;
135     }
136
137     /**
138      * Reads a relation entry from a line in a properties file.
139      */
140     public Relation(String propName, DbMapping ownType) {
141         this.ownType = ownType;
142         this.propName = propName;
143         otherType = null;
144     }
145
146     ////////////////////////////////////////////////////////////////////////////////////////////
147     // parse methods for new file format
148     ////////////////////////////////////////////////////////////////////////////////////////////
149     public void update(Object desc, Properties props) {
150         Application app = ownType.getApplication();
151
152         if (desc instanceof Properties || parseDescriptor(desc, props)) {
153             // new style foo.collectionOf = Bar mapping
154             String proto;
155             if (props.containsKey("collection")) {
156                 proto = props.getProperty("collection");
157                 virtual = !"_children".equalsIgnoreCase(propName);
158                 reftype = COLLECTION;
159             } else if (props.containsKey("mountpoint")) {
160                 proto = props.getProperty("mountpoint");
161                 reftype = COLLECTION;
162                 virtual = true;
163                 this.prototype = proto;
164             } else if (props.containsKey("object")) {
165                 proto = props.getProperty("object");
166                 if (reftype != COMPLEX_REFERENCE) {
167                     reftype = REFERENCE;
168                 }
169                 virtual = false;
170             } else {
171                 throw new RuntimeException("Invalid property Mapping: " + desc);
172             }
173
174             otherType = app.getDbMapping(proto);
175
176             if (otherType == null) {
177                 throw new RuntimeException("DbMapping for " + proto +
178                                            " not found from " + ownType.getTypeName());
179             }
180
181             // make sure the type we're referring to is up to date!
182             if (otherType.needsUpdate()) {
183                 otherType.update();
184             }
185
186         }
187
188         readonly = "true".equalsIgnoreCase(props.getProperty("readonly"));
189         isPrivate = "true".equalsIgnoreCase(props.getProperty("private"));
190
191         // the following options only apply to object and collection relations
192         if ((reftype != PRIMITIVE) && (reftype != INVALID)) {
193             Vector newConstraints = new Vector();
194
195             parseOptions(newConstraints, props);
196
197             constraints = new Constraint[newConstraints.size()];
198             newConstraints.copyInto(constraints);
199
200
201             if (reftype == REFERENCE || reftype == COMPLEX_REFERENCE) {
202                 if (constraints.length == 0) {
203                     referencesPrimaryKey = true;
204                 } else {
205                     boolean rprim = false;
206                     for (int i=0; i<constraints.length; i++) {
207                         if (constraints[i].foreignKeyIsPrimary()) {
208                             rprim = true;
209                             break;
210                         }
211                     }
212                     referencesPrimaryKey = rprim;
213                 }
214
215                 // check if this is a non-trivial reference
216                 if (constraints.length > 1 || !usesPrimaryKey()) {
217                     reftype = COMPLEX_REFERENCE;
218                 } else {
219                     reftype = REFERENCE;
220                 }
221             }
222
223             if (reftype == COLLECTION) {
224                 referencesPrimaryKey = (accessName == null) ||
225                         accessName.equalsIgnoreCase(otherType.getIDField());
226             }
227
228             // if DbMapping for virtual nodes has already been created,
229             // update its subnode relation.
230             // FIXME: needs to be synchronized?
231             if (virtualMapping != null) {
232                 virtualMapping.lastTypeChange = ownType.lastTypeChange;
233                 virtualMapping.subRelation = getVirtualSubnodeRelation();
234                 virtualMapping.propRelation = getVirtualPropertyRelation();
235             }
236         } else {
237             referencesPrimaryKey = false;
238         }
239     }
240
241     /**
242      * Converts old style foo = collection(Bar) mapping to new style
243      * foo.collection = Bar mappinng and returns true if a non-primitive mapping
244      * was encountered.
245      * @param value the value of the top level property mapping
246      * @param config the sub-map for this property mapping
247      * @return true if the value describes a valid, non-primitive property mapping
248      */
249     protected boolean parseDescriptor(Object value, Map config) {
250         String desc = value instanceof String ? (String) value : null;
251
252         if ((desc == null) || "".equals(desc.trim())) {
253             if (propName != null) {
254                 reftype = PRIMITIVE;
255                 columnName = propName;
256             } else {
257                 reftype = INVALID;
258                 columnName = propName;
259             }
260             return false;
261         } else {
262             desc = desc.trim();
263
264             int open = desc.indexOf("(");
265             int close = desc.indexOf(")");
266
267             if ((open > -1) && (close > open)) {
268                 String ref = desc.substring(0, open).trim();
269                 String proto = desc.substring(open + 1, close).trim();
270
271                 if ("collection".equalsIgnoreCase(ref)) {
272                     config.put("collection", proto);
273                 } else if ("mountpoint".equalsIgnoreCase(ref)) {
274                     config.put("mountpoint", proto);
275                 } else if ("object".equalsIgnoreCase(ref)) {
276                     config.put("object", proto);
277                 } else {
278                     throw new RuntimeException("Invalid property Mapping: " + desc);
279                 }
280
281                 return true;
282
283             } else {
284                 virtual = false;
285                 columnName = desc;
286                 reftype = PRIMITIVE;
287                 return false;
288             }
289         }
290
291     }
292
293     protected void parseOptions(Vector cnst, Properties props) {
294         String loading = props.getProperty("loadmode");
295
296         aggressiveLoading = (loading != null) &&
297                             "aggressive".equalsIgnoreCase(loading.trim());
298
299         String caching = props.getProperty("cachemode");
300
301         aggressiveCaching = (caching != null) &&
302                             "aggressive".equalsIgnoreCase(caching.trim());
303
304         // get order property
305         order = props.getProperty("order");
306
307         if ((order != null) && (order.trim().length() == 0)) {
308             order = null;
309         }
310
311         // get the criteria(s) for updating this collection
312         updateCriteria = props.getProperty("updatecriteria");
313
314         // get the autosorting flag
315         autoSorted = "auto".equalsIgnoreCase(props.getProperty("sortmode"));
316
317         // get additional filter property
318         filter = props.getProperty("filter");
319
320         if (filter != null) {
321             if (filter.trim().length() == 0) {
322                 filter = null;
323                 filterFragments = filterPropertyRefs = null;
324             } else {
325                 // parenthesise filter
326                 Vector fragments = new Vector();
327                 Vector propertyRefs = new Vector();
328                 parsePropertyString(filter, fragments, propertyRefs);
329                 // if no references where found, just use the filter string
330                 // otherwise use the filter fragments and proeprty refs instead
331                 if (propertyRefs.size() > 0) {
332                     filterFragments = fragments;
333                     filterPropertyRefs = propertyRefs;
334                 } else {
335                     filterFragments = filterPropertyRefs = null;
336                 }
337             }
338         }
339
340         // get additional tables
341         additionalTables = props.getProperty("filter.additionalTables");
342
343         if (additionalTables != null) {
344             if (additionalTables.trim().length() == 0) {
345                 additionalTables = null;
346             } else {
347                 String ucTables = additionalTables.toUpperCase();
348                 // create dependencies implied by additional tables
349                 DbSource dbsource = otherType.getDbSource();
350                 if (dbsource != null) {
351                     String[] tables = StringUtils.split(ucTables, ", ");
352                     for (int i=0; i<tables.length; i++) {
353                         // Skip some join-related keyworks we might encounter here
354                         if ("AS".equals(tables[i]) || "ON".equals(tables[i])) {
355                             continue;
356                         }
357                         DbMapping dbmap = dbsource.getDbMapping(tables[i]);
358                         if (dbmap != null) {
359                             dbmap.addDependency(otherType);
360                         }
361                     }
362                 }
363                 // see wether the JOIN syntax is used. look for " join " with whitespaces on both sides
364                 // and for "join " at the beginning:
365                 additionalTablesJoined = (ucTables.indexOf(" JOIN ") != -1 ||
366                         ucTables.startsWith("STRAIGHT_JOIN ") || ucTables.startsWith("JOIN "));
367             }
368         }
369
370         // get query hints
371         queryHints = props.getProperty("hints");
372
373         // get max size of collection
374         maxSize = getIntegerProperty("maxSize", props, 0);
375         if (maxSize == 0) {
376             // use limit as alias for maxSize
377             maxSize = getIntegerProperty("limit", props, 0);
378         }
379         offset = getIntegerProperty("offset", props, 0);
380
381         // get group by property
382         groupby = props.getProperty("group");
383
384         if ((groupby != null) && (groupby.trim().length() == 0)) {
385             groupby = null;
386         }
387
388         if (groupby != null) {
389             groupbyOrder = props.getProperty("group.order");
390
391             if ((groupbyOrder != null) && (groupbyOrder.trim().length() == 0)) {
392                 groupbyOrder = null;
393             }
394
395             groupbyPrototype = props.getProperty("group.prototype");
396
397             if ((groupbyPrototype != null) && (groupbyPrototype.trim().length() == 0)) {
398                 groupbyPrototype = null;
399             }
400
401             // aggressive loading and caching is not supported for groupby-nodes
402             // aggressiveLoading = aggressiveCaching = false;
403         }
404
405         // check if subnode condition should be applied for property relations
406         accessName = props.getProperty("accessname");
407
408         // parse contstraints
409         String local = props.getProperty("local");
410         String foreign = props.getProperty("foreign");
411
412         if ((local != null) && (foreign != null)) {
413             cnst.addElement(new Constraint(local, foreign, false));
414             columnName = local;
415         }
416
417         // parse additional contstraints from *.1 to *.9
418         for (int i=1; i<10; i++) {
419             local = props.getProperty("local."+i);
420             foreign = props.getProperty("foreign."+i);
421
422             if ((local != null) && (foreign != null)) {
423                 cnst.addElement(new Constraint(local, foreign, false));
424             }
425         }
426
427         // parse constraints logic
428         if (cnst.size() > 1) {
429             String logic = props.getProperty("logicalOperator");
430             if ("and".equalsIgnoreCase(logic)) {
431                 logicalOperator = AND;
432             } else if ("or".equalsIgnoreCase(logic)) {
433                 logicalOperator = OR;
434             } else if ("xor".equalsIgnoreCase(logic)) {
435                 logicalOperator = XOR;
436             } else {
437                 logicalOperator = AND;
438             }
439         } else {
440             logicalOperator = AND;
441         }
442
443     }
444
445     private int getIntegerProperty(String name, Properties props, int defaultValue) {
446         Object value = props.get(name);
447
448         if (value instanceof Number) {
449             return ((Number) value).intValue();
450         } else if (value instanceof String) {
451             return Integer.parseInt((String) value);
452         }
453         return defaultValue;
454     }
455
456     ///////////////////////////////////////////////////////////////////////////////////////////
457
458     /**
459      * Get the configuration properties for this relation.
460      */
461     public Map getConfig() {
462         return ownType.getSubProperties(propName + '.');
463     }
464
465     /**
466      * Does this relation describe a virtual (collection) node?
467      */
468     public boolean isVirtual() {
469         return virtual;
470     }
471
472     /**
473      * Return the target type of this relation, or null if this is a primitive mapping.
474      */
475     public DbMapping getTargetType() {
476         return otherType;
477     }
478
479     /**
480      * Get the reference type of this relation.
481      */
482     public int getRefType() {
483         return reftype;
484     }
485
486     /**
487      * Tell if this relation represents a primitive (scalar) value mapping.
488      */
489     public boolean isPrimitive() {
490         return reftype == PRIMITIVE;
491     }
492
493     /**
494      *  Returns true if this Relation describes an object reference property
495      */
496     public boolean isReference() {
497         return reftype == REFERENCE;
498     }
499
500     /**
501      *  Returns true if this Relation describes either a primitive value
502      *  or an object reference.
503      */
504     public boolean isPrimitiveOrReference() {
505         return reftype == PRIMITIVE || reftype == REFERENCE;
506     }
507
508     /**
509      *  Returns true if this Relation describes a collection.
510      *  <b>NOTE:</b> this will return true both for collection objects
511      *  (aka virtual nodes) and direct child object relations, so
512      *  isVirtual() should be used to identify relations that define
513      *  <i>collection properties</i>!
514      */
515     public boolean isCollection() {
516         return reftype == COLLECTION;
517     }
518
519     /**
520      *  Returns true if this Relation describes a complex object reference property
521      */
522     public boolean isComplexReference() {
523         return reftype == COMPLEX_REFERENCE;
524     }
525
526     /**
527      *  Tell wether the property described by this relation is to be handled as private, i.e.
528      *  a change on it should not result in any changed object/collection relations.
529      */
530     public boolean isPrivate() {
531         return isPrivate;
532     }
533
534     /**
535      *  Check whether aggressive loading is set for this relation
536      */
537     public boolean loadAggressively() {
538         return aggressiveLoading;
539     }
540
541     /**
542      *  Returns the number of constraints for this relation.
543      */
544     public int countConstraints() {
545         if (constraints == null)
546             return 0;
547         return constraints.length;
548     }
549
550     /**
551      *  Returns true if the object represented by this Relation has to be
552      *  created on demand at runtime by the NodeManager. This is true for:
553      *
554      *  - collection (aka virtual) nodes
555      *  - nodes accessed via accessname
556      *  - group nodes
557      *  - complex reference nodes
558      */
559     public boolean createOnDemand() {
560         if (otherType == null) {
561             return false;
562         }
563
564         return virtual ||
565             (otherType.isRelational() && accessName != null) ||
566             (groupby != null) || isComplexReference();
567     }
568
569     /**
570      *  Returns true if the object represented by this Relation has to be
571      *  persisted in the internal db in order to be functional. This is true if
572      *  the subnodes contained in this collection are stored in the embedded
573      *  database. In this case, the collection itself must also be an ordinary
574      *  object stored in the db, since a virtual collection would lose its
575      *  its content after restarts.
576      */
577     public boolean needsPersistence() {
578         if (!virtual) {
579             // ordinary object references always need to be persisted
580             return true;
581         }
582
583         // collections/mountpoints need to be persisted if the
584         // child object type is non-relational.
585         if (prototype == null) {
586             return !otherType.isRelational();
587         }
588
589         DbMapping sub = otherType.getSubnodeMapping();
590
591         return (sub != null) && !sub.isRelational();
592     }
593
594     /**
595      * Return the prototype to be used for object reached by this relation
596      */
597     public String getPrototype() {
598         return prototype;
599     }
600
601     /**
602      * Return the name of the local property this relation is defined for
603      */
604     public String getPropName() {
605         return propName;
606     }
607
608     /**
609      *
610      *
611      * @param ct ...
612      */
613     public void setColumnType(int ct) {
614         columnType = ct;
615     }
616
617     /**
618      *
619      *
620      * @return ...
621      */
622     public int getColumnType() {
623         return columnType;
624     }
625
626     /**
627      *  Get the group for a collection relation, if defined.
628      *
629      * @return the name of the column used to group child objects, if any.
630      */
631     public String getGroup() {
632         return groupby;
633     }
634
635     /**
636      * Add a constraint to the current list of constraints
637      */
638     protected void addConstraint(Constraint c) {
639         if (constraints == null) {
640             constraints = new Constraint[1];
641             constraints[0] = c;
642         } else {
643             Constraint[] nc = new Constraint[constraints.length + 1];
644
645             System.arraycopy(constraints, 0, nc, 0, constraints.length);
646             nc[nc.length - 1] = c;
647             constraints = nc;
648         }
649     }
650
651     /**
652      *
653      *
654      * @return true if the foreign key used for this relation is the
655      * other object's primary key.
656      */
657     public boolean usesPrimaryKey() {
658         return referencesPrimaryKey;
659     }
660
661     /**
662      *
663      *
664      * @return ...
665      */
666     public boolean hasAccessName() {
667         return accessName != null;
668     }
669
670     /**
671      *
672      *
673      * @return ...
674      */
675     public String getAccessName() {
676         return accessName;
677     }
678
679     /**
680      *
681      *
682      * @return ...
683      */
684     public Relation getSubnodeRelation() {
685         // return subnoderelation;
686         return null;
687     }
688
689     /**
690      * Return the local field name for updates.
691      */
692     public String getDbField() {
693         return columnName;
694     }
695
696     /**
697      * This is taken from org.apache.tools.ant ProjectHelper.java
698      * distributed under the Apache Software License, Version 1.1
699      *
700      * Parses a string containing <code>${xxx}</code> style property
701      * references into two lists. The first list is a collection
702      * of text fragments, while the other is a set of string property names.
703      * <code>null</code> entries in the first list indicate a property
704      * reference from the second list.
705      *
706      * @param value     Text to parse. Must not be <code>null</code>.
707      * @param fragments List to add text fragments to.
708      *                  Must not be <code>null</code>.
709      * @param propertyRefs List to add property names to.
710      *                     Must not be <code>null</code>.
711      */
712     protected void parsePropertyString(String value, Vector fragments, Vector propertyRefs) {
713         int prev = 0;
714         int pos;
715         //search for the next instance of $ from the 'prev' position
716         while ((pos = value.indexOf("$", prev)) >= 0) {
717
718             //if there was any text before this, add it as a fragment
719             //TODO, this check could be modified to go if pos>prev;
720             //seems like this current version could stick empty strings
721             //into the list
722             if (pos > 0) {
723                 fragments.addElement(value.substring(prev, pos));
724             }
725             //if we are at the end of the string, we tack on a $
726             //then move past it
727             if (pos == (value.length() - 1)) {
728                 fragments.addElement("$");
729                 prev = pos + 1;
730             } else if (value.charAt(pos + 1) != '{') {
731                 //peek ahead to see if the next char is a property or not
732                 //not a property: insert the char as a literal
733                 /*
734                 fragments.addElement(value.substring(pos + 1, pos + 2));
735                 prev = pos + 2;
736                 */
737                 if (value.charAt(pos + 1) == '$') {
738                     //backwards compatibility two $ map to one mode
739                     fragments.addElement("$");
740                     prev = pos + 2;
741                 } else {
742                     //new behaviour: $X maps to $X for all values of X!='$'
743                     fragments.addElement(value.substring(pos, pos + 2));
744                     prev = pos + 2;
745                 }
746
747             } else {
748                 //property found, extract its name or bail on a typo
749                 int endName = value.indexOf('}', pos);
750                 if (endName < 0) {
751                     throw new RuntimeException("Syntax error in property: "
752                                                  + value);
753                 }
754                 String propertyName = value.substring(pos + 2, endName);
755                 fragments.addElement(null);
756                 propertyRefs.addElement(propertyName);
757                 prev = endName + 1;
758             }
759         }
760         //no more $ signs found
761         //if there is any tail to the file, append it
762         if (prev < value.length()) {
763             fragments.addElement(value.substring(prev));
764         }
765     }
766
767     /**
768      *  get a DbMapping to use for virtual aka collection nodes.
769      */
770     public DbMapping getVirtualMapping() {
771         // return null unless this relation describes a virtual/collection node.
772         if (!virtual) {
773             return null;
774         }
775
776         // create a synthetic DbMapping that describes how to fetch the
777         // collection's child objects.
778         if (virtualMapping == null) {
779             // if the collection node is prototyped (a mountpoint), create
780             // a virtual sub-mapping from the app's DbMapping for that prototype
781             if (prototype != null) {
782                 virtualMapping = new DbMapping(ownType.app, prototype);
783             } else {
784                 virtualMapping = new DbMapping(ownType.app, null);
785                 virtualMapping.subRelation = getVirtualSubnodeRelation();
786                 virtualMapping.propRelation = getVirtualPropertyRelation();
787             }
788         }
789
790         return virtualMapping;
791     }
792
793     /**
794      * Return the db mapping for a propery relation.
795      * @return the target mapping of this property relation
796      */
797     public DbMapping getPropertyMapping() {
798         // if this is an untyped virtual node, it doesn't have a dbmapping
799         if (!virtual || prototype != null) {
800             return otherType;
801         }
802         return null;
803     }
804
805     /**
806      * Return a Relation that defines the subnodes of a virtual node.
807      */
808     Relation getVirtualSubnodeRelation() {
809         if (!virtual) {
810             throw new RuntimeException("getVirtualSubnodeRelation called on non-virtual relation");
811         }
812
813         Relation vr = new Relation(this);
814
815         vr.groupby = groupby;
816         vr.groupbyOrder = groupbyOrder;
817         vr.groupbyPrototype = groupbyPrototype;
818
819         return vr;
820     }
821
822     /**
823      * Return a Relation that defines the properties of a virtual node.
824      */
825     Relation getVirtualPropertyRelation() {
826         if (!virtual) {
827             throw new RuntimeException("getVirtualPropertyRelation called on non-virtual relation");
828         }
829
830         Relation vr = new Relation(this);
831
832         vr.groupby = groupby;
833         vr.groupbyOrder = groupbyOrder;
834         vr.groupbyPrototype = groupbyPrototype;
835
836         return vr;
837     }
838
839     /**
840      * Return a Relation that defines the subnodes of a group-by node.
841      */
842     Relation getGroupbySubnodeRelation() {
843         if (groupby == null) {
844             throw new RuntimeException("getGroupbySubnodeRelation called on non-group-by relation");
845         }
846
847         Relation vr = new Relation(this);
848
849         vr.prototype = groupbyPrototype;
850         vr.addConstraint(new Constraint(null, groupby, true));
851
852         return vr;
853     }
854
855     /**
856      * Return a Relation that defines the properties of a group-by node.
857      */
858     Relation getGroupbyPropertyRelation() {
859         if (groupby == null) {
860             throw new RuntimeException("getGroupbyPropertyRelation called on non-group-by relation");
861         }
862
863         Relation vr = new Relation(this);
864
865         vr.prototype = groupbyPrototype;
866         vr.addConstraint(new Constraint(null, groupby, true));
867
868         return vr;
869     }
870
871     /**
872      *  Build the second half of an SQL select statement according to this relation
873      *  and a local object.
874      */
875     public String buildQuery(INode home, INode nonvirtual,
876                              String kstr, String pre, boolean useOrder)
877             throws SQLException, ClassNotFoundException {
878         return buildQuery(home, nonvirtual, otherType, kstr, pre, useOrder);
879     }
880
881     /**
882      *  Build the second half of an SQL select statement according to this relation
883      *  and a local object.
884      */
885     public String buildQuery(INode home, INode nonvirtual, DbMapping otherDbm,
886                              String kstr, String pre, boolean useOrder)
887             throws SQLException, ClassNotFoundException {
888         StringBuffer q = new StringBuffer();
889         String prefix = pre;
890
891         if (kstr != null && !isComplexReference()) {
892             q.append(prefix);
893
894             String accessColumn = (accessName == null) ?
895                     otherDbm.getIDField() : accessName;
896             otherDbm.appendCondition(q, accessColumn, kstr);
897
898             prefix = " AND ";
899         }
900
901         // render the constraints and filter
902         renderConstraints(q, home, nonvirtual, otherDbm, prefix);
903
904         // add joined fetch constraints
905         ownType.addJoinConstraints(q, prefix);
906
907         // add group and order clauses
908         if (groupby != null) {
909             q.append(" GROUP BY ").append(groupby);
910
911             if (useOrder && (groupbyOrder != null)) {
912                 q.append(" ORDER BY ").append(groupbyOrder);
913             }
914         } else if (useOrder && (order != null)) {
915             q.append(" ORDER BY ").append(order);
916         }
917
918         if (maxSize > 0 && !ownType.isOracle()) {
919             q.append(" LIMIT ").append(maxSize);
920             if (offset > 0) {
921                 q.append(" OFFSET ").append(offset);
922             }
923         }
924
925         return q.toString();
926     }
927
928     protected void appendAdditionalTables(StringBuffer q) {
929         if (additionalTables != null) {
930             q.append(additionalTablesJoined ? ' ' : ',');
931             q.append(additionalTables);
932         }
933     }
934
935     /**
936      *  Build the filter.
937      */
938     protected void appendFilter(StringBuffer q, INode nonvirtual, String prefix) {
939         q.append(prefix);
940         q.append('(');
941         if (filterFragments == null) {
942             q.append(filter);
943         } else {
944             Enumeration i = filterFragments.elements();
945             Enumeration j = filterPropertyRefs.elements();
946             while (i.hasMoreElements()) {
947                 String fragment = (String) i.nextElement();
948                 if (fragment == null) {
949                     // begin column version
950                     String columnName = (String) j.nextElement();
951                     Object value = null;
952                     if (columnName != null) {
953                         DbMapping dbmap = nonvirtual.getDbMapping();
954                         String propertyName = dbmap.columnNameToProperty(columnName);
955                         if (propertyName == null)
956                             propertyName = columnName;
957                         IProperty property = nonvirtual.get(propertyName);
958                         if (property != null) {
959                             value = property.getStringValue();
960                         }
961                         if (value == null) {
962                             if (columnName.equalsIgnoreCase(dbmap.getIDField())) {
963                                 value = nonvirtual.getID();
964                             } else if (columnName.equalsIgnoreCase(dbmap.getNameField())) {
965                                 value = nonvirtual.getName();
966                             } else if (columnName.equalsIgnoreCase(dbmap.getPrototypeField())) {
967                                 value = dbmap.getExtensionId();
968                             }
969                         }
970                     }
971                     // end column version
972                     if (value != null) {
973                         q.append(DbMapping.escapeString(value.toString()));
974                     } else {
975                         q.append("NULL");
976                     }
977                 } else {
978                     q.append(fragment);
979                 }
980             }
981         }
982         q.append(')');
983     }
984
985     /**
986      * Render contraints and filter conditions to an SQL query string buffer.
987      *
988      * @param q the query string
989      * @param home our home node
990      * @param nonvirtual our non-virtual home node
991      * @param prefix the prefix to use to append to the existing query (e.g. " AND ")
992      *
993      * @throws SQLException sql related exception
994      * @throws ClassNotFoundException driver class not found
995      */
996     public void renderConstraints(StringBuffer q, INode home, INode nonvirtual,
997                                   String prefix)
998                              throws SQLException, ClassNotFoundException {
999         renderConstraints(q, home, nonvirtual, otherType, prefix);
1000     }
1001
1002     /**
1003      * Render contraints and filter conditions to an SQL query string buffer.
1004      *
1005      * @param q the query string
1006      * @param home our home node
1007      * @param nonvirtual our non-virtual home nod
1008      * @param otherDbm the DbMapping of the remote Node
1009      * @param prefix the prefix to use to append to the existing query (e.g. " AND ")
1010      *
1011      * @throws SQLException sql related exception
1012      * @throws ClassNotFoundException driver class not found
1013      */
1014     public void renderConstraints(StringBuffer q, INode home, INode nonvirtual,
1015                                   DbMapping otherDbm, String prefix)
1016                              throws SQLException, ClassNotFoundException {
1017
1018         if (constraints.length > 1 && logicalOperator != AND) {
1019             q.append(prefix);
1020             q.append("(");
1021             prefix = "";
1022         }
1023
1024         for (int i = 0; i < constraints.length; i++) {
1025             if (constraints[i].foreignKeyIsPrototype()) {
1026                 // if foreign key is $prototype we already have this constraint
1027                 // covered by doing the select on the proper table
1028                 continue;
1029             }
1030             q.append(prefix);
1031             constraints[i].addToQuery(q, home, nonvirtual, otherDbm);
1032             prefix = logicalOperator;
1033         }
1034
1035         if (constraints.length > 1 && logicalOperator != AND) {
1036             q.append(")");
1037             prefix = " AND ";
1038         }
1039
1040         // also take the prototype into consideration if someone
1041         // specifies an extension of an prototype inside the brakets of
1042         // a type.properties's collection, only nodes having this proto
1043         // sould appear inside the collection
1044         if (otherDbm.inheritsStorage()) {
1045             String protoField = otherDbm.getPrototypeField();
1046             String[] extensions = otherDbm.getExtensions();
1047
1048             // extensions should never be null for extension- and
1049             // extended prototypes. nevertheless we check it here
1050             if (extensions != null && protoField != null) {
1051                 q.append(prefix);
1052                 otherDbm.appendCondition(q, protoField, extensions);
1053                 prefix = " AND ";
1054             }
1055         }
1056
1057         if (filter != null) {
1058             appendFilter(q, nonvirtual, prefix);
1059         }
1060     }
1061
1062     /**
1063      *  Render the constraints for this relation for use within
1064      *  a left outer join select statement for the base object.
1065      *
1066      * @param select the string buffer to write to
1067      * @param isOracle create Oracle pre-9 style left outer join
1068      */
1069     public void renderJoinConstraints(StringBuffer select, boolean isOracle) {
1070         for (int i = 0; i < constraints.length; i++) {
1071             select.append(ownType.getTableName());
1072             select.append(".");
1073             select.append(constraints[i].localKey);
1074             select.append(" = ");
1075             select.append(JOIN_PREFIX);
1076             select.append(propName);
1077             select.append(".");
1078             select.append(constraints[i].foreignKey);
1079             if (isOracle) {
1080                 // create old oracle style join - see
1081                 // http://www.praetoriate.com/oracle_tips_outer_joins.htm
1082                 select.append("(+)");
1083             }
1084             if (i == constraints.length-1) {
1085                 select.append(" ");
1086             } else {
1087                 select.append(" AND ");
1088             }
1089         }
1090
1091     }
1092
1093     /**
1094      * Get the order section to use for this relation
1095      */
1096     public String getOrder() {
1097         if (groupby != null) {
1098             return groupbyOrder;
1099         } else {
1100             return order;
1101         }
1102     }
1103
1104     /**
1105      *  Tell wether the property described by this relation is to be handled
1106      *  as readonly/write protected.
1107      */
1108     public boolean isReadonly() {
1109         return readonly;
1110     }
1111
1112     /**
1113      * Check if the child node fullfills the constraints defined by this relation.
1114      * FIXME: This always returns false if the relation has a filter value set,
1115      * since we can't determine if the filter constraints are met without
1116      * querying the database.
1117      *
1118      * @param parent the parent object - may be a virtual or group node
1119      * @param child the child object
1120      * @return true if all constraints are met
1121      */
1122     public boolean checkConstraints(Node parent, Node child) {
1123         // problem: if a filter property is defined for this relation,
1124         // i.e. a piece of static SQL-where clause, we'd have to evaluate it
1125         // in order to check the constraints. Because of this, if a filter
1126         // is defined, we return false as soon as the modified-time is greater
1127         // than the create-time of the child, i.e. if the child node has been
1128         // modified since it was first fetched from the db.
1129         if (filter != null && child.lastModified() > child.created()) {
1130             return false;
1131         }
1132
1133         // counter for constraints and satisfied constraints
1134         int count = 0;
1135         int satisfied = 0;
1136
1137         INode nonvirtual = parent.getNonVirtualParent();
1138         DbMapping otherDbm = child.getDbMapping();
1139         if (otherDbm == null) {
1140             otherDbm = otherType;
1141         }
1142
1143         for (int i = 0; i < constraints.length; i++) {
1144             Constraint cnst = constraints[i];
1145             String propname = cnst.foreignProperty(otherDbm);
1146
1147             if (propname != null) {
1148                 INode home = cnst.isGroupby ? parent
1149                                             : nonvirtual;
1150                 String value = null;
1151
1152                 if (cnst.localKeyIsPrimary(home.getDbMapping())) {
1153                     value = home.getID();
1154                 } else if (cnst.localKeyIsPrototype()) {
1155                     value = home.getDbMapping().getStorageTypeName();
1156                 } else if (ownType.isRelational()) {
1157                     value = home.getString(cnst.localProperty());
1158                 } else {
1159                     value = home.getString(cnst.localKey);
1160                 }
1161
1162                 count++;
1163
1164                 if (value != null && value.equals(child.getString(propname))) {
1165                     satisfied++;
1166                 }
1167             }
1168         }
1169
1170         // check if enough constraints are met depending on logical operator
1171         if (logicalOperator == OR) {
1172             return satisfied > 0;
1173         } else if (logicalOperator == XOR) {
1174             return satisfied == 1;
1175         } else {
1176             return satisfied == count;
1177         }
1178     }
1179
1180     /**
1181      * Make sure that the child node fullfills the constraints defined by this relation by setting the
1182      * appropriate properties
1183      */
1184     public void setConstraints(Node parent, Node child) {
1185
1186         // if logical operator is OR or XOR we just return because we
1187         // wouldn't know what to do anyway
1188         if (logicalOperator != AND) {
1189             return;
1190         }
1191
1192         Node home = parent.getNonVirtualParent();
1193
1194         for (int i = 0; i < constraints.length; i++) {
1195             Constraint cnst = constraints[i];
1196             // don't set groupby constraints since we don't know if the
1197             // parent node is the base node or a group node
1198             if (cnst.isGroupby) {
1199                 continue;
1200             }
1201
1202             // check if we update the local or the other object, depending on
1203             // whether the primary key of either side is used.
1204             boolean foreignIsPrimary = cnst.foreignKeyIsPrimary();
1205             if (foreignIsPrimary || cnst.foreignKeyIsPrototype()) {
1206                 String localProp = cnst.localProperty();
1207                 if (localProp == null) {
1208                     throw new RuntimeException("Error: column " + cnst.localKey +
1209                        " must be mapped in order to be used as constraint in " +
1210                        Relation.this);
1211                 } else if (foreignIsPrimary && child.getState() == Node.TRANSIENT) {
1212                     throw new RuntimeException(propName + " set to transient object, " +
1213                        "can't derive persistent ID for " + localProp);
1214                 } else {
1215                     String value = foreignIsPrimary ?
1216                             child.getID() : child.getDbMapping().getStorageTypeName();
1217                     home.setString(localProp, value);
1218                 }
1219                 continue;
1220             }
1221
1222             DbMapping otherDbm = child.getDbMapping();
1223             if (otherDbm == null) {
1224                 otherDbm = otherType;
1225             }
1226
1227             Relation crel = otherDbm.columnNameToRelation(cnst.foreignKey);
1228
1229             if (crel != null) {
1230
1231                 if (cnst.localKeyIsPrimary(home.getDbMapping())) {
1232                     // only set node if property in child object is defined as reference.
1233                     if (crel.reftype == REFERENCE) {
1234                         INode currentValue = child.getNode(crel.propName);
1235
1236                         // we set the backwards reference iff the reference is currently unset, if
1237                         // is set to a transient object, or if the new target is not transient. This
1238                         // prevents us from overwriting a persistent refererence with a transient one,
1239                         // which would most probably not be what we want.
1240                         if ((currentValue == null) ||
1241                                 ((currentValue != home) &&
1242                                 ((currentValue.getState() == Node.TRANSIENT) ||
1243                                 (home.getState() != Node.TRANSIENT)))) try {
1244                             child.setNode(crel.propName, home);
1245                         } catch (Exception ignore) {
1246                             // in some cases, getNonVirtualParent() doesn't work
1247                             // correctly for transient nodes, so this may fail.
1248                         }
1249                     } else if (crel.reftype == PRIMITIVE) {
1250                         if (home.getState() == Node.TRANSIENT) {
1251                             throw new RuntimeException("Object is transient, can't derive persistent ID for " + crel);
1252                         }
1253                         child.setString(crel.propName, home.getID());
1254                     }
1255                 } else if (crel.reftype == PRIMITIVE) {
1256                     if (cnst.localKeyIsPrototype()) {
1257                         child.setString(crel.propName, home.getDbMapping().getStorageTypeName());
1258                     } else {
1259                         Property prop = home.getProperty(cnst.localProperty());
1260                         if (prop != null) {
1261                             child.set(crel.propName, prop.getValue(), prop.getType());
1262                         } else {
1263                             prop = child.getProperty(cnst.foreignProperty(child.getDbMapping()));
1264                             if (prop != null) {
1265                                 home.set(cnst.localProperty(), prop.getValue(), prop.getType());
1266                             }
1267                         }
1268                     }
1269                 }
1270             }
1271         }
1272     }
1273
1274     /**
1275      * Unset the constraints that link two objects together.
1276      */
1277     public void unsetConstraints(Node parent, INode child) {
1278         Node home = parent.getNonVirtualParent();
1279
1280         for (int i = 0; i < constraints.length; i++) {
1281             Constraint cnst = constraints[i];
1282             // don't set groupby constraints since we don't know if the
1283             // parent node is the base node or a group node
1284             if (cnst.isGroupby) {
1285                 continue;
1286             }
1287
1288             // check if we update the local or the other object, depending on
1289             // whether the primary key of either side is used.
1290
1291             if (cnst.foreignKeyIsPrimary() || cnst.foreignKeyIsPrototype()) {
1292                 String localProp = cnst.localProperty();
1293                 if (localProp != null) {
1294                     home.setString(localProp, null);
1295                 }
1296                 continue;
1297             }
1298
1299             DbMapping otherDbm = child.getDbMapping();
1300             if (otherDbm == null) {
1301                 otherDbm = otherType;
1302             }
1303
1304             Relation crel = otherDbm.columnNameToRelation(cnst.foreignKey);
1305
1306             if (crel != null) {
1307                 if (cnst.localKeyIsPrimary(home.getDbMapping())) {
1308                     // only set node if property in child object is defined as reference.
1309                     if (crel.reftype == REFERENCE) {
1310                         INode currentValue = child.getNode(crel.propName);
1311
1312                         if ((currentValue == home)) {
1313                             child.setString(crel.propName, null);
1314                         }
1315                     } else if (crel.reftype == PRIMITIVE) {
1316                         child.setString(crel.propName, null);
1317                     }
1318                 } else if (crel.reftype == PRIMITIVE) {
1319                     child.setString(crel.propName, null);
1320                 }
1321             }
1322         }
1323     }
1324
1325     /**
1326      *  Returns a map containing the key/value pairs for a specific Node
1327      */
1328     public Map getKeyParts(INode home) {
1329         Map map = new HashMap();
1330         for (int i=0; i<constraints.length; i++) {
1331             Constraint cnst = constraints[i];
1332             if (cnst.localKeyIsPrimary(ownType)) {
1333                 map.put(cnst.foreignKey, home.getID());
1334             } else if (cnst.localKeyIsPrototype()) {
1335                 map.put(cnst.foreignKey, home.getDbMapping().getStorageTypeName());
1336             } else {
1337                 map.put(cnst.foreignKey, home.getString(cnst.localProperty()));
1338             }
1339         }
1340         // add filter as pseudo-constraint
1341         if (filter != null) {
1342             map.put("__filter__", filter);
1343         }
1344         return map;
1345     }
1346
1347     /**
1348      *
1349      *
1350      * @return ...
1351      */
1352     public String toString() {
1353         String c = "";
1354         String spacer = "";
1355
1356         if (constraints != null) {
1357             c = " constraints: ";
1358             for (int i = 0; i < constraints.length; i++) {
1359                 c += spacer;
1360                 c += constraints[i].toString();
1361                 spacer = ", ";
1362             }
1363         }
1364
1365         String target = otherType == null ? columnName : otherType.toString();
1366
1367         return "Relation " + ownType+"."+propName + " -> " + target + c;
1368     }
1369
1370     /**
1371      * The Constraint class represents a part of the where clause in the query used to
1372      * establish a relation between database mapped objects.
1373      */
1374     class Constraint {
1375         String localKey;
1376         String foreignKey;
1377         boolean isGroupby;
1378
1379         Constraint(String local, String foreign, boolean groupby) {
1380             localKey = local;
1381             foreignKey = foreign;
1382             isGroupby = groupby;
1383         }
1384
1385         public void addToQuery(StringBuffer q, INode home, INode nonvirtual, DbMapping otherDbm)
1386                         throws SQLException, ClassNotFoundException {
1387             String local;
1388             INode ref = isGroupby ? home : nonvirtual;
1389
1390             if (localKeyIsPrimary(ref.getDbMapping())) {
1391                 local = ref.getID();
1392             } else if (localKeyIsPrototype()) {
1393                 local = ref.getDbMapping().getStorageTypeName();
1394             } else {
1395                 String homeprop = ownType.columnNameToProperty(localKey);
1396                 if (homeprop == null) {
1397                     throw new SQLException("Invalid local name '" + localKey +
1398                             "' on " + ownType);
1399                 }
1400                 local = ref.getString(homeprop);
1401             }
1402
1403             String columnName;
1404             if (foreignKeyIsPrimary()) {
1405                 columnName = otherDbm.getIDField();
1406             } else {
1407                 columnName = foreignKey;
1408             }
1409             otherDbm.appendCondition(q, columnName, local);
1410         }
1411
1412         public boolean foreignKeyIsPrimary() {
1413             return (foreignKey == null) ||
1414                     "$id".equalsIgnoreCase(foreignKey) ||
1415                    foreignKey.equalsIgnoreCase(otherType.getIDField());
1416         }
1417
1418         public boolean foreignKeyIsPrototype() {
1419             return "$prototype".equalsIgnoreCase(foreignKey);
1420         }
1421
1422         public boolean localKeyIsPrimary(DbMapping homeMapping) {
1423             return (homeMapping == null) || (localKey == null) ||
1424                    "$id".equalsIgnoreCase(localKey) ||
1425                    localKey.equalsIgnoreCase(homeMapping.getIDField());
1426         }
1427
1428         public boolean localKeyIsPrototype() {
1429             return "$prototype".equalsIgnoreCase(localKey);
1430         }
1431
1432         public String foreignProperty(DbMapping otherDbm) {
1433             if (otherDbm.isRelational())
1434                 return otherDbm.columnNameToProperty(foreignKey);
1435             return foreignKey;
1436         }
1437
1438         public String localProperty() {
1439             if (ownType.isRelational())
1440                 return ownType.columnNameToProperty(localKey);
1441             return localKey;
1442         }
1443
1444         public String toString() {
1445             return localKey + "=" + otherType.getTypeName() + "." + foreignKey;
1446         }
1447     }
1448 }
Note: See TracBrowser for help on using the browser.