0001: /*
0002: * Helma License Notice
0003: *
0004: * The contents of this file are subject to the Helma License
0005: * Version 2.0 (the "License"). You may not use this file except in
0006: * compliance with the License. A copy of the License is available at
0007: * http://adele.helma.org/download/helma/license.txt
0008: *
0009: * Copyright 1998-2003 Helma Software. All Rights Reserved.
0010: *
0011: * $RCSfile$
0012: * $Author: hannes $
0013: * $Revision: 8704 $
0014: * $Date: 2007-12-11 17:16:28 +0100 (Die, 11 Dez 2007) $
0015: */
0016:
0017: package helma.objectmodel.db;
0018:
0019: import helma.framework.core.Application;
0020: import helma.objectmodel.INode;
0021: import helma.objectmodel.IProperty;
0022: import helma.util.StringUtils;
0023: import helma.util.ResourceProperties;
0024:
0025: import java.sql.SQLException;
0026: import java.util.HashMap;
0027: import java.util.Map;
0028: import java.util.Properties;
0029: import java.util.Enumeration;
0030: import java.util.Vector;
0031:
0032: /**
0033: * This describes how a property of a persistent Object is stored in a
0034: * relational database table. This can be either a scalar property (string, date, number etc.)
0035: * or a reference to one or more other objects.
0036: */
0037: public final class Relation {
0038: // these constants define different type of property-to-db-mappings
0039: // there is an error in the description of this relation
0040: public final static int INVALID = -1;
0041:
0042: // a mapping of a non-object, scalar type
0043: public final static int PRIMITIVE = 0;
0044:
0045: // a 1-to-1 relation, i.e. a field in the table is a foreign key to another object
0046: public final static int REFERENCE = 1;
0047:
0048: // a 1-to-many relation, a field in another table points to objects of this type
0049: public final static int COLLECTION = 2;
0050:
0051: // a 1-to-1 reference with multiple or otherwise not-trivial constraints
0052: // this is managed differently than REFERENCE, hence the separate type.
0053: public final static int COMPLEX_REFERENCE = 3;
0054:
0055: // constraints linked together by OR or AND if applicable?
0056: public final static String AND = " AND ";
0057: public final static String OR = " OR ";
0058: public final static String XOR = " XOR ";
0059: private String logicalOperator = AND;
0060:
0061: // prefix to use for symbolic names of joined tables. The name is composed
0062: // from this prefix and the name of the property we're doing the join for
0063: final static String JOIN_PREFIX = "JOIN_";
0064:
0065: // direct mapping is a very powerful feature:
0066: // objects of some types can be directly accessed
0067: // by one of their properties/db fields.
0068: // public final static int DIRECT = 3;
0069: // the DbMapping of the type we come from
0070: DbMapping ownType;
0071:
0072: // the DbMapping of the prototype we link to, unless this is a "primitive" (non-object) relation
0073: DbMapping otherType;
0074:
0075: // the column type, as defined in java.sql.Types
0076: int columnType;
0077:
0078: // if this relation defines a virtual node, we need to provide a DbMapping for these virtual nodes
0079: DbMapping virtualMapping;
0080: String propName;
0081: String columnName;
0082: int reftype;
0083: Constraint[] constraints;
0084: boolean virtual;
0085: boolean readonly;
0086: boolean aggressiveLoading;
0087: boolean aggressiveCaching;
0088: boolean isPrivate = false;
0089: boolean referencesPrimaryKey = false;
0090: String updateCriteria;
0091: String accessName; // db column used to access objects through this relation
0092: String order;
0093: boolean autoSorted = false;
0094: String groupbyOrder;
0095: String groupby;
0096: String prototype;
0097: String groupbyPrototype;
0098: String filter;
0099: private String additionalTables;
0100: private boolean additionalTablesJoined = false;
0101: String queryHints;
0102: Vector filterFragments;
0103: Vector filterPropertyRefs;
0104: int maxSize = 0;
0105:
0106: /**
0107: * This constructor makes a copy of an existing relation. Not all fields are copied, just those
0108: * which are needed in groupby- and virtual nodes defined by this relation.
0109: */
0110: private Relation(Relation rel) {
0111: // Note: prototype, groupby, groupbyPrototype and groupbyOrder aren't copied here.
0112: // these are set by the individual get*Relation() methods as appropriate.
0113: this .ownType = rel.ownType;
0114: this .otherType = rel.otherType;
0115: this .propName = rel.propName;
0116: this .columnName = rel.columnName;
0117: this .reftype = rel.reftype;
0118: this .order = rel.order;
0119: this .filter = rel.filter;
0120: this .filterFragments = rel.filterFragments;
0121: this .filterPropertyRefs = rel.filterPropertyRefs;
0122: this .additionalTables = rel.additionalTables;
0123: this .additionalTablesJoined = rel.additionalTablesJoined;
0124: this .queryHints = rel.queryHints;
0125: this .maxSize = rel.maxSize;
0126: this .constraints = rel.constraints;
0127: this .accessName = rel.accessName;
0128: this .maxSize = rel.maxSize;
0129: this .logicalOperator = rel.logicalOperator;
0130: this .aggressiveLoading = rel.aggressiveLoading;
0131: this .aggressiveCaching = rel.aggressiveCaching;
0132: this .updateCriteria = rel.updateCriteria;
0133: this .autoSorted = rel.autoSorted;
0134: }
0135:
0136: /**
0137: * Reads a relation entry from a line in a properties file.
0138: */
0139: public Relation(String propName, DbMapping ownType) {
0140: this .ownType = ownType;
0141: this .propName = propName;
0142: otherType = null;
0143: }
0144:
0145: ////////////////////////////////////////////////////////////////////////////////////////////
0146: // parse methods for new file format
0147: ////////////////////////////////////////////////////////////////////////////////////////////
0148: public void update(String desc, ResourceProperties props) {
0149: Application app = ownType.getApplication();
0150:
0151: if ((desc == null) || "".equals(desc.trim())) {
0152: if (propName != null) {
0153: reftype = PRIMITIVE;
0154: columnName = propName;
0155: } else {
0156: reftype = INVALID;
0157: columnName = propName;
0158: }
0159: } else {
0160: desc = desc.trim();
0161:
0162: int open = desc.indexOf("(");
0163: int close = desc.indexOf(")");
0164:
0165: if ((open > -1) && (close > open)) {
0166: String ref = desc.substring(0, open).trim();
0167: String proto = desc.substring(open + 1, close).trim();
0168:
0169: if ("collection".equalsIgnoreCase(ref)) {
0170: virtual = !"_children".equalsIgnoreCase(propName);
0171: reftype = COLLECTION;
0172: } else if ("mountpoint".equalsIgnoreCase(ref)) {
0173: virtual = true;
0174: reftype = COLLECTION;
0175: prototype = proto;
0176: } else if ("object".equalsIgnoreCase(ref)) {
0177: virtual = false;
0178: if (reftype != COMPLEX_REFERENCE) {
0179: reftype = REFERENCE;
0180: }
0181: } else {
0182: throw new RuntimeException(
0183: "Invalid property Mapping: " + desc);
0184: }
0185:
0186: otherType = app.getDbMapping(proto);
0187:
0188: if (otherType == null) {
0189: throw new RuntimeException("DbMapping for " + proto
0190: + " not found from "
0191: + ownType.getTypeName());
0192: }
0193:
0194: // make sure the type we're referring to is up to date!
0195: if (otherType.needsUpdate()) {
0196: otherType.update();
0197: }
0198:
0199: } else {
0200: virtual = false;
0201: columnName = desc;
0202: reftype = PRIMITIVE;
0203: }
0204: }
0205:
0206: ResourceProperties config = props
0207: .getSubProperties(propName + '.');
0208:
0209: readonly = "true".equalsIgnoreCase(config
0210: .getProperty("readonly"));
0211:
0212: isPrivate = "true".equalsIgnoreCase(config
0213: .getProperty("private"));
0214:
0215: // the following options only apply to object and collection relations
0216: if ((reftype != PRIMITIVE) && (reftype != INVALID)) {
0217: Vector newConstraints = new Vector();
0218:
0219: parseOptions(newConstraints, config);
0220:
0221: constraints = new Constraint[newConstraints.size()];
0222: newConstraints.copyInto(constraints);
0223:
0224: if (reftype == REFERENCE || reftype == COMPLEX_REFERENCE) {
0225: if (constraints.length == 0) {
0226: referencesPrimaryKey = true;
0227: } else {
0228: boolean rprim = false;
0229: for (int i = 0; i < constraints.length; i++) {
0230: if (constraints[i].foreignKeyIsPrimary()) {
0231: rprim = true;
0232: break;
0233: }
0234: }
0235: referencesPrimaryKey = rprim;
0236: }
0237:
0238: // check if this is a non-trivial reference
0239: if (constraints.length > 1 || !usesPrimaryKey()) {
0240: reftype = COMPLEX_REFERENCE;
0241: } else {
0242: reftype = REFERENCE;
0243: }
0244: }
0245:
0246: if (reftype == COLLECTION) {
0247: referencesPrimaryKey = (accessName == null)
0248: || accessName.equalsIgnoreCase(otherType
0249: .getIDField());
0250: }
0251:
0252: // if DbMapping for virtual nodes has already been created,
0253: // update its subnode relation.
0254: // FIXME: needs to be synchronized?
0255: if (virtualMapping != null) {
0256: virtualMapping.lastTypeChange = ownType.lastTypeChange;
0257: virtualMapping.subRelation = getVirtualSubnodeRelation();
0258: virtualMapping.propRelation = getVirtualPropertyRelation();
0259: }
0260: } else {
0261: referencesPrimaryKey = false;
0262: }
0263: }
0264:
0265: protected void parseOptions(Vector cnst, Properties config) {
0266: String loading = config.getProperty("loadmode");
0267:
0268: aggressiveLoading = (loading != null)
0269: && "aggressive".equalsIgnoreCase(loading.trim());
0270:
0271: String caching = config.getProperty("cachemode");
0272:
0273: aggressiveCaching = (caching != null)
0274: && "aggressive".equalsIgnoreCase(caching.trim());
0275:
0276: // get order property
0277: order = config.getProperty("order");
0278:
0279: if ((order != null) && (order.trim().length() == 0)) {
0280: order = null;
0281: }
0282:
0283: // get the criteria(s) for updating this collection
0284: updateCriteria = config.getProperty("updatecriteria");
0285:
0286: // get the autosorting flag
0287: autoSorted = "auto".equalsIgnoreCase(config
0288: .getProperty("sortmode"));
0289:
0290: // get additional filter property
0291: filter = config.getProperty("filter");
0292:
0293: if (filter != null) {
0294: if (filter.trim().length() == 0) {
0295: filter = null;
0296: filterFragments = filterPropertyRefs = null;
0297: } else {
0298: // parenthesise filter
0299: Vector fragments = new Vector();
0300: Vector propertyRefs = new Vector();
0301: parsePropertyString(filter, fragments, propertyRefs);
0302: // if no references where found, just use the filter string
0303: // otherwise use the filter fragments and proeprty refs instead
0304: if (propertyRefs.size() > 0) {
0305: filterFragments = fragments;
0306: filterPropertyRefs = propertyRefs;
0307: } else {
0308: filterFragments = filterPropertyRefs = null;
0309: }
0310: }
0311: }
0312:
0313: // get additional tables
0314: additionalTables = config
0315: .getProperty("filter.additionalTables");
0316:
0317: if (additionalTables != null) {
0318: if (additionalTables.trim().length() == 0) {
0319: additionalTables = null;
0320: } else {
0321: String ucTables = additionalTables.toUpperCase();
0322: // create dependencies implied by additional tables
0323: DbSource dbsource = otherType.getDbSource();
0324: if (dbsource != null) {
0325: String[] tables = StringUtils.split(ucTables, ", ");
0326: for (int i = 0; i < tables.length; i++) {
0327: // Skip some join-related keyworks we might encounter here
0328: if ("AS".equals(tables[i])
0329: || "ON".equals(tables[i])) {
0330: continue;
0331: }
0332: DbMapping dbmap = dbsource
0333: .getDbMapping(tables[i]);
0334: if (dbmap != null) {
0335: dbmap.addDependency(otherType);
0336: }
0337: }
0338: }
0339: // see wether the JOIN syntax is used. look for " join " with whitespaces on both sides
0340: // and for "join " at the beginning:
0341: additionalTablesJoined = (ucTables.indexOf(" JOIN ") != -1
0342: || ucTables.startsWith("STRAIGHT_JOIN ") || ucTables
0343: .startsWith("JOIN "));
0344: }
0345: }
0346:
0347: // get query hints
0348: queryHints = config.getProperty("hints");
0349:
0350: // get max size of collection
0351: String max = config.getProperty("maxSize");
0352:
0353: if (max != null) {
0354: try {
0355: maxSize = Integer.parseInt(max);
0356: } catch (NumberFormatException nfe) {
0357: maxSize = 0;
0358: }
0359: } else {
0360: maxSize = 0;
0361: }
0362:
0363: // get group by property
0364: groupby = config.getProperty("group");
0365:
0366: if ((groupby != null) && (groupby.trim().length() == 0)) {
0367: groupby = null;
0368: }
0369:
0370: if (groupby != null) {
0371: groupbyOrder = config.getProperty("group.order");
0372:
0373: if ((groupbyOrder != null)
0374: && (groupbyOrder.trim().length() == 0)) {
0375: groupbyOrder = null;
0376: }
0377:
0378: groupbyPrototype = config.getProperty("group.prototype");
0379:
0380: if ((groupbyPrototype != null)
0381: && (groupbyPrototype.trim().length() == 0)) {
0382: groupbyPrototype = null;
0383: }
0384:
0385: // aggressive loading and caching is not supported for groupby-nodes
0386: // aggressiveLoading = aggressiveCaching = false;
0387: }
0388:
0389: // check if subnode condition should be applied for property relations
0390: accessName = config.getProperty("accessname");
0391:
0392: // parse contstraints
0393: String local = config.getProperty("local");
0394: String foreign = config.getProperty("foreign");
0395:
0396: if ((local != null) && (foreign != null)) {
0397: cnst.addElement(new Constraint(local, foreign, false));
0398: columnName = local;
0399: }
0400:
0401: // parse additional contstraints from *.1 to *.9
0402: for (int i = 1; i < 10; i++) {
0403: local = config.getProperty("local." + i);
0404: foreign = config.getProperty("foreign." + i);
0405:
0406: if ((local != null) && (foreign != null)) {
0407: cnst.addElement(new Constraint(local, foreign, false));
0408: }
0409: }
0410:
0411: // parse constraints logic
0412: if (cnst.size() > 1) {
0413: String logic = config.getProperty("logicalOperator");
0414: if ("and".equalsIgnoreCase(logic)) {
0415: logicalOperator = AND;
0416: } else if ("or".equalsIgnoreCase(logic)) {
0417: logicalOperator = OR;
0418: } else if ("xor".equalsIgnoreCase(logic)) {
0419: logicalOperator = XOR;
0420: } else {
0421: logicalOperator = AND;
0422: }
0423: } else {
0424: logicalOperator = AND;
0425: }
0426:
0427: }
0428:
0429: ///////////////////////////////////////////////////////////////////////////////////////////
0430:
0431: /**
0432: * Get the configuration properties for this relation.
0433: */
0434: public ResourceProperties getConfig() {
0435: return ownType.getProperties().getSubProperties(propName + '.');
0436: }
0437:
0438: /**
0439: * Does this relation describe a virtual (collection) node?
0440: */
0441: public boolean isVirtual() {
0442: return virtual;
0443: }
0444:
0445: /**
0446: * Return the target type of this relation, or null if this is a primitive mapping.
0447: */
0448: public DbMapping getTargetType() {
0449: return otherType;
0450: }
0451:
0452: /**
0453: * Get the reference type of this relation.
0454: */
0455: public int getRefType() {
0456: return reftype;
0457: }
0458:
0459: /**
0460: * Tell if this relation represents a primitive (scalar) value mapping.
0461: */
0462: public boolean isPrimitive() {
0463: return reftype == PRIMITIVE;
0464: }
0465:
0466: /**
0467: * Returns true if this Relation describes an object reference property
0468: */
0469: public boolean isReference() {
0470: return reftype == REFERENCE;
0471: }
0472:
0473: /**
0474: * Returns true if this Relation describes either a primitive value
0475: * or an object reference.
0476: */
0477: public boolean isPrimitiveOrReference() {
0478: return reftype == PRIMITIVE || reftype == REFERENCE;
0479: }
0480:
0481: /**
0482: * Returns true if this Relation describes a collection.
0483: * <b>NOTE:</b> this will return true both for collection objects
0484: * (aka virtual nodes) and direct child object relations, so
0485: * isVirtual() should be used to identify relations that define
0486: * <i>collection properties</i>!
0487: */
0488: public boolean isCollection() {
0489: return reftype == COLLECTION;
0490: }
0491:
0492: /**
0493: * Returns true if this Relation describes a complex object reference property
0494: */
0495: public boolean isComplexReference() {
0496: return reftype == COMPLEX_REFERENCE;
0497: }
0498:
0499: /**
0500: * Tell wether the property described by this relation is to be handled as private, i.e.
0501: * a change on it should not result in any changed object/collection relations.
0502: */
0503: public boolean isPrivate() {
0504: return isPrivate;
0505: }
0506:
0507: /**
0508: * Check whether aggressive loading is set for this relation
0509: */
0510: public boolean loadAggressively() {
0511: return aggressiveLoading;
0512: }
0513:
0514: /**
0515: * Returns the number of constraints for this relation.
0516: */
0517: public int countConstraints() {
0518: if (constraints == null)
0519: return 0;
0520: return constraints.length;
0521: }
0522:
0523: /**
0524: * Returns true if the object represented by this Relation has to be
0525: * created on demand at runtime by the NodeManager. This is true for:
0526: *
0527: * - collection (aka virtual) nodes
0528: * - nodes accessed via accessname
0529: * - group nodes
0530: * - complex reference nodes
0531: */
0532: public boolean createOnDemand() {
0533: if (otherType == null) {
0534: return false;
0535: }
0536:
0537: return virtual
0538: || (otherType.isRelational() && accessName != null)
0539: || (groupby != null) || isComplexReference();
0540: }
0541:
0542: /**
0543: * Returns true if the object represented by this Relation has to be
0544: * persisted in the internal db in order to be functional. This is true if
0545: * the subnodes contained in this collection are stored in the embedded
0546: * database. In this case, the collection itself must also be an ordinary
0547: * object stored in the db, since a virtual collection would lose its
0548: * its content after restarts.
0549: */
0550: public boolean needsPersistence() {
0551: if (!virtual) {
0552: // ordinary object references always need to be persisted
0553: return true;
0554: }
0555:
0556: // collections/mountpoints need to be persisted if the
0557: // child object type is non-relational.
0558: if (prototype == null) {
0559: return !otherType.isRelational();
0560: }
0561:
0562: DbMapping sub = otherType.getSubnodeMapping();
0563:
0564: return (sub != null) && !sub.isRelational();
0565: }
0566:
0567: /**
0568: * Return the prototype to be used for object reached by this relation
0569: */
0570: public String getPrototype() {
0571: return prototype;
0572: }
0573:
0574: /**
0575: * Return the name of the local property this relation is defined for
0576: */
0577: public String getPropName() {
0578: return propName;
0579: }
0580:
0581: /**
0582: *
0583: *
0584: * @param ct ...
0585: */
0586: public void setColumnType(int ct) {
0587: columnType = ct;
0588: }
0589:
0590: /**
0591: *
0592: *
0593: * @return ...
0594: */
0595: public int getColumnType() {
0596: return columnType;
0597: }
0598:
0599: /**
0600: * Get the group for a collection relation, if defined.
0601: *
0602: * @return the name of the column used to group child objects, if any.
0603: */
0604: public String getGroup() {
0605: return groupby;
0606: }
0607:
0608: /**
0609: * Add a constraint to the current list of constraints
0610: */
0611: protected void addConstraint(Constraint c) {
0612: if (constraints == null) {
0613: constraints = new Constraint[1];
0614: constraints[0] = c;
0615: } else {
0616: Constraint[] nc = new Constraint[constraints.length + 1];
0617:
0618: System.arraycopy(constraints, 0, nc, 0, constraints.length);
0619: nc[nc.length - 1] = c;
0620: constraints = nc;
0621: }
0622: }
0623:
0624: /**
0625: *
0626: *
0627: * @return true if the foreign key used for this relation is the
0628: * other object's primary key.
0629: */
0630: public boolean usesPrimaryKey() {
0631: return referencesPrimaryKey;
0632: }
0633:
0634: /**
0635: *
0636: *
0637: * @return ...
0638: */
0639: public boolean hasAccessName() {
0640: return accessName != null;
0641: }
0642:
0643: /**
0644: *
0645: *
0646: * @return ...
0647: */
0648: public String getAccessName() {
0649: return accessName;
0650: }
0651:
0652: /**
0653: *
0654: *
0655: * @return ...
0656: */
0657: public Relation getSubnodeRelation() {
0658: // return subnoderelation;
0659: return null;
0660: }
0661:
0662: /**
0663: * Return the local field name for updates.
0664: */
0665: public String getDbField() {
0666: return columnName;
0667: }
0668:
0669: /**
0670: * This is taken from org.apache.tools.ant ProjectHelper.java
0671: * distributed under the Apache Software License, Version 1.1
0672: *
0673: * Parses a string containing <code>${xxx}</code> style property
0674: * references into two lists. The first list is a collection
0675: * of text fragments, while the other is a set of string property names.
0676: * <code>null</code> entries in the first list indicate a property
0677: * reference from the second list.
0678: *
0679: * @param value Text to parse. Must not be <code>null</code>.
0680: * @param fragments List to add text fragments to.
0681: * Must not be <code>null</code>.
0682: * @param propertyRefs List to add property names to.
0683: * Must not be <code>null</code>.
0684: */
0685: protected void parsePropertyString(String value, Vector fragments,
0686: Vector propertyRefs) {
0687: int prev = 0;
0688: int pos;
0689: //search for the next instance of $ from the 'prev' position
0690: while ((pos = value.indexOf("$", prev)) >= 0) {
0691:
0692: //if there was any text before this, add it as a fragment
0693: //TODO, this check could be modified to go if pos>prev;
0694: //seems like this current version could stick empty strings
0695: //into the list
0696: if (pos > 0) {
0697: fragments.addElement(value.substring(prev, pos));
0698: }
0699: //if we are at the end of the string, we tack on a $
0700: //then move past it
0701: if (pos == (value.length() - 1)) {
0702: fragments.addElement("$");
0703: prev = pos + 1;
0704: } else if (value.charAt(pos + 1) != '{') {
0705: //peek ahead to see if the next char is a property or not
0706: //not a property: insert the char as a literal
0707: /*
0708: fragments.addElement(value.substring(pos + 1, pos + 2));
0709: prev = pos + 2;
0710: */
0711: if (value.charAt(pos + 1) == '$') {
0712: //backwards compatibility two $ map to one mode
0713: fragments.addElement("$");
0714: prev = pos + 2;
0715: } else {
0716: //new behaviour: $X maps to $X for all values of X!='$'
0717: fragments.addElement(value.substring(pos, pos + 2));
0718: prev = pos + 2;
0719: }
0720:
0721: } else {
0722: //property found, extract its name or bail on a typo
0723: int endName = value.indexOf('}', pos);
0724: if (endName < 0) {
0725: throw new RuntimeException(
0726: "Syntax error in property: " + value);
0727: }
0728: String propertyName = value.substring(pos + 2, endName);
0729: fragments.addElement(null);
0730: propertyRefs.addElement(propertyName);
0731: prev = endName + 1;
0732: }
0733: }
0734: //no more $ signs found
0735: //if there is any tail to the file, append it
0736: if (prev < value.length()) {
0737: fragments.addElement(value.substring(prev));
0738: }
0739: }
0740:
0741: /**
0742: * get a DbMapping to use for virtual aka collection nodes.
0743: */
0744: public DbMapping getVirtualMapping() {
0745: // return null unless this relation describes a virtual/collection node.
0746: if (!virtual) {
0747: return null;
0748: }
0749:
0750: // create a synthetic DbMapping that describes how to fetch the
0751: // collection's child objects.
0752: if (virtualMapping == null) {
0753: // if the collection node is prototyped (a mountpoint), create
0754: // a virtual sub-mapping from the app's DbMapping for that prototype
0755: if (prototype != null) {
0756: virtualMapping = new DbMapping(ownType.app, prototype);
0757: } else {
0758: virtualMapping = new DbMapping(ownType.app, null);
0759: virtualMapping.subRelation = getVirtualSubnodeRelation();
0760: virtualMapping.propRelation = getVirtualPropertyRelation();
0761: }
0762: }
0763:
0764: return virtualMapping;
0765: }
0766:
0767: /**
0768: * Return the db mapping for a propery relation.
0769: * @return the target mapping of this property relation
0770: */
0771: public DbMapping getPropertyMapping() {
0772: // if this is an untyped virtual node, it doesn't have a dbmapping
0773: if (!virtual || prototype != null) {
0774: return otherType;
0775: }
0776: return null;
0777: }
0778:
0779: /**
0780: * Return a Relation that defines the subnodes of a virtual node.
0781: */
0782: Relation getVirtualSubnodeRelation() {
0783: if (!virtual) {
0784: throw new RuntimeException(
0785: "getVirtualSubnodeRelation called on non-virtual relation");
0786: }
0787:
0788: Relation vr = new Relation(this );
0789:
0790: vr.groupby = groupby;
0791: vr.groupbyOrder = groupbyOrder;
0792: vr.groupbyPrototype = groupbyPrototype;
0793:
0794: return vr;
0795: }
0796:
0797: /**
0798: * Return a Relation that defines the properties of a virtual node.
0799: */
0800: Relation getVirtualPropertyRelation() {
0801: if (!virtual) {
0802: throw new RuntimeException(
0803: "getVirtualPropertyRelation called on non-virtual relation");
0804: }
0805:
0806: Relation vr = new Relation(this );
0807:
0808: vr.groupby = groupby;
0809: vr.groupbyOrder = groupbyOrder;
0810: vr.groupbyPrototype = groupbyPrototype;
0811:
0812: return vr;
0813: }
0814:
0815: /**
0816: * Return a Relation that defines the subnodes of a group-by node.
0817: */
0818: Relation getGroupbySubnodeRelation() {
0819: if (groupby == null) {
0820: throw new RuntimeException(
0821: "getGroupbySubnodeRelation called on non-group-by relation");
0822: }
0823:
0824: Relation vr = new Relation(this );
0825:
0826: vr.prototype = groupbyPrototype;
0827: vr.addConstraint(new Constraint(null, groupby, true));
0828:
0829: return vr;
0830: }
0831:
0832: /**
0833: * Return a Relation that defines the properties of a group-by node.
0834: */
0835: Relation getGroupbyPropertyRelation() {
0836: if (groupby == null) {
0837: throw new RuntimeException(
0838: "getGroupbyPropertyRelation called on non-group-by relation");
0839: }
0840:
0841: Relation vr = new Relation(this );
0842:
0843: vr.prototype = groupbyPrototype;
0844: vr.addConstraint(new Constraint(null, groupby, true));
0845:
0846: return vr;
0847: }
0848:
0849: /**
0850: * Build the second half of an SQL select statement according to this relation
0851: * and a local object.
0852: */
0853: public String buildQuery(INode home, INode nonvirtual, String kstr,
0854: String pre, boolean useOrder) throws SQLException,
0855: ClassNotFoundException {
0856: return buildQuery(home, nonvirtual, otherType, kstr, pre,
0857: useOrder);
0858: }
0859:
0860: /**
0861: * Build the second half of an SQL select statement according to this relation
0862: * and a local object.
0863: */
0864: public String buildQuery(INode home, INode nonvirtual,
0865: DbMapping otherDbm, String kstr, String pre,
0866: boolean useOrder) throws SQLException,
0867: ClassNotFoundException {
0868: StringBuffer q = new StringBuffer();
0869: String prefix = pre;
0870:
0871: if (kstr != null && !isComplexReference()) {
0872: q.append(prefix);
0873:
0874: String accessColumn = (accessName == null) ? otherDbm
0875: .getIDField() : accessName;
0876: otherDbm.appendCondition(q, accessColumn, kstr);
0877:
0878: prefix = " AND ";
0879: }
0880:
0881: // render the constraints and filter
0882: renderConstraints(q, home, nonvirtual, otherDbm, prefix);
0883:
0884: // add joined fetch constraints
0885: ownType.addJoinConstraints(q, prefix);
0886:
0887: // add group and order clauses
0888: if (groupby != null) {
0889: q.append(" GROUP BY ").append(groupby);
0890:
0891: if (useOrder && (groupbyOrder != null)) {
0892: q.append(" ORDER BY ").append(groupbyOrder);
0893: }
0894: } else if (useOrder && (order != null)) {
0895: q.append(" ORDER BY ").append(order);
0896: }
0897:
0898: return q.toString();
0899: }
0900:
0901: protected void appendAdditionalTables(StringBuffer q) {
0902: if (additionalTables != null) {
0903: q.append(additionalTablesJoined ? ' ' : ',');
0904: q.append(additionalTables);
0905: }
0906: }
0907:
0908: /**
0909: * Build the filter.
0910: */
0911: protected void appendFilter(StringBuffer q, INode nonvirtual,
0912: String prefix) {
0913: q.append(prefix);
0914: q.append('(');
0915: if (filterFragments == null) {
0916: q.append(filter);
0917: } else {
0918: Enumeration i = filterFragments.elements();
0919: Enumeration j = filterPropertyRefs.elements();
0920: while (i.hasMoreElements()) {
0921: String fragment = (String) i.nextElement();
0922: if (fragment == null) {
0923: // begin column version
0924: String columnName = (String) j.nextElement();
0925: Object value = null;
0926: if (columnName != null) {
0927: DbMapping dbmap = nonvirtual.getDbMapping();
0928: String propertyName = dbmap
0929: .columnNameToProperty(columnName);
0930: if (propertyName == null)
0931: propertyName = columnName;
0932: IProperty property = nonvirtual
0933: .get(propertyName);
0934: if (property != null) {
0935: value = property.getStringValue();
0936: }
0937: if (value == null) {
0938: if (columnName.equalsIgnoreCase(dbmap
0939: .getIDField())) {
0940: value = nonvirtual.getID();
0941: } else if (columnName
0942: .equalsIgnoreCase(dbmap
0943: .getNameField())) {
0944: value = nonvirtual.getName();
0945: } else if (columnName
0946: .equalsIgnoreCase(dbmap
0947: .getPrototypeField())) {
0948: value = dbmap.getExtensionId();
0949: }
0950: }
0951: }
0952: // end column version
0953: if (value != null) {
0954: q.append(DbMapping.escapeString(value
0955: .toString()));
0956: } else {
0957: q.append("NULL");
0958: }
0959: } else {
0960: q.append(fragment);
0961: }
0962: }
0963: }
0964: q.append(')');
0965: }
0966:
0967: /**
0968: * Render contraints and filter conditions to an SQL query string buffer.
0969: *
0970: * @param q the query string
0971: * @param home our home node
0972: * @param nonvirtual our non-virtual home node
0973: * @param prefix the prefix to use to append to the existing query (e.g. " AND ")
0974: *
0975: * @throws SQLException sql related exception
0976: * @throws ClassNotFoundException driver class not found
0977: */
0978: public void renderConstraints(StringBuffer q, INode home,
0979: INode nonvirtual, String prefix) throws SQLException,
0980: ClassNotFoundException {
0981: renderConstraints(q, home, nonvirtual, otherType, prefix);
0982: }
0983:
0984: /**
0985: * Render contraints and filter conditions to an SQL query string buffer.
0986: *
0987: * @param q the query string
0988: * @param home our home node
0989: * @param nonvirtual our non-virtual home nod
0990: * @param otherDbm the DbMapping of the remote Node
0991: * @param prefix the prefix to use to append to the existing query (e.g. " AND ")
0992: *
0993: * @throws SQLException sql related exception
0994: * @throws ClassNotFoundException driver class not found
0995: */
0996: public void renderConstraints(StringBuffer q, INode home,
0997: INode nonvirtual, DbMapping otherDbm, String prefix)
0998: throws SQLException, ClassNotFoundException {
0999:
1000: if (constraints.length > 1 && logicalOperator != AND) {
1001: q.append(prefix);
1002: q.append("(");
1003: prefix = "";
1004: }
1005:
1006: for (int i = 0; i < constraints.length; i++) {
1007: if (constraints[i].foreignKeyIsPrototype()) {
1008: // if foreign key is $prototype we already have this constraint
1009: // covered by doing the select on the proper table
1010: continue;
1011: }
1012: q.append(prefix);
1013: constraints[i].addToQuery(q, home, nonvirtual, otherDbm);
1014: prefix = logicalOperator;
1015: }
1016:
1017: if (constraints.length > 1 && logicalOperator != AND) {
1018: q.append(")");
1019: prefix = " AND ";
1020: }
1021:
1022: // also take the prototype into consideration if someone
1023: // specifies an extension of an prototype inside the brakets of
1024: // a type.properties's collection, only nodes having this proto
1025: // sould appear inside the collection
1026: if (otherDbm.inheritsStorage()) {
1027: String protoField = otherDbm.getPrototypeField();
1028: String[] extensions = otherDbm.getExtensions();
1029:
1030: // extensions should never be null for extension- and
1031: // extended prototypes. nevertheless we check it here
1032: if (extensions != null && protoField != null) {
1033: q.append(prefix);
1034: otherDbm.appendCondition(q, protoField, extensions);
1035: prefix = " AND ";
1036: }
1037: }
1038:
1039: if (filter != null) {
1040: appendFilter(q, nonvirtual, prefix);
1041: }
1042: }
1043:
1044: /**
1045: * Render the constraints for this relation for use within
1046: * a left outer join select statement for the base object.
1047: *
1048: * @param select the string buffer to write to
1049: * @param isOracle create Oracle pre-9 style left outer join
1050: */
1051: public void renderJoinConstraints(StringBuffer select,
1052: boolean isOracle) {
1053: for (int i = 0; i < constraints.length; i++) {
1054: select.append(ownType.getTableName());
1055: select.append(".");
1056: select.append(constraints[i].localKey);
1057: select.append(" = ");
1058: select.append(JOIN_PREFIX);
1059: select.append(propName);
1060: select.append(".");
1061: select.append(constraints[i].foreignKey);
1062: if (isOracle) {
1063: // create old oracle style join - see
1064: // http://www.praetoriate.com/oracle_tips_outer_joins.htm
1065: select.append("(+)");
1066: }
1067: if (i == constraints.length - 1) {
1068: select.append(" ");
1069: } else {
1070: select.append(" AND ");
1071: }
1072: }
1073:
1074: }
1075:
1076: /**
1077: * Get the order section to use for this relation
1078: */
1079: public String getOrder() {
1080: if (groupby != null) {
1081: return groupbyOrder;
1082: } else {
1083: return order;
1084: }
1085: }
1086:
1087: /**
1088: * Tell wether the property described by this relation is to be handled
1089: * as readonly/write protected.
1090: */
1091: public boolean isReadonly() {
1092: return readonly;
1093: }
1094:
1095: /**
1096: * Check if the child node fullfills the constraints defined by this relation.
1097: * FIXME: This always returns false if the relation has a filter value set,
1098: * since we can't determine if the filter constraints are met without
1099: * querying the database.
1100: *
1101: * @param parent the parent object - may be a virtual or group node
1102: * @param child the child object
1103: * @return true if all constraints are met
1104: */
1105: public boolean checkConstraints(Node parent, Node child) {
1106: // problem: if a filter property is defined for this relation,
1107: // i.e. a piece of static SQL-where clause, we'd have to evaluate it
1108: // in order to check the constraints. Because of this, if a filter
1109: // is defined, we return false as soon as the modified-time is greater
1110: // than the create-time of the child, i.e. if the child node has been
1111: // modified since it was first fetched from the db.
1112: if (filter != null && child.lastModified() > child.created()) {
1113: return false;
1114: }
1115:
1116: // counter for constraints and satisfied constraints
1117: int count = 0;
1118: int satisfied = 0;
1119:
1120: INode nonvirtual = parent.getNonVirtualParent();
1121: DbMapping otherDbm = child.getDbMapping();
1122: if (otherDbm == null) {
1123: otherDbm = otherType;
1124: }
1125:
1126: for (int i = 0; i < constraints.length; i++) {
1127: Constraint cnst = constraints[i];
1128: String propname = cnst.foreignProperty(otherDbm);
1129:
1130: if (propname != null) {
1131: INode home = cnst.isGroupby ? parent : nonvirtual;
1132: String value = null;
1133:
1134: if (cnst.localKeyIsPrimary(home.getDbMapping())) {
1135: value = home.getID();
1136: } else if (cnst.localKeyIsPrototype()) {
1137: value = home.getDbMapping().getStorageTypeName();
1138: } else if (ownType.isRelational()) {
1139: value = home.getString(cnst.localProperty());
1140: } else {
1141: value = home.getString(cnst.localKey);
1142: }
1143:
1144: count++;
1145:
1146: if (value != null
1147: && value.equals(child.getString(propname))) {
1148: satisfied++;
1149: }
1150: }
1151: }
1152:
1153: // check if enough constraints are met depending on logical operator
1154: if (logicalOperator == OR) {
1155: return satisfied > 0;
1156: } else if (logicalOperator == XOR) {
1157: return satisfied == 1;
1158: } else {
1159: return satisfied == count;
1160: }
1161: }
1162:
1163: /**
1164: * Make sure that the child node fullfills the constraints defined by this relation by setting the
1165: * appropriate properties
1166: */
1167: public void setConstraints(Node parent, Node child) {
1168:
1169: // if logical operator is OR or XOR we just return because we
1170: // wouldn't know what to do anyway
1171: if (logicalOperator != AND) {
1172: return;
1173: }
1174:
1175: Node home = parent.getNonVirtualParent();
1176:
1177: for (int i = 0; i < constraints.length; i++) {
1178: Constraint cnst = constraints[i];
1179: // don't set groupby constraints since we don't know if the
1180: // parent node is the base node or a group node
1181: if (cnst.isGroupby) {
1182: continue;
1183: }
1184:
1185: // check if we update the local or the other object, depending on
1186: // whether the primary key of either side is used.
1187: boolean foreignIsPrimary = cnst.foreignKeyIsPrimary();
1188: if (foreignIsPrimary || cnst.foreignKeyIsPrototype()) {
1189: String localProp = cnst.localProperty();
1190: if (localProp == null) {
1191: ownType.app
1192: .logError("Error: column "
1193: + cnst.localKey
1194: + " must be mapped in order to be used as constraint in "
1195: + Relation.this );
1196: } else {
1197: String value = foreignIsPrimary ? child.getID()
1198: : child.getDbMapping().getStorageTypeName();
1199: home.setString(localProp, value);
1200: }
1201: continue;
1202: }
1203:
1204: DbMapping otherDbm = child.getDbMapping();
1205: if (otherDbm == null) {
1206: otherDbm = otherType;
1207: }
1208:
1209: Relation crel = otherDbm
1210: .columnNameToRelation(cnst.foreignKey);
1211:
1212: if (crel != null) {
1213:
1214: if (cnst.localKeyIsPrimary(home.getDbMapping())) {
1215: // only set node if property in child object is defined as reference.
1216: if (crel.reftype == REFERENCE) {
1217: INode currentValue = child
1218: .getNode(crel.propName);
1219:
1220: // we set the backwards reference iff the reference is currently unset, if
1221: // is set to a transient object, or if the new target is not transient. This
1222: // prevents us from overwriting a persistent refererence with a transient one,
1223: // which would most probably not be what we want.
1224: if ((currentValue == null)
1225: || ((currentValue != home) && ((currentValue
1226: .getState() == Node.TRANSIENT) || (home
1227: .getState() != Node.TRANSIENT))))
1228: try {
1229: child.setNode(crel.propName, home);
1230: } catch (Exception ignore) {
1231: // in some cases, getNonVirtualParent() doesn't work
1232: // correctly for transient nodes, so this may fail.
1233: }
1234: } else if (crel.reftype == PRIMITIVE) {
1235: child.setString(crel.propName, home.getID());
1236: }
1237: } else if (crel.reftype == PRIMITIVE) {
1238: if (cnst.localKeyIsPrototype()) {
1239: child.setString(crel.propName, home
1240: .getDbMapping().getStorageTypeName());
1241: } else {
1242: Property prop = home.getProperty(cnst
1243: .localProperty());
1244: if (prop != null) {
1245: child.set(crel.propName, prop.getValue(),
1246: prop.getType());
1247: } else {
1248: prop = child.getProperty(cnst
1249: .foreignProperty(child
1250: .getDbMapping()));
1251: if (prop != null) {
1252: home.set(cnst.localProperty(), prop
1253: .getValue(), prop.getType());
1254: }
1255: }
1256: }
1257: }
1258: }
1259: }
1260: }
1261:
1262: /**
1263: * Unset the constraints that link two objects together.
1264: */
1265: public void unsetConstraints(Node parent, INode child) {
1266: Node home = parent.getNonVirtualParent();
1267:
1268: for (int i = 0; i < constraints.length; i++) {
1269: Constraint cnst = constraints[i];
1270: // don't set groupby constraints since we don't know if the
1271: // parent node is the base node or a group node
1272: if (cnst.isGroupby) {
1273: continue;
1274: }
1275:
1276: // check if we update the local or the other object, depending on
1277: // whether the primary key of either side is used.
1278:
1279: if (cnst.foreignKeyIsPrimary()
1280: || cnst.foreignKeyIsPrototype()) {
1281: String localProp = cnst.localProperty();
1282: if (localProp != null) {
1283: home.setString(localProp, null);
1284: }
1285: continue;
1286: }
1287:
1288: DbMapping otherDbm = child.getDbMapping();
1289: if (otherDbm == null) {
1290: otherDbm = otherType;
1291: }
1292:
1293: Relation crel = otherDbm
1294: .columnNameToRelation(cnst.foreignKey);
1295:
1296: if (crel != null) {
1297: if (cnst.localKeyIsPrimary(home.getDbMapping())) {
1298: // only set node if property in child object is defined as reference.
1299: if (crel.reftype == REFERENCE) {
1300: INode currentValue = child
1301: .getNode(crel.propName);
1302:
1303: if ((currentValue == home)) {
1304: child.setString(crel.propName, null);
1305: }
1306: } else if (crel.reftype == PRIMITIVE) {
1307: child.setString(crel.propName, null);
1308: }
1309: } else if (crel.reftype == PRIMITIVE) {
1310: child.setString(crel.propName, null);
1311: }
1312: }
1313: }
1314: }
1315:
1316: /**
1317: * Returns a map containing the key/value pairs for a specific Node
1318: */
1319: public Map getKeyParts(INode home) {
1320: Map map = new HashMap();
1321: for (int i = 0; i < constraints.length; i++) {
1322: Constraint cnst = constraints[i];
1323: if (cnst.localKeyIsPrimary(ownType)) {
1324: map.put(cnst.foreignKey, home.getID());
1325: } else if (cnst.localKeyIsPrototype()) {
1326: map.put(cnst.foreignKey, home.getDbMapping()
1327: .getStorageTypeName());
1328: } else {
1329: map.put(cnst.foreignKey, home.getString(cnst
1330: .localProperty()));
1331: }
1332: }
1333: // add filter as pseudo-constraint
1334: if (filter != null) {
1335: map.put("__filter__", filter);
1336: }
1337: return map;
1338: }
1339:
1340: /**
1341: *
1342: *
1343: * @return ...
1344: */
1345: public String toString() {
1346: String c = "";
1347: String spacer = "";
1348:
1349: if (constraints != null) {
1350: c = " constraints: ";
1351: for (int i = 0; i < constraints.length; i++) {
1352: c += spacer;
1353: c += constraints[i].toString();
1354: spacer = ", ";
1355: }
1356: }
1357:
1358: String target = otherType == null ? columnName : otherType
1359: .toString();
1360:
1361: return "Relation " + ownType + "." + propName + " -> " + target
1362: + c;
1363: }
1364:
1365: /**
1366: * The Constraint class represents a part of the where clause in the query used to
1367: * establish a relation between database mapped objects.
1368: */
1369: class Constraint {
1370: String localKey;
1371: String foreignKey;
1372: boolean isGroupby;
1373:
1374: Constraint(String local, String foreign, boolean groupby) {
1375: localKey = local;
1376: foreignKey = foreign;
1377: isGroupby = groupby;
1378: }
1379:
1380: public void addToQuery(StringBuffer q, INode home,
1381: INode nonvirtual, DbMapping otherDbm)
1382: throws SQLException, ClassNotFoundException {
1383: String local;
1384: INode ref = isGroupby ? home : nonvirtual;
1385:
1386: if (localKeyIsPrimary(ref.getDbMapping())) {
1387: local = ref.getID();
1388: } else if (localKeyIsPrototype()) {
1389: local = ref.getDbMapping().getStorageTypeName();
1390: } else {
1391: String homeprop = ownType
1392: .columnNameToProperty(localKey);
1393: if (homeprop == null) {
1394: throw new SQLException("Invalid local name '"
1395: + localKey + "' on " + ownType);
1396: }
1397: local = ref.getString(homeprop);
1398: }
1399:
1400: String columnName;
1401: if (foreignKeyIsPrimary()) {
1402: columnName = otherDbm.getIDField();
1403: } else {
1404: columnName = foreignKey;
1405: }
1406: otherDbm.appendCondition(q, columnName, local);
1407: }
1408:
1409: public boolean foreignKeyIsPrimary() {
1410: return (foreignKey == null)
1411: || "$id".equalsIgnoreCase(foreignKey)
1412: || foreignKey.equalsIgnoreCase(otherType
1413: .getIDField());
1414: }
1415:
1416: public boolean foreignKeyIsPrototype() {
1417: return "$prototype".equalsIgnoreCase(foreignKey);
1418: }
1419:
1420: public boolean localKeyIsPrimary(DbMapping homeMapping) {
1421: return (homeMapping == null)
1422: || (localKey == null)
1423: || "$id".equalsIgnoreCase(localKey)
1424: || localKey.equalsIgnoreCase(homeMapping
1425: .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() + "."
1446: + foreignKey;
1447: }
1448: }
1449: }
|