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: 8674 $
0014: * $Date: 2007-11-28 16:32:09 +0100 (Mit, 28 Nov 2007) $
0015: */
0016:
0017: package helma.objectmodel.db;
0018:
0019: import helma.framework.core.Application;
0020: import helma.framework.core.Prototype;
0021: import helma.util.ResourceProperties;
0022:
0023: import java.sql.*;
0024: import java.util.*;
0025:
0026: /**
0027: * A DbMapping describes how a certain type of Nodes is to mapped to a
0028: * relational database table. Basically it consists of a set of JavaScript property-to-
0029: * Database row bindings which are represented by instances of the Relation class.
0030: */
0031: public final class DbMapping {
0032: // DbMappings belong to an application
0033: protected final Application app;
0034:
0035: // prototype name of this mapping
0036: private final String typename;
0037:
0038: // properties from where the mapping is read
0039: private final ResourceProperties props;
0040:
0041: // name of data dbSource to which this mapping writes
0042: private DbSource dbSource;
0043:
0044: // name of datasource
0045: private String dbSourceName;
0046:
0047: // name of db table
0048: private String tableName;
0049:
0050: // the verbatim, unparsed _parent specification
0051: private String parentSetting;
0052:
0053: // list of properties to try for parent
0054: private ParentInfo[] parentInfo;
0055:
0056: // Relations describing subnodes and properties.
0057: protected Relation subRelation;
0058: protected Relation propRelation;
0059:
0060: // if this defines a subnode mapping with groupby layer,
0061: // we need a DbMapping for those groupby nodes
0062: private DbMapping groupbyMapping;
0063:
0064: // Map of property names to Relations objects
0065: private HashMap prop2db;
0066:
0067: // Map of db columns to Relations objects.
0068: // Case insensitive, keys are stored in lower case so
0069: // lookups must do a toLowerCase().
0070: private HashMap db2prop;
0071:
0072: // list of columns to fetch from db
0073: private DbColumn[] columns = null;
0074:
0075: // Map of db columns by name
0076: private HashMap columnMap;
0077:
0078: // Array of aggressively loaded references
0079: private Relation[] joins;
0080:
0081: // pre-rendered select statement
0082: private String selectString = null;
0083: private String insertString = null;
0084: private String updateString = null;
0085:
0086: // db field used as primary key
0087: private String idField;
0088:
0089: // db field used as object name
0090: private String nameField;
0091:
0092: // db field used to identify name of prototype to use for object instantiation
0093: private String protoField;
0094:
0095: // Used to map prototype ids to prototype names for
0096: // prototypes which extend the prototype represented by
0097: // this DbMapping.
0098: private ResourceProperties extensionMap;
0099:
0100: // a numeric or literal id used to represent this type in db
0101: private String extensionId;
0102:
0103: // dbmapping of parent prototype, if any
0104: private DbMapping parentMapping;
0105:
0106: // descriptor for key generation method
0107: private String idgen;
0108:
0109: // remember last key generated for this table
0110: private long lastID;
0111:
0112: // timestamp of last modification of the mapping (type.properties)
0113: // init value is -1 so we know we have to run update once even if
0114: // the underlying properties file is non-existent
0115: long lastTypeChange = -1;
0116:
0117: // timestamp of last modification of an object of this type
0118: long lastDataChange = 0;
0119:
0120: // evict objects of this type when received via replication
0121: private boolean evictOnReplication;
0122:
0123: // Set of mappings that depend on us and should be forwarded last data change events
0124: HashSet dependentMappings = new HashSet();
0125:
0126: // does this DbMapping describe a virtual node (collection, mountpoint, groupnode)?
0127: private boolean virtual = false;
0128:
0129: /**
0130: * Create an internal DbMapping used for "virtual" mappings aka collections, mountpoints etc.
0131: */
0132: public DbMapping(Application app, String parentTypeName) {
0133: this (app, parentTypeName, null);
0134: // DbMappings created with this constructor always define virtual nodes
0135: virtual = true;
0136: if (parentTypeName != null) {
0137: parentMapping = app.getDbMapping(parentTypeName);
0138: if (parentMapping == null) {
0139: throw new IllegalArgumentException(
0140: "Unknown parent mapping: " + parentTypeName);
0141: }
0142: }
0143: }
0144:
0145: /**
0146: * Create a DbMapping from a type.properties property file
0147: */
0148: public DbMapping(Application app, String typename,
0149: ResourceProperties props) {
0150: this .app = app;
0151: // create a unique instance of the string. This is useful so
0152: // we can compare types just by using == instead of equals.
0153: this .typename = typename == null ? null : typename.intern();
0154:
0155: prop2db = new HashMap();
0156: db2prop = new HashMap();
0157: columnMap = new HashMap();
0158: parentInfo = null;
0159: idField = null;
0160: this .props = props;
0161:
0162: if (props != null) {
0163: readBasicProperties();
0164: }
0165: }
0166:
0167: /**
0168: * Tell the type manager whether we need update() to be called
0169: */
0170: public boolean needsUpdate() {
0171: return props.lastModified() != lastTypeChange;
0172: }
0173:
0174: /**
0175: * Read in basic properties and register dbmapping with the
0176: * dbsource.
0177: */
0178: private void readBasicProperties() {
0179: tableName = props.getProperty("_table");
0180: dbSourceName = props.getProperty("_db");
0181:
0182: if (dbSourceName != null) {
0183: dbSource = app.getDbSource(dbSourceName);
0184:
0185: if (dbSource == null) {
0186: app
0187: .logError("*** Data Source for prototype "
0188: + typename + " does not exist: "
0189: + dbSourceName);
0190: app.logError("*** accessing or storing a " + typename
0191: + " object will cause an error.");
0192: } else if (tableName == null) {
0193: app
0194: .logError("*** No table name specified for prototype "
0195: + typename);
0196: app.logError("*** accessing or storing a " + typename
0197: + " object will cause an error.");
0198:
0199: // mark mapping as invalid by nulling the dbSource field
0200: dbSource = null;
0201: } else {
0202: // dbSource and tableName not null - register this instance
0203: dbSource.registerDbMapping(this );
0204: }
0205: }
0206: }
0207:
0208: /**
0209: * Read the mapping from the Properties. Return true if the properties were changed.
0210: * The read is split in two, this method and the rewire method. The reason is that in order
0211: * for rewire to work, all other db mappings must have been initialized and registered.
0212: */
0213: public synchronized void update() {
0214: // read in properties
0215: readBasicProperties();
0216: idgen = props.getProperty("_idgen");
0217: // if id field is null, we assume "ID" as default. We don't set it
0218: // however, so that if null we check the parent prototype first.
0219: idField = props.getProperty("_id");
0220: nameField = props.getProperty("_name");
0221: protoField = props.getProperty("_prototype");
0222: evictOnReplication = "true".equals(props
0223: .getProperty("_evictOnReplication"));
0224:
0225: parentSetting = props.getProperty("_parent");
0226: if (parentSetting != null) {
0227: // comma-separated list of properties to be used as parent
0228: StringTokenizer st = new StringTokenizer(parentSetting,
0229: ",;");
0230: parentInfo = new ParentInfo[st.countTokens()];
0231:
0232: for (int i = 0; i < parentInfo.length; i++) {
0233: parentInfo[i] = new ParentInfo(st.nextToken().trim());
0234: }
0235: } else {
0236: parentInfo = null;
0237: }
0238:
0239: lastTypeChange = props.lastModified();
0240:
0241: // see if this prototype extends (inherits from) any other prototype
0242: String extendsProto = props.getProperty("_extends");
0243:
0244: if (extendsProto != null) {
0245: parentMapping = app.getDbMapping(extendsProto);
0246: if (parentMapping == null) {
0247: app
0248: .logError("*** Parent mapping for prototype "
0249: + typename + " does not exist: "
0250: + extendsProto);
0251: } else {
0252: if (parentMapping.needsUpdate()) {
0253: parentMapping.update();
0254: }
0255: // if tableName or DbSource are inherited from the parent mapping
0256: // set them to null so we are aware of the fact.
0257: if (tableName != null
0258: && tableName.equals(parentMapping
0259: .getTableName())) {
0260: tableName = null;
0261: }
0262: if (dbSourceName != null
0263: && dbSourceName.equals(parentMapping
0264: .getDbSourceName())) {
0265: dbSourceName = null;
0266: dbSource = null;
0267: }
0268: }
0269: } else {
0270: parentMapping = null;
0271: }
0272:
0273: if (inheritsStorage() && getPrototypeField() == null) {
0274: app
0275: .logError("*** Prototype not stored for extended relational type "
0276: + typename);
0277: app
0278: .logError("*** objects fetched from db will have base prototype!");
0279: }
0280:
0281: // check if there is an extension-id specified inside the type.properties
0282: extensionId = props.getProperty("_extensionId", typename);
0283: registerExtension(extensionId, typename);
0284:
0285: // set the parent prototype in the corresponding Prototype object!
0286: // this was previously done by TypeManager, but we need to do it
0287: // ourself because DbMapping.update() may be called by other code than
0288: // the TypeManager.
0289: if (typename != null && !"global".equalsIgnoreCase(typename)
0290: && !"hopobject".equalsIgnoreCase(typename)) {
0291: Prototype proto = app.getPrototypeByName(typename);
0292: if (proto != null) {
0293: if (extendsProto != null) {
0294: proto.setParentPrototype(app
0295: .getPrototypeByName(extendsProto));
0296: } else if (!app.isJavaPrototype(typename)) {
0297: proto.setParentPrototype(app
0298: .getPrototypeByName("hopobject"));
0299: }
0300: }
0301: }
0302:
0303: // null the cached columns and select string
0304: columns = null;
0305: columnMap.clear();
0306: selectString = insertString = updateString = null;
0307:
0308: HashMap p2d = new HashMap();
0309: HashMap d2p = new HashMap();
0310: ArrayList joinList = new ArrayList();
0311:
0312: for (Enumeration e = props.keys(); e.hasMoreElements();) {
0313: String propName = (String) e.nextElement();
0314:
0315: try {
0316: // ignore internal properties (starting with "_") and sub-options (containing a ".")
0317: if (!propName.startsWith("_")
0318: && (propName.indexOf(".") < 0)) {
0319: String dbField = props.getProperty(propName);
0320:
0321: // check if a relation for this propery already exists. If so, reuse it
0322: Relation rel = (Relation) prop2db.get(propName
0323: .toLowerCase());
0324:
0325: if (rel == null) {
0326: rel = new Relation(propName, this );
0327: }
0328:
0329: rel.update(dbField, props);
0330:
0331: // store relation with lower case property name
0332: // (ResourceProperties now preserve key capitalization!)
0333: p2d.put(propName.toLowerCase(), rel);
0334:
0335: if ((rel.columnName != null)
0336: && rel.isPrimitiveOrReference()) {
0337: Relation old = (Relation) d2p.put(
0338: rel.columnName.toLowerCase(), rel);
0339: // check if we're overwriting another relation
0340: // if so, primitive relations get precendence to references
0341: if (old != null) {
0342: if (rel.isPrimitive() && old.isPrimitive()) {
0343: app
0344: .logEvent("*** Duplicate mapping for "
0345: + typename
0346: + "."
0347: + rel.columnName);
0348: } else if (rel.isReference()
0349: && old.isPrimitive()) {
0350: // if a column is used both in a primitive and a reference mapping,
0351: // use primitive mapping as primary one and mark reference as
0352: // complex so it will be fetched separately
0353: d2p.put(old.columnName.toLowerCase(),
0354: old);
0355: rel.reftype = Relation.COMPLEX_REFERENCE;
0356: } else if (rel.isPrimitive()
0357: && old.isReference()) {
0358: old.reftype = Relation.COMPLEX_REFERENCE;
0359: }
0360: }
0361: }
0362:
0363: // check if a reference is aggressively fetched
0364: if (rel.aggressiveLoading
0365: && (rel.isReference() || rel
0366: .isComplexReference())) {
0367: joinList.add(rel);
0368: }
0369:
0370: // app.logEvent ("Mapping "+propName+" -> "+dbField);
0371: }
0372: } catch (Exception x) {
0373: app.logEvent("Error in type.properties: "
0374: + x.getMessage());
0375: }
0376: }
0377:
0378: prop2db = p2d;
0379: db2prop = d2p;
0380:
0381: joins = new Relation[joinList.size()];
0382: joins = (Relation[]) joinList.toArray(joins);
0383:
0384: String subnodeMapping = props.getProperty("_children");
0385:
0386: if (subnodeMapping != null) {
0387: try {
0388: // check if subnode relation already exists. If so, reuse it
0389: if (subRelation == null) {
0390: subRelation = new Relation("_children", this );
0391: }
0392:
0393: subRelation.update(subnodeMapping, props);
0394:
0395: // if subnodes are accessed via access name or group name,
0396: // the subnode relation is also the property relation.
0397: if ((subRelation.accessName != null)
0398: || (subRelation.groupby != null)) {
0399: propRelation = subRelation;
0400: } else {
0401: propRelation = null;
0402: }
0403: } catch (Exception x) {
0404: app.logEvent("Error reading _subnodes relation for "
0405: + typename + ": " + x.getMessage());
0406:
0407: // subRelation = null;
0408: }
0409: } else {
0410: subRelation = propRelation = null;
0411: }
0412:
0413: if (groupbyMapping != null) {
0414: initGroupbyMapping();
0415: groupbyMapping.lastTypeChange = this .lastTypeChange;
0416: }
0417: }
0418:
0419: /**
0420: * Add the given extensionId and the coresponding prototypename
0421: * to extensionMap for later lookup.
0422: * @param extID the id mapping to the prototypename recogniced by helma
0423: * @param extName the name of the extending prototype
0424: */
0425: private void registerExtension(String extID, String extName) {
0426: // lazy initialization of extensionMap
0427: if (extensionMap == null) {
0428: extensionMap = new ResourceProperties();
0429: extensionMap.setIgnoreCase(true);
0430: } else if (extensionMap.containsValue(extName)) {
0431: // remove any preexisting mapping for the given childmapping
0432: extensionMap.values().remove(extName);
0433: }
0434: extensionMap.setProperty(extID, extName);
0435: if (inheritsStorage()) {
0436: parentMapping.registerExtension(extID, extName);
0437: }
0438: }
0439:
0440: /**
0441: * Returns the Set of Prototypes extending this prototype
0442: * @return the Set of Prototypes extending this prototype
0443: */
0444: public String[] getExtensions() {
0445: return extensionMap == null ? new String[] { extensionId }
0446: : (String[]) extensionMap.keySet().toArray(
0447: new String[0]);
0448: }
0449:
0450: /**
0451: * Looks up the prototype name identified by the given id, returing
0452: * our own type name if it can't be resolved
0453: * @param id the id specified for the prototype
0454: * @return the name of the extending prototype
0455: */
0456: public String getPrototypeName(String id) {
0457: if (inheritsStorage()) {
0458: return parentMapping.getPrototypeName(id);
0459: }
0460: // fallback to base-prototype if the proto isn't recogniced
0461: if (id == null) {
0462: return typename;
0463: }
0464: return extensionMap.getProperty(id, typename);
0465: }
0466:
0467: /**
0468: * get the id-value of this extension
0469: */
0470: public String getExtensionId() {
0471: return extensionId;
0472: }
0473:
0474: /**
0475: * Method in interface Updatable.
0476: */
0477: public void remove() {
0478: // do nothing, removing of type properties is not implemented.
0479: }
0480:
0481: /**
0482: * Get a JDBC connection for this DbMapping.
0483: */
0484: public Connection getConnection() throws ClassNotFoundException,
0485: SQLException {
0486: if (dbSourceName == null) {
0487: if (parentMapping != null) {
0488: return parentMapping.getConnection();
0489: } else {
0490: throw new SQLException(
0491: "Tried to get Connection from non-relational embedded data source.");
0492: }
0493: }
0494:
0495: if (tableName == null) {
0496: throw new SQLException(
0497: "Invalid DbMapping, _table not specified: " + this );
0498: }
0499:
0500: // if dbSource was previously not available, check again
0501: if (dbSource == null) {
0502: dbSource = app.getDbSource(dbSourceName);
0503: }
0504:
0505: if (dbSource == null) {
0506: throw new SQLException(
0507: "Datasource not defined or unable to load driver: "
0508: + dbSourceName + ".");
0509: }
0510:
0511: return dbSource.getConnection();
0512: }
0513:
0514: /**
0515: * Get the DbSource object for this DbMapping. The DbSource describes a JDBC
0516: * data source including URL, JDBC driver, username and password.
0517: */
0518: public DbSource getDbSource() {
0519: if (dbSource == null) {
0520: if ((tableName != null) && (dbSourceName != null)) {
0521: dbSource = app.getDbSource(dbSourceName);
0522: } else if (parentMapping != null) {
0523: return parentMapping.getDbSource();
0524: }
0525: }
0526:
0527: return dbSource;
0528: }
0529:
0530: /**
0531: * Get the dbsource name used for this type mapping.
0532: */
0533: public String getDbSourceName() {
0534: if ((dbSourceName == null) && (parentMapping != null)) {
0535: return parentMapping.getDbSourceName();
0536: }
0537:
0538: return dbSourceName;
0539: }
0540:
0541: /**
0542: * Get the table name used for this type mapping.
0543: */
0544: public String getTableName() {
0545: if ((tableName == null) && (parentMapping != null)) {
0546: return parentMapping.getTableName();
0547: }
0548:
0549: return tableName;
0550: }
0551:
0552: /**
0553: * Get the application this DbMapping belongs to.
0554: */
0555: public Application getApplication() {
0556: return app;
0557: }
0558:
0559: /**
0560: * Get the name of this mapping's application
0561: */
0562: public String getAppName() {
0563: return app.getName();
0564: }
0565:
0566: /**
0567: * Get the name of the object type this DbMapping belongs to.
0568: */
0569: public String getTypeName() {
0570: return typename;
0571: }
0572:
0573: /**
0574: * Get the name of this type's parent type, if any.
0575: */
0576: public String getExtends() {
0577: return parentMapping == null ? null : parentMapping
0578: .getTypeName();
0579: }
0580:
0581: /**
0582: * Get the primary key column name for objects using this mapping.
0583: */
0584: public String getIDField() {
0585: if ((idField == null) && (parentMapping != null)) {
0586: return parentMapping.getIDField();
0587: }
0588:
0589: return (idField == null) ? "ID" : idField;
0590: }
0591:
0592: /**
0593: * Get the column used for (internal) names of objects of this type.
0594: */
0595: public String getNameField() {
0596: if ((nameField == null) && (parentMapping != null)) {
0597: return parentMapping.getNameField();
0598: }
0599:
0600: return nameField;
0601: }
0602:
0603: /**
0604: * Get the column used for names of prototype.
0605: */
0606: public String getPrototypeField() {
0607: if ((protoField == null) && (parentMapping != null)) {
0608: return parentMapping.getPrototypeField();
0609: }
0610:
0611: return protoField;
0612: }
0613:
0614: /**
0615: * Should objects of this type be evicted/discarded/reloaded when received via
0616: * cache replication?
0617: */
0618: public boolean evictOnReplication() {
0619: return evictOnReplication;
0620: }
0621:
0622: /**
0623: * Translate a database column name to an object property name according to this mapping.
0624: */
0625: public String columnNameToProperty(String columnName) {
0626: if (columnName == null) {
0627: return null;
0628: }
0629:
0630: // SEMIHACK: If columnName is a function call, try to extract actual
0631: // column name from it
0632: int open = columnName.indexOf('(');
0633: int close = columnName.indexOf(')');
0634: if (open > -1 && close > open) {
0635: columnName = columnName.substring(open + 1, close);
0636: }
0637:
0638: return _columnNameToProperty(columnName.toLowerCase());
0639: }
0640:
0641: private String _columnNameToProperty(final String columnName) {
0642: Relation rel = (Relation) db2prop.get(columnName);
0643:
0644: if ((rel == null) && (parentMapping != null)) {
0645: return parentMapping._columnNameToProperty(columnName);
0646: }
0647:
0648: if ((rel != null) && rel.isPrimitiveOrReference()) {
0649: return rel.propName;
0650: }
0651:
0652: return null;
0653: }
0654:
0655: /**
0656: * Translate an object property name to a database column name according
0657: * to this mapping. If no mapping is found, the property name is returned,
0658: * assuming property and column names are equal.
0659: */
0660: public String propertyToColumnName(String propName) {
0661: if (propName == null) {
0662: return null;
0663: }
0664:
0665: // prop2db stores keys in lower case
0666: return _propertyToColumnName(propName.toLowerCase());
0667: }
0668:
0669: private String _propertyToColumnName(final String propName) {
0670: Relation rel = (Relation) prop2db.get(propName);
0671:
0672: if ((rel == null) && (parentMapping != null)) {
0673: return parentMapping._propertyToColumnName(propName);
0674: }
0675:
0676: if ((rel != null) && (rel.isPrimitiveOrReference())) {
0677: return rel.columnName;
0678: }
0679:
0680: return null;
0681: }
0682:
0683: /**
0684: * Translate a database column name to an object property name according to this mapping.
0685: */
0686: public Relation columnNameToRelation(String columnName) {
0687: if (columnName == null) {
0688: return null;
0689: }
0690:
0691: return _columnNameToRelation(columnName.toLowerCase());
0692: }
0693:
0694: private Relation _columnNameToRelation(final String columnName) {
0695: Relation rel = (Relation) db2prop.get(columnName);
0696:
0697: if ((rel == null) && (parentMapping != null)) {
0698: return parentMapping._columnNameToRelation(columnName);
0699: }
0700:
0701: return rel;
0702: }
0703:
0704: /**
0705: * Translate an object property name to a database column name according to this mapping.
0706: */
0707: public Relation propertyToRelation(String propName) {
0708: if (propName == null) {
0709: return null;
0710: }
0711:
0712: // FIXME: prop2db stores keys in lower case, because it gets them
0713: // from a SystemProperties object which converts keys to lower case.
0714: return _propertyToRelation(propName.toLowerCase());
0715: }
0716:
0717: private Relation _propertyToRelation(String propName) {
0718: Relation rel = (Relation) prop2db.get(propName);
0719:
0720: if ((rel == null) && (parentMapping != null)) {
0721: return parentMapping._propertyToRelation(propName);
0722: }
0723:
0724: return rel;
0725: }
0726:
0727: /**
0728: * @return the parent info as unparsed string.
0729: */
0730: public String getParentSetting() {
0731: if ((parentSetting == null) && (parentMapping != null)) {
0732: return parentMapping.getParentSetting();
0733: }
0734: return parentSetting;
0735: }
0736:
0737: /**
0738: * @return the parent info array, which tells an object of this type how to
0739: * determine its parent object.
0740: */
0741: public synchronized ParentInfo[] getParentInfo() {
0742: if ((parentInfo == null) && (parentMapping != null)) {
0743: return parentMapping.getParentInfo();
0744: }
0745:
0746: return parentInfo;
0747: }
0748:
0749: /**
0750: *
0751: *
0752: * @return ...
0753: */
0754: public DbMapping getSubnodeMapping() {
0755: if (subRelation != null) {
0756: return subRelation.otherType;
0757: }
0758:
0759: if (parentMapping != null) {
0760: return parentMapping.getSubnodeMapping();
0761: }
0762:
0763: return null;
0764: }
0765:
0766: /**
0767: *
0768: *
0769: * @param propname ...
0770: *
0771: * @return ...
0772: */
0773: public DbMapping getPropertyMapping(String propname) {
0774: Relation rel = getPropertyRelation(propname);
0775:
0776: if (rel != null) {
0777: // if this is a virtual node, it doesn't have a dbmapping
0778: if (rel.virtual && (rel.prototype == null)) {
0779: return null;
0780: } else {
0781: return rel.otherType;
0782: }
0783: }
0784:
0785: return null;
0786: }
0787:
0788: /**
0789: * If subnodes are grouped by one of their properties, return the
0790: * db-mapping with the right relations to create the group-by nodes
0791: */
0792: public synchronized DbMapping getGroupbyMapping() {
0793: if ((subRelation == null) && (parentMapping != null)) {
0794: return parentMapping.getGroupbyMapping();
0795: } else if (subRelation.groupby == null) {
0796: return null;
0797: } else if (groupbyMapping == null) {
0798: initGroupbyMapping();
0799: }
0800:
0801: return groupbyMapping;
0802: }
0803:
0804: /**
0805: * Initialize the dbmapping used for group-by nodes.
0806: */
0807: private void initGroupbyMapping() {
0808: // if a prototype is defined for groupby nodes, use that
0809: // if mapping doesn' exist or isn't defined, create a new (anonymous internal) one
0810: groupbyMapping = new DbMapping(app,
0811: subRelation.groupbyPrototype);
0812:
0813: // set subnode and property relations
0814: groupbyMapping.subRelation = subRelation
0815: .getGroupbySubnodeRelation();
0816:
0817: if (propRelation != null) {
0818: groupbyMapping.propRelation = propRelation
0819: .getGroupbyPropertyRelation();
0820: } else {
0821: groupbyMapping.propRelation = subRelation
0822: .getGroupbyPropertyRelation();
0823: }
0824: }
0825:
0826: /**
0827: *
0828: *
0829: * @param rel ...
0830: */
0831: public void setPropertyRelation(Relation rel) {
0832: propRelation = rel;
0833: }
0834:
0835: /**
0836: *
0837: *
0838: * @return ...
0839: */
0840: public Relation getSubnodeRelation() {
0841: if ((subRelation == null) && (parentMapping != null)) {
0842: return parentMapping.getSubnodeRelation();
0843: }
0844:
0845: return subRelation;
0846: }
0847:
0848: /**
0849: * Return the list of defined property names as String array.
0850: */
0851: public String[] getPropertyNames() {
0852: return (String[]) prop2db.keySet().toArray(
0853: new String[prop2db.size()]);
0854: }
0855:
0856: /**
0857: *
0858: *
0859: * @return ...
0860: */
0861: private Relation getPropertyRelation() {
0862: if ((propRelation == null) && (parentMapping != null)) {
0863: return parentMapping.getPropertyRelation();
0864: }
0865:
0866: return propRelation;
0867: }
0868:
0869: /**
0870: *
0871: *
0872: * @param propname ...
0873: *
0874: * @return ...
0875: */
0876: public Relation getPropertyRelation(String propname) {
0877: if (propname == null) {
0878: return getPropertyRelation();
0879: }
0880:
0881: // first try finding an exact match for the property name
0882: Relation rel = getExactPropertyRelation(propname);
0883:
0884: // if not defined, return the generic property mapping
0885: if (rel == null) {
0886: rel = getPropertyRelation();
0887: }
0888:
0889: return rel;
0890: }
0891:
0892: /**
0893: *
0894: *
0895: * @param propname ...
0896: *
0897: * @return ...
0898: */
0899: public Relation getExactPropertyRelation(String propname) {
0900: if (propname == null) {
0901: return null;
0902: }
0903:
0904: Relation rel = (Relation) prop2db.get(propname.toLowerCase());
0905:
0906: if ((rel == null) && (parentMapping != null)) {
0907: rel = parentMapping.getExactPropertyRelation(propname);
0908: }
0909:
0910: return rel;
0911: }
0912:
0913: /**
0914: *
0915: *
0916: * @return ...
0917: */
0918: public String getSubnodeGroupby() {
0919: if ((subRelation == null) && (parentMapping != null)) {
0920: return parentMapping.getSubnodeGroupby();
0921: }
0922:
0923: return (subRelation == null) ? null : subRelation.groupby;
0924: }
0925:
0926: /**
0927: *
0928: *
0929: * @return ...
0930: */
0931: public String getIDgen() {
0932: if ((idgen == null) && (parentMapping != null)) {
0933: return parentMapping.getIDgen();
0934: }
0935:
0936: return idgen;
0937: }
0938:
0939: /**
0940: *
0941: *
0942: * @return ...
0943: */
0944: public WrappedNodeManager getWrappedNodeManager() {
0945: if (app == null) {
0946: throw new RuntimeException(
0947: "Can't get node manager from internal db mapping");
0948: }
0949:
0950: return app.getWrappedNodeManager();
0951: }
0952:
0953: /**
0954: * Tell whether this data mapping maps to a relational database table. This returns true
0955: * if a datasource is specified, even if it is not a valid one. Otherwise, objects with invalid
0956: * mappings would be stored in the embedded db instead of an error being thrown, which is
0957: * not what we want.
0958: */
0959: public boolean isRelational() {
0960: return dbSourceName != null
0961: || (parentMapping != null && parentMapping
0962: .isRelational());
0963: }
0964:
0965: /**
0966: * Return an array of DbColumns for the relational table mapped by this DbMapping.
0967: */
0968: public synchronized DbColumn[] getColumns()
0969: throws ClassNotFoundException, SQLException {
0970: if (!isRelational()) {
0971: throw new SQLException(
0972: "Can't get columns for non-relational data mapping "
0973: + this );
0974: }
0975:
0976: // Use local variable cols to avoid synchronization (schema may be nulled elsewhere)
0977: if (columns == null) {
0978: // we do two things here: set the SQL type on the Relation mappings
0979: // and build a string of column names.
0980: Connection con = getConnection();
0981: Statement stmt = con.createStatement();
0982: String table = getTableName();
0983:
0984: if (table == null) {
0985: throw new SQLException(
0986: "Table name is null in getColumns() for "
0987: + this );
0988: }
0989:
0990: ResultSet rs = stmt.executeQuery(new StringBuffer(
0991: "SELECT * FROM ").append(table).append(
0992: " WHERE 1 = 0").toString());
0993:
0994: if (rs == null) {
0995: throw new SQLException("Error retrieving columns for "
0996: + this );
0997: }
0998:
0999: ResultSetMetaData meta = rs.getMetaData();
1000:
1001: // ok, we have the meta data, now loop through mapping...
1002: int ncols = meta.getColumnCount();
1003: ArrayList list = new ArrayList(ncols);
1004:
1005: for (int i = 0; i < ncols; i++) {
1006: String colName = meta.getColumnName(i + 1);
1007: Relation rel = columnNameToRelation(colName);
1008:
1009: DbColumn col = new DbColumn(colName, meta
1010: .getColumnType(i + 1), rel, this );
1011: list.add(col);
1012: }
1013: columns = (DbColumn[]) list.toArray(new DbColumn[list
1014: .size()]);
1015: }
1016:
1017: return columns;
1018: }
1019:
1020: /**
1021: * Return the array of relations that are fetched with objects of this type.
1022: */
1023: public Relation[] getJoins() {
1024: return joins;
1025: }
1026:
1027: /**
1028: *
1029: *
1030: * @param columnName ...
1031: *
1032: * @return ...
1033: *
1034: * @throws ClassNotFoundException ...
1035: * @throws SQLException ...
1036: */
1037: public DbColumn getColumn(String columnName)
1038: throws ClassNotFoundException, SQLException {
1039: DbColumn col = (DbColumn) columnMap.get(columnName);
1040: if (col == null) {
1041: DbColumn[] cols = columns;
1042: if (cols == null) {
1043: cols = getColumns();
1044: }
1045: for (int i = 0; i < cols.length; i++) {
1046: if (columnName.equalsIgnoreCase(cols[i].getName())) {
1047: col = cols[i];
1048: break;
1049: }
1050: }
1051: columnMap.put(columnName, col);
1052: }
1053: return col;
1054: }
1055:
1056: /**
1057: * Get a StringBuffer initialized to the first part of the select statement
1058: * for objects defined by this DbMapping
1059: *
1060: * @param rel the Relation we use to select. Currently only used for optimizer hints.
1061: * Is null if selecting by primary key.
1062: * @return the StringBuffer containing the first part of the select query
1063: */
1064: public StringBuffer getSelect(Relation rel) {
1065: // assign to local variable first so we are thread safe
1066: // (selectString may be reset by other threads)
1067: String sel = selectString;
1068: boolean isOracle = isOracle();
1069:
1070: if (rel == null && sel != null) {
1071: return new StringBuffer(sel);
1072: }
1073:
1074: StringBuffer s = new StringBuffer("SELECT ");
1075:
1076: if (rel != null && rel.queryHints != null) {
1077: s.append(rel.queryHints).append(" ");
1078: }
1079:
1080: String table = getTableName();
1081:
1082: // all columns from the main table
1083: s.append(table);
1084: s.append(".*");
1085:
1086: for (int i = 0; i < joins.length; i++) {
1087: if (!joins[i].otherType.isRelational()) {
1088: continue;
1089: }
1090: s.append(", ");
1091: s.append(Relation.JOIN_PREFIX);
1092: s.append(joins[i].propName);
1093: s.append(".*");
1094: }
1095:
1096: s.append(" FROM ");
1097:
1098: s.append(table);
1099:
1100: if (rel != null) {
1101: rel.appendAdditionalTables(s);
1102: }
1103:
1104: s.append(" ");
1105:
1106: for (int i = 0; i < joins.length; i++) {
1107: if (!joins[i].otherType.isRelational()) {
1108: continue;
1109: }
1110: if (isOracle) {
1111: // generate an old-style oracle left join - see
1112: // http://www.praetoriate.com/oracle_tips_outer_joins.htm
1113: s.append(", ");
1114: s.append(joins[i].otherType.getTableName());
1115: s.append(" ");
1116: s.append(Relation.JOIN_PREFIX);
1117: s.append(joins[i].propName);
1118: s.append(" ");
1119: } else {
1120: s.append("LEFT OUTER JOIN ");
1121: s.append(joins[i].otherType.getTableName());
1122: s.append(" ");
1123: s.append(Relation.JOIN_PREFIX);
1124: s.append(joins[i].propName);
1125: s.append(" ON ");
1126: joins[i].renderJoinConstraints(s, isOracle);
1127: }
1128: }
1129:
1130: // cache rendered string for later calls, but only if it wasn't
1131: // built for a particular Relation
1132: if (rel == null) {
1133: selectString = s.toString();
1134: }
1135:
1136: return s;
1137: }
1138:
1139: /**
1140: *
1141: *
1142: * @return ...
1143: */
1144: public String getInsert() throws ClassNotFoundException,
1145: SQLException {
1146: String ins = insertString;
1147:
1148: if (ins != null) {
1149: return ins;
1150: }
1151:
1152: StringBuffer b1 = new StringBuffer("INSERT INTO ");
1153: StringBuffer b2 = new StringBuffer(" ) VALUES ( ");
1154: b1.append(getTableName());
1155: b1.append(" ( ");
1156:
1157: DbColumn[] cols = getColumns();
1158: boolean needsComma = false;
1159:
1160: for (int i = 0; i < cols.length; i++) {
1161: if (cols[i].isMapped()) {
1162: if (needsComma) {
1163: b1.append(", ");
1164: b2.append(", ");
1165: }
1166: b1.append(cols[i].getName());
1167: b2.append("?");
1168: needsComma = true;
1169: }
1170: }
1171:
1172: b1.append(b2.toString());
1173: b1.append(" )");
1174:
1175: // cache rendered string for later calls.
1176: ins = insertString = b1.toString();
1177:
1178: return ins;
1179: }
1180:
1181: /**
1182: *
1183: *
1184: * @return ...
1185: */
1186: public StringBuffer getUpdate() {
1187: String upd = updateString;
1188:
1189: if (upd != null) {
1190: return new StringBuffer(upd);
1191: }
1192:
1193: StringBuffer s = new StringBuffer("UPDATE ");
1194:
1195: s.append(getTableName());
1196: s.append(" SET ");
1197:
1198: // cache rendered string for later calls.
1199: updateString = s.toString();
1200:
1201: return s;
1202: }
1203:
1204: /**
1205: * Return true if values for the column identified by the parameter need
1206: * to be quoted in SQL queries.
1207: */
1208: public boolean needsQuotes(String columnName) throws SQLException,
1209: ClassNotFoundException {
1210: if ((tableName == null) && (parentMapping != null)) {
1211: return parentMapping.needsQuotes(columnName);
1212: }
1213: DbColumn col = getColumn(columnName);
1214: // This is not a mapped column. In case of doubt, add quotes.
1215: if (col == null) {
1216: return true;
1217: } else {
1218: return col.needsQuotes();
1219: }
1220: }
1221:
1222: /**
1223: * Add constraints to select query string to join object references
1224: */
1225: public void addJoinConstraints(StringBuffer s, String pre) {
1226: boolean isOracle = isOracle();
1227: String prefix = pre;
1228:
1229: if (!isOracle) {
1230: // constraints have already been rendered by getSelect()
1231: return;
1232: }
1233:
1234: for (int i = 0; i < joins.length; i++) {
1235: if (!joins[i].otherType.isRelational()) {
1236: continue;
1237: }
1238: s.append(prefix);
1239: joins[i].renderJoinConstraints(s, isOracle);
1240: prefix = " AND ";
1241: }
1242: }
1243:
1244: /**
1245: * Is the database behind this an Oracle db?
1246: *
1247: * @return true if the dbsource is using an oracle JDBC driver
1248: */
1249: public boolean isOracle() {
1250: if (dbSource != null) {
1251: return dbSource.isOracle();
1252: }
1253: if (parentMapping != null) {
1254: return parentMapping.isOracle();
1255: }
1256: return false;
1257: }
1258:
1259: /**
1260: * Is the database behind this a MySQL db?
1261: *
1262: * @return true if the dbsource is using a MySQL JDBC driver
1263: */
1264: public boolean isMySQL() {
1265: if (dbSource != null) {
1266: return dbSource.isMySQL();
1267: }
1268: if (parentMapping != null) {
1269: return parentMapping.isMySQL();
1270: }
1271: return false;
1272: }
1273:
1274: /**
1275: * Is the database behind this a PostgreSQL db?
1276: *
1277: * @return true if the dbsource is using a PostgreSQL JDBC driver
1278: */
1279: public boolean isPostgreSQL() {
1280: if (dbSource != null) {
1281: return dbSource.isPostgreSQL();
1282: }
1283: if (parentMapping != null) {
1284: return parentMapping.isPostgreSQL();
1285: }
1286: return false;
1287: }
1288:
1289: /**
1290: * Is the database behind this a H2 db?
1291: *
1292: * @return true if the dbsource is using a H2 JDBC driver
1293: */
1294: public boolean isH2() {
1295: if (dbSource != null) {
1296: return dbSource.isH2();
1297: }
1298: if (parentMapping != null) {
1299: return parentMapping.isH2();
1300: }
1301: return false;
1302: }
1303:
1304: /**
1305: * Return a string representation for this DbMapping
1306: *
1307: * @return a string representation
1308: */
1309: public String toString() {
1310: if (typename == null) {
1311: return "[unspecified internal DbMapping]";
1312: } else {
1313: return ("[" + app.getName() + "." + typename + "]");
1314: }
1315: }
1316:
1317: /**
1318: * Get the last time something changed in the Mapping
1319: *
1320: * @return time of last mapping change
1321: */
1322: public long getLastTypeChange() {
1323: return lastTypeChange;
1324: }
1325:
1326: /**
1327: * Get the last time something changed in our data
1328: *
1329: * @return time of last data change
1330: */
1331: public long getLastDataChange() {
1332: // refer to parent mapping if it uses the same db/table
1333: if (inheritsStorage()) {
1334: return parentMapping.getLastDataChange();
1335: } else {
1336: return lastDataChange;
1337: }
1338: }
1339:
1340: /**
1341: * Set the last time something changed in the data, propagating the event
1342: * to mappings that depend on us through an additionalTables switch.
1343: */
1344: public void setLastDataChange() {
1345: // forward data change timestamp to storage-compatible parent mapping
1346: if (inheritsStorage()) {
1347: parentMapping.setLastDataChange();
1348: } else {
1349: lastDataChange += 1;
1350: // propagate data change timestamp to mappings that depend on us
1351: if (!dependentMappings.isEmpty()) {
1352: Iterator it = dependentMappings.iterator();
1353: while (it.hasNext()) {
1354: DbMapping dbmap = (DbMapping) it.next();
1355: dbmap.setIndirectDataChange();
1356: }
1357: }
1358: }
1359: }
1360:
1361: /**
1362: * Set the last time something changed in the data. This is already an indirect
1363: * data change triggered by a mapping we depend on, so we don't propagate it to
1364: * mappings that depend on us through an additionalTables switch.
1365: */
1366: protected void setIndirectDataChange() {
1367: // forward data change timestamp to storage-compatible parent mapping
1368: if (inheritsStorage()) {
1369: parentMapping.setIndirectDataChange();
1370: } else {
1371: lastDataChange += 1;
1372: }
1373: }
1374:
1375: /**
1376: * Helper method to generate a new ID. This is only used in the special case
1377: * when using the select(max) method and the underlying table is still empty.
1378: *
1379: * @param dbmax the maximum value already stored in db
1380: * @return a new and hopefully unique id
1381: */
1382: protected synchronized long getNewID(long dbmax) {
1383: // refer to parent mapping if it uses the same db/table
1384: if (inheritsStorage()) {
1385: return parentMapping.getNewID(dbmax);
1386: } else {
1387: lastID = Math.max(dbmax + 1, lastID + 1);
1388: return lastID;
1389: }
1390: }
1391:
1392: /**
1393: * Return an enumeration of all properties defined by this db mapping.
1394: *
1395: * @return the property enumeration
1396: */
1397: public Enumeration getPropertyEnumeration() {
1398: HashSet set = new HashSet();
1399:
1400: collectPropertyNames(set);
1401:
1402: final Iterator it = set.iterator();
1403:
1404: return new Enumeration() {
1405: public boolean hasMoreElements() {
1406: return it.hasNext();
1407: }
1408:
1409: public Object nextElement() {
1410: return it.next();
1411: }
1412: };
1413: }
1414:
1415: /**
1416: * Collect a set of all properties defined by this db mapping
1417: *
1418: * @param basket the set to put properties into
1419: */
1420: private void collectPropertyNames(HashSet basket) {
1421: // fetch propnames from parent mapping first, than add our own.
1422: if (parentMapping != null) {
1423: parentMapping.collectPropertyNames(basket);
1424: }
1425:
1426: if (!prop2db.isEmpty()) {
1427: basket.addAll(prop2db.keySet());
1428: }
1429: }
1430:
1431: /**
1432: * Return the name of the prototype which specifies the storage location
1433: * (dbsource + tablename) for this type, or null if it is stored in the embedded
1434: * db.
1435: */
1436: public String getStorageTypeName() {
1437: if (inheritsStorage()) {
1438: return parentMapping.getStorageTypeName();
1439: }
1440: return (dbSourceName == null) ? null : typename;
1441: }
1442:
1443: /**
1444: * Check whether this DbMapping inherits its storage location from its
1445: * parent mapping. The raison d'etre for this is that we need to detect
1446: * inherited storage even if the dbsource and table are explicitly set
1447: * in the extended mapping.
1448: *
1449: * @return true if this mapping shares its parent mapping storage
1450: */
1451: protected boolean inheritsStorage() {
1452: // note: tableName and dbSourceName are nulled out in update() if they
1453: // are inherited from the parent mapping. This way we know that
1454: // storage is not inherited if either of them is not null.
1455: return isRelational() && parentMapping != null
1456: && tableName == null && dbSourceName == null;
1457: }
1458:
1459: /**
1460: * Static utility method to check whether two DbMappings use the same storage.
1461: *
1462: * @return true if both use the embedded database or the same relational table.
1463: */
1464: public static boolean areStorageCompatible(DbMapping dbm1,
1465: DbMapping dbm2) {
1466: if (dbm1 == null)
1467: return dbm2 == null || !dbm2.isRelational();
1468: return dbm1.isStorageCompatible(dbm2);
1469: }
1470:
1471: /**
1472: * Tell if this DbMapping uses the same storage as the given DbMapping.
1473: *
1474: * @return true if both use the embedded database or the same relational table.
1475: */
1476: public boolean isStorageCompatible(DbMapping other) {
1477: if (other == null) {
1478: return !isRelational();
1479: } else if (other == this ) {
1480: return true;
1481: } else if (isRelational()) {
1482: return getTableName().equals(other.getTableName())
1483: && getDbSource().equals(other.getDbSource());
1484: }
1485:
1486: return !other.isRelational();
1487: }
1488:
1489: /**
1490: * Return true if this db mapping represents the prototype indicated
1491: * by the string argument, either itself or via one of its parent prototypes.
1492: */
1493: public boolean isInstanceOf(String other) {
1494: if ((typename != null) && typename.equals(other)) {
1495: return true;
1496: }
1497:
1498: DbMapping p = parentMapping;
1499:
1500: while (p != null) {
1501: if ((p.typename != null) && p.typename.equals(other)) {
1502: return true;
1503: }
1504:
1505: p = p.parentMapping;
1506: }
1507:
1508: return false;
1509: }
1510:
1511: /**
1512: * Get the mapping we inherit from, or null
1513: *
1514: * @return the parent DbMapping, or null
1515: */
1516: public DbMapping getParentMapping() {
1517: return parentMapping;
1518: }
1519:
1520: /**
1521: * Get our ResourceProperties
1522: *
1523: * @return our properties
1524: */
1525: public ResourceProperties getProperties() {
1526: return props;
1527: }
1528:
1529: /**
1530: * Register a DbMapping that depends on this DbMapping, so that collections of other mapping
1531: * should be reloaded if data on this mapping is updated.
1532: *
1533: * @param dbmap the DbMapping that depends on us
1534: */
1535: protected void addDependency(DbMapping dbmap) {
1536: this .dependentMappings.add(dbmap);
1537: }
1538:
1539: /**
1540: * Append a sql-condition for the given column which must have
1541: * one of the values contained inside the given Set to the given
1542: * StringBuffer.
1543: * @param q the StringBuffer to append to
1544: * @param column the column which must match one of the values
1545: * @param values the list of values
1546: * @throws SQLException
1547: */
1548: protected void appendCondition(StringBuffer q, String column,
1549: String[] values) throws SQLException,
1550: ClassNotFoundException {
1551: if (values.length == 1) {
1552: appendCondition(q, column, values[0]);
1553: return;
1554: }
1555: if (column.indexOf('(') == -1 && column.indexOf('.') == -1) {
1556: q.append(getTableName()).append(".");
1557: }
1558: q.append(column).append(" in (");
1559:
1560: if (needsQuotes(column)) {
1561: for (int i = 0; i < values.length; i++) {
1562: if (i > 0)
1563: q.append(", ");
1564: q.append("'").append(escapeString(values[i])).append(
1565: "'");
1566: }
1567: } else {
1568: for (int i = 0; i < values.length; i++) {
1569: if (i > 0)
1570: q.append(", ");
1571: q.append(checkNumber(values[i]));
1572: }
1573: }
1574: q.append(")");
1575: }
1576:
1577: /**
1578: * Append a sql-condition for the given column which must have
1579: * the value given to the given StringBuffer.
1580: * @param q the StringBuffer to append to
1581: * @param column the column which must match one of the values
1582: * @param val the value
1583: * @throws SQLException
1584: */
1585: protected void appendCondition(StringBuffer q, String column,
1586: String val) throws SQLException, ClassNotFoundException {
1587: if (column.indexOf('(') == -1 && column.indexOf('.') == -1) {
1588: q.append(getTableName()).append(".");
1589: }
1590: q.append(column).append(" = ");
1591:
1592: if (needsQuotes(column)) {
1593: q.append("'").append(escapeString(val)).append("'");
1594: } else {
1595: q.append(checkNumber(val));
1596: }
1597: }
1598:
1599: /**
1600: * a utility method to escape single quotes used for inserting
1601: * string-values into relational databases.
1602: * Searches for "'" characters and escapes them by duplicating them (= "''")
1603: * @param str the string to escape
1604: * @return the escaped string
1605: */
1606: static String escapeString(Object value) {
1607: String str = value == null ? null : value.toString();
1608: if (str == null) {
1609: return null;
1610: } else if (str.indexOf("'") < 0) {
1611: return str;
1612: }
1613:
1614: int l = str.length();
1615: StringBuffer sbuf = new StringBuffer(l + 10);
1616:
1617: for (int i = 0; i < l; i++) {
1618: char c = str.charAt(i);
1619:
1620: if (c == '\'') {
1621: sbuf.append('\'');
1622: }
1623: sbuf.append(c);
1624: }
1625: return sbuf.toString();
1626: }
1627:
1628: /**
1629: * Utility method to check whether the argument is a number literal.
1630: * @param str a string representing a number literal
1631: * @return the argument, if it conforms to the number literal syntax
1632: * @throws IllegalArgumentException if the argument does not represent a number
1633: */
1634: static String checkNumber(Object value)
1635: throws IllegalArgumentException {
1636: String str = value == null ? null : value.toString();
1637: if (str == null) {
1638: return null;
1639: } else {
1640: str = str.trim();
1641: if (str.matches("(?:\\+|\\-)??\\d+(?:\\.\\d+)??")) {
1642: return str;
1643: }
1644: }
1645: throw new IllegalArgumentException("Illegal numeric literal: "
1646: + str);
1647: }
1648:
1649: /**
1650: * Find if this DbMapping describes a virtual node (collection, mountpoint, groupnode)
1651: * @return true if this instance describes a virtual node.
1652: */
1653: public boolean isVirtual() {
1654: return virtual;
1655: }
1656: }
|