/*
* Helma License Notice
*
* The contents of this file are subject to the Helma License
* Version 2.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://adele.helma.org/download/helma/license.txt
*
* Copyright 1998-2003 Helma Software. All Rights Reserved.
*
* $RCSfile$
* $Author: hannes $
* $Revision: 9992 $
* $Date: 2009-11-24 11:34:50 +0100 (Die, 24. Nov 2009) $
*/
package helma.objectmodel.db;
import helma.framework.core.Application;
import helma.objectmodel.INode;
import helma.objectmodel.IProperty;
import helma.util.StringUtils;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Enumeration;
import java.util.Vector;
/**
* This describes how a property of a persistent Object is stored in a
* relational database table. This can be either a scalar property (string, date, number etc.)
* or a reference to one or more other objects.
*/
public final class Relation {
// these constants define different type of property-to-db-mappings
// there is an error in the description of this relation
public final static int INVALID = -1;
// a mapping of a non-object, scalar type
public final static int PRIMITIVE = 0;
// a 1-to-1 relation, i.e. a field in the table is a foreign key to another object
public final static int REFERENCE = 1;
// a 1-to-many relation, a field in another table points to objects of this type
public final static int COLLECTION = 2;
// a 1-to-1 reference with multiple or otherwise not-trivial constraints
// this is managed differently than REFERENCE, hence the separate type.
public final static int COMPLEX_REFERENCE = 3;
// constraints linked together by OR or AND if applicable?
public final static String AND = " AND ";
public final static String OR = " OR ";
public final static String XOR = " XOR ";
private String logicalOperator = AND;
// prefix to use for symbolic names of joined tables. The name is composed
// from this prefix and the name of the property we're doing the join for
final static String JOIN_PREFIX = "JOIN_";
// direct mapping is a very powerful feature:
// objects of some types can be directly accessed
// by one of their properties/db fields.
// public final static int DIRECT = 3;
// the DbMapping of the type we come from
DbMapping ownType;
// the DbMapping of the prototype we link to, unless this is a "primitive" (non-object) relation
DbMapping otherType;
// the column type, as defined in java.sql.Types
int columnType;
// if this relation defines a virtual node, we need to provide a DbMapping for these virtual nodes
DbMapping virtualMapping;
String propName;
String columnName;
int reftype;
Constraint[] constraints;
boolean virtual;
boolean readonly;
boolean lazyLoading;
boolean aggressiveLoading;
boolean aggressiveCaching;
boolean isPrivate = false;
boolean referencesPrimaryKey = false;
String updateCriteria;
String accessName; // db column used to access objects through this relation
String order;
boolean autoSorted = false;
String groupbyOrder;
String groupby;
String prototype;
String groupbyPrototype;
String filter;
private String additionalTables;
private boolean additionalTablesJoined = false;
String queryHints;
Vector filterFragments;
Vector filterPropertyRefs;
int maxSize = 0;
int offset = 0;
/**
* This constructor makes a copy of an existing relation. Not all fields are copied, just those
* which are needed in groupby- and virtual nodes defined by this relation. use
* {@link Relation#getClone()} to get a full copy of this relation.
*/
protected Relation(Relation rel) {
// Note: prototype, groupby, groupbyPrototype and groupbyOrder aren't copied here.
// these are set by the individual get*Relation() methods as appropriate.
this.ownType = rel.ownType;
this.otherType = rel.otherType;
this.propName = rel.propName;
this.columnName = rel.columnName;
this.reftype = rel.reftype;
this.order = rel.order;
this.filter = rel.filter;
this.filterFragments = rel.filterFragments;
this.filterPropertyRefs = rel.filterPropertyRefs;
this.additionalTables = rel.additionalTables;
this.additionalTablesJoined = rel.additionalTablesJoined;
this.queryHints = rel.queryHints;
this.maxSize = rel.maxSize;
this.offset = rel.offset;
this.constraints = rel.constraints;
this.accessName = rel.accessName;
this.logicalOperator = rel.logicalOperator;
this.lazyLoading = rel.lazyLoading;
this.aggressiveLoading = rel.aggressiveLoading;
this.aggressiveCaching = rel.aggressiveCaching;
this.updateCriteria = rel.updateCriteria;
this.autoSorted = rel.autoSorted;
}
/**
* Reads a relation entry from a line in a properties file.
*/
public Relation(String propName, DbMapping ownType) {
this.ownType = ownType;
this.propName = propName;
otherType = null;
}
/**
* Update this relation object from a properties object.
* @param desc the top level relation descriptor. For relations
* defined in a type.properties file, this is a string like
* "collection(Type)", but for relations defined from
* JavaScript, it is the top level descriptor object.
* @param props The subproperties for this relation.
*/
public void update(Object desc, Properties props) {
Application app = ownType.getApplication();
if (desc instanceof Properties || parseDescriptor(desc, props)) {
// converted to internal foo.collection = Bar representation
String proto;
if (props.containsKey("collection")) {
proto = props.getProperty("collection");
virtual = !"_children".equalsIgnoreCase(propName);
reftype = COLLECTION;
} else if (props.containsKey("mountpoint")) {
proto = props.getProperty("mountpoint");
reftype = COLLECTION;
virtual = true;
this.prototype = proto;
} else if (props.containsKey("object")) {
proto = props.getProperty("object");
if (reftype != COMPLEX_REFERENCE) {
reftype = REFERENCE;
}
virtual = false;
} else {
throw new RuntimeException("Invalid property Mapping: " + desc);
}
otherType = app.getDbMapping(proto);
if (otherType == null) {
throw new RuntimeException("DbMapping for " + proto +
" not found from " + ownType.getTypeName());
}
// make sure the type we're referring to is up to date!
if (otherType.needsUpdate()) {
otherType.update();
}
}
readonly = "true".equalsIgnoreCase(props.getProperty("readonly"));
isPrivate = "true".equalsIgnoreCase(props.getProperty("private"));
// the following options only apply to object and collection relations
if ((reftype != PRIMITIVE) && (reftype != INVALID)) {
Vector newConstraints = new Vector();
parseOptions(newConstraints, props);
constraints = new Constraint[newConstraints.size()];
newConstraints.copyInto(constraints);
if (reftype == REFERENCE || reftype == COMPLEX_REFERENCE) {
if (constraints.length == 0) {
referencesPrimaryKey = true;
} else {
boolean rprim = false;
for (int i=0; i<constraints.length; i++) {
if (constraints[i].foreignKeyIsPrimary()) {
rprim = true;
break;
}
}
referencesPrimaryKey = rprim;
}
// check if this is a non-trivial reference
if (constraints.length > 1 || !usesPrimaryKey()) {
reftype = COMPLEX_REFERENCE;
} else {
reftype = REFERENCE;
}
}
if (reftype == COLLECTION) {
referencesPrimaryKey = (accessName == null) ||
accessName.equalsIgnoreCase(otherType.getIDField());
}
// if DbMapping for virtual nodes has already been created,
// update its subnode relation.
// FIXME: needs to be synchronized?
if (virtualMapping != null) {
virtualMapping.lastTypeChange = ownType.lastTypeChange;
virtualMapping.subRelation = getVirtualSubnodeRelation();
virtualMapping.propRelation = getVirtualPropertyRelation();
}
} else {
referencesPrimaryKey = false;
}
}
/**
* Converts old style foo = collection(Bar) mapping to new style
* foo.collection = Bar mappinng and returns true if a non-primitive mapping
* was encountered.
* @param value the value of the top level property mapping
* @param config the sub-map for this property mapping
* @return true if the value describes a valid, non-primitive property mapping
*/
protected boolean parseDescriptor(Object value, Map config) {
String desc = value instanceof String ? (String) value : null;
if (desc == null || "".equals(desc.trim())) {
if (propName != null) {
reftype = PRIMITIVE;
columnName = propName;
} else {
reftype = INVALID;
columnName = propName;
}
return false;
} else {
desc = desc.trim();
int open = desc.indexOf("(");
int close = desc.indexOf(")", open);
if (open > -1 && close > open) {
String ref = desc.substring(0, open).trim();
String proto = desc.substring(open + 1, close).trim();
if ("collection".equalsIgnoreCase(ref)) {
config.put("collection", proto);
} else if ("mountpoint".equalsIgnoreCase(ref)) {
config.put("mountpoint", proto);
} else if ("object".equalsIgnoreCase(ref)) {
config.put("object", proto);
} else {
throw new RuntimeException("Invalid property Mapping: " + desc);
}
return true;
} else {
virtual = false;
columnName = desc;
reftype = PRIMITIVE;
return false;
}
}
}
protected void parseOptions(Vector cnst, Properties props) {
String loading = props.getProperty("loadmode");
if (loading != null) {
loading = loading.trim();
if ("aggressive".equalsIgnoreCase(loading)) {
aggressiveLoading = true;
lazyLoading = false;
} else if ("lazy".equalsIgnoreCase(loading)) {
lazyLoading = true;
aggressiveLoading = false;
} else {
System.err.println("Unsupported loadmode property in " + ownType + ": " + loading);
aggressiveLoading = lazyLoading = false;
}
} else {
aggressiveLoading = lazyLoading = false;
}
String caching = props.getProperty("cachemode");
aggressiveCaching = (caching != null) &&
"aggressive".equalsIgnoreCase(caching.trim());
// get order property
order = props.getProperty("order");
if ((order != null) && (order.trim().length() == 0)) {
order = null;
}
// get the criteria(s) for updating this collection
updateCriteria = props.getProperty("updatecriteria");
// get the autosorting flag
autoSorted = "auto".equalsIgnoreCase(props.getProperty("sortmode"));
// get additional filter property
filter = props.getProperty("filter");
if (filter != null) {
if (filter.trim().length() == 0) {
filter = null;
filterFragments = filterPropertyRefs = null;
} else {
// parenthesise filter
Vector fragments = new Vector();
Vector propertyRefs = new Vector();
parsePropertyString(filter, fragments, propertyRefs);
// if no references where found, just use the filter string
// otherwise use the filter fragments and proeprty refs instead
if (propertyRefs.size() > 0) {
filterFragments = fragments;
filterPropertyRefs = propertyRefs;
} else {
filterFragments = filterPropertyRefs = null;
}
}
}
// get additional tables
additionalTables = props.getProperty("filter.additionalTables");
if (additionalTables != null) {
if (additionalTables.trim().length() == 0) {
additionalTables = null;
} else {
String ucTables = additionalTables.toUpperCase();
// create dependencies implied by additional tables
DbSource dbsource = otherType.getDbSource();
if (dbsource != null) {
String[] tables = StringUtils.split(ucTables, ", ");
for (int i=0; i<tables.length; i++) {
// Skip some join-related keyworks we might encounter here
if ("AS".equals(tables[i]) || "ON".equals(tables[i])) {
continue;
}
DbMapping dbmap = dbsource.getDbMapping(tables[i]);
if (dbmap != null) {
dbmap.addDependency(otherType);
}
}
}
// see wether the JOIN syntax is used. look for " join " with whitespaces on both sides
// and for "join " at the beginning:
additionalTablesJoined = (ucTables.indexOf(" JOIN ") != -1 ||
ucTables.startsWith("STRAIGHT_JOIN ") || ucTables.startsWith("JOIN "));
}
}
// get query hints
queryHints = props.getProperty("hints");
// get max size of collection
maxSize = getIntegerProperty("maxSize", props, 0);
if (maxSize == 0) {
// use limit as alias for maxSize
maxSize = getIntegerProperty("limit", props, 0);
}
offset = getIntegerProperty("offset", props, 0);
// get group by property
groupby = props.getProperty("group");
if (groupby != null && groupby.trim().length() == 0) {
groupby = null;
}
if (groupby != null) {
groupbyOrder = props.getProperty("group.order");
if (groupbyOrder != null && groupbyOrder.trim().length() == 0) {
groupbyOrder = null;
}
groupbyPrototype = props.getProperty("group.prototype");
if (groupbyPrototype != null && groupbyPrototype.trim().length() == 0) {
groupbyPrototype = null;
}
// aggressive loading and caching is not supported for groupby-nodes
// aggressiveLoading = aggressiveCaching = false;
}
// check if subnode condition should be applied for property relations
accessName = props.getProperty("accessname");
// parse contstraints
String local = props.getProperty("local");
String foreign = props.getProperty("foreign");
if (local != null && foreign != null) {
cnst.addElement(new Constraint(local, foreign, false));
columnName = local;
}
// parse additional contstraints from *.1 to *.9
for (int i = 1; i < 10; i++) {
local = props.getProperty("local."+i);
foreign = props.getProperty("foreign."+i);
if (local != null && foreign != null) {
cnst.addElement(new Constraint(local, foreign, false));
}
}
// parse constraints logic
if (cnst.size() > 1) {
String logic = props.getProperty("logicalOperator");
if ("and".equalsIgnoreCase(logic)) {
logicalOperator = AND;
} else if ("or".equalsIgnoreCase(logic)) {
logicalOperator = OR;
} else if ("xor".equalsIgnoreCase(logic)) {
logicalOperator = XOR;
} else {
logicalOperator = AND;
}
} else {
logicalOperator = AND;
}
}
private int getIntegerProperty(String name, Properties props, int defaultValue) {
Object value = props.get(name);
if (value instanceof Number) {
return ((Number) value).intValue();
} else if (value instanceof String) {
try {
return Integer.parseInt((String) value);
} catch (NumberFormatException nfx) {
ownType.getApplication().logError("Can't parse integer for property "
+ name + " from value " + value, nfx);
}
}
return defaultValue;
}
///////////////////////////////////////////////////////////////////////////////////////////
/**
* Get the configuration properties for this relation.
*/
public Map getConfig() {
return ownType.getSubProperties(propName + '.');
}
/**
* Does this relation describe a virtual (collection) node?
*/
public boolean isVirtual() {
return virtual;
}
/**
* Return the target type of this relation, or null if this is a primitive mapping.
*/
public DbMapping getTargetType() {
return otherType;
}
/**
* Get the reference type of this relation.
*/
public int getRefType() {
return reftype;
}
/**
* Tell if this relation represents a primitive (scalar) value mapping.
*/
public boolean isPrimitive() {
return reftype == PRIMITIVE;
}
/**
* Returns true if this Relation describes an object reference property
*/
public boolean isReference() {
return reftype == REFERENCE;
}
/**
* Returns true if this Relation describes either a primitive value
* or an object reference.
*/
public boolean isPrimitiveOrReference() {
return reftype == PRIMITIVE || reftype == REFERENCE;
}
/**
* Returns true if this Relation describes a collection.
* <b>NOTE:</b> this will return true both for collection objects
* (aka virtual nodes) and direct child object relations, so
* isVirtual() should be used to identify relations that define
* <i>collection properties</i>!
*/
public boolean isCollection() {
return reftype == COLLECTION;
}
/**
* Returns true if this Relation describes a complex object reference property
*/
public boolean isComplexReference() {
return reftype == COMPLEX_REFERENCE;
}
/**
* Tell wether the property described by this relation is to be handled as private, i.e.
* a change on it should not result in any changed object/collection relations.
*/
public boolean isPrivate() {
return isPrivate;
}
/**
* Check whether aggressive loading is set for this relation
*/
public boolean loadAggressively() {
return aggressiveLoading;
}
/**
* Returns the number of constraints for this relation.
*/
public int countConstraints() {
if (constraints == null)
return 0;
return constraints.length;
}
/**
* Returns true if the object represented by this Relation has to be
* created on demand at runtime by the NodeManager. This is true for:
*
* - collection (aka virtual) nodes
* - nodes accessed via accessname
* - group nodes
* - complex reference nodes
*/
public boolean createOnDemand() {
if (otherType == null) {
return false;
}
return virtual ||
(otherType.isRelational() && accessName != null) ||
(groupby != null) || isComplexReference();
}
/**
* Returns true if the object represented by this Relation has to be
* persisted in the internal db in order to be functional. This is true if
* the subnodes contained in this collection are stored in the embedded
* database. In this case, the collection itself must also be an ordinary
* object stored in the db, since a virtual collection would lose its
* its content after restarts.
*/
public boolean needsPersistence() {
if (!virtual) {
// ordinary object references always need to be persisted
return true;
}
// collections/mountpoints need to be persisted if the
// child object type is non-relational. Depending on
// whether prototype is null or not, we need to look at
// otherType itself or otherType's subnode mapping.
if (prototype == null) {
// an ordinary, unprototyped virtual node -
// otherType is the content type
return !otherType.isRelational();
} else {
// a prototyped virtual node or mountpoint -
// otherType is the virtual node type itself
DbMapping sub = otherType.getSubnodeMapping();
return sub != null && !sub.isRelational();
}
}
/**
* Return the prototype to be used for object reached by this relation
*/
public String getPrototype() {
return prototype;
}
/**
* Return the name of the local property this relation is defined for
*/
public String getPropName() {
return propName;
}
/**
*
*
* @param ct ...
*/
public void setColumnType(int ct) {
columnType = ct;
}
/**
*
*
* @return ...
*/
public int getColumnType() {
return columnType;
}
/**
* Get the group for a collection relation, if defined.
*
* @return the name of the column used to group child objects, if any.
*/
public String getGroup() {
return groupby;
}
/**
* Add a constraint to the current list of constraints
*/
protected void addConstraint(Constraint c) {
if (constraints == null) {
constraints = new Constraint[1];
constraints[0] = c;
} else {
Constraint[] nc = new Constraint[constraints.length + 1];
System.arraycopy(constraints, 0, nc, 0, constraints.length);
nc[nc.length - 1] = c;
constraints = nc;
}
}
/**
*
*
* @return true if the foreign key used for this relation is the
* other object's primary key.
*/
public boolean usesPrimaryKey() {
return referencesPrimaryKey;
}
/**
*
*
* @return ...
*/
public boolean hasAccessName() {
return accessName != null;
}
/**
*
*
* @return ...
*/
public String getAccessName() {
return accessName;
}
/**
*
*
* @return ...
*/
public Relation getSubnodeRelation() {
// return subnoderelation;
return null;
}
/**
* Return the local field name for updates.
*/
public String getDbField() {
return columnName;
}
/**
* This is taken from org.apache.tools.ant ProjectHelper.java
* distributed under the Apache Software License, Version 1.1
*
* Parses a string containing <code>${xxx}</code> style property
* references into two lists. The first list is a collection
* of text fragments, while the other is a set of string property names.
* <code>null</code> entries in the first list indicate a property
* reference from the second list.
*
* @param value Text to parse. Must not be <code>null</code>.
* @param fragments List to add text fragments to.
* Must not be <code>null</code>.
* @param propertyRefs List to add property names to.
* Must not be <code>null</code>.
*/
protected void parsePropertyString(String value, Vector fragments, Vector propertyRefs) {
int prev = 0;
int pos;
//search for the next instance of $ from the 'prev' position
while ((pos = value.indexOf("$", prev)) >= 0) {
//if there was any text before this, add it as a fragment
//TODO, this check could be modified to go if pos>prev;
//seems like this current version could stick empty strings
//into the list
if (pos > 0) {
fragments.addElement(value.substring(prev, pos));
}
//if we are at the end of the string, we tack on a $
//then move past it
if (pos == (value.length() - 1)) {
fragments.addElement("$");
prev = pos + 1;
} else if (value.charAt(pos + 1) != '{') {
//peek ahead to see if the next char is a property or not
//not a property: insert the char as a literal
/*
fragments.addElement(value.substring(pos + 1, pos + 2));
prev = pos + 2;
*/
if (value.charAt(pos + 1) == '$') {
//backwards compatibility two $ map to one mode
fragments.addElement("$");
prev = pos + 2;
} else {
//new behaviour: $X maps to $X for all values of X!='$'
fragments.addElement(value.substring(pos, pos + 2));
prev = pos + 2;
}
} else {
//property found, extract its name or bail on a typo
int endName = value.indexOf('}', pos);
if (endName < 0) {
throw new RuntimeException("Syntax error in property: "
+ value);
}
String propertyName = value.substring(pos + 2, endName);
fragments.addElement(null);
propertyRefs.addElement(propertyName);
prev = endName + 1;
}
}
//no more $ signs found
//if there is any tail to the file, append it
if (prev < value.length()) {
fragments.addElement(value.substring(prev));
}
}
/**
* get a DbMapping to use for virtual aka collection nodes.
*/
public DbMapping getVirtualMapping() {
// return null unless this relation describes a virtual/collection node.
if (!virtual) {
return null;
}
// create a synthetic DbMapping that describes how to fetch the
// collection's child objects.
if (virtualMapping == null) {
// if the collection node is prototyped (a mountpoint), create
// a virtual sub-mapping from the app's DbMapping for that prototype
if (prototype != null) {
virtualMapping = new DbMapping(ownType.app, prototype);
} else {
virtualMapping = new DbMapping(ownType.app, null);
virtualMapping.subRelation = getVirtualSubnodeRelation();
virtualMapping.propRelation = getVirtualPropertyRelation();
}
}
virtualMapping.lastTypeChange = ownType.lastTypeChange;
return virtualMapping;
}
/**
* Return the db mapping for a propery relation.
* @return the target mapping of this property relation
*/
public DbMapping getPropertyMapping() {
// if this is an untyped virtual node, it doesn't have a dbmapping
if (!virtual || prototype != null) {
return otherType;
}
return null;
}
/**
* Return a Relation that defines the subnodes of a virtual node.
*/
Relation getVirtualSubnodeRelation() {
if (!virtual) {
throw new RuntimeException("getVirtualSubnodeRelation called on non-virtual relation");
}
Relation vr = new Relation(this);
vr.groupby = groupby;
vr.groupbyOrder = groupbyOrder;
vr.groupbyPrototype = groupbyPrototype;
return vr;
}
/**
* Return a Relation that defines the properties of a virtual node.
*/
Relation getVirtualPropertyRelation() {
if (!virtual) {
throw new RuntimeException("getVirtualPropertyRelation called on non-virtual relation");
}
Relation vr = new Relation(this);
vr.groupby = groupby;
vr.groupbyOrder = groupbyOrder;
vr.groupbyPrototype = groupbyPrototype;
return vr;
}
/**
* Return a Relation that defines the subnodes of a group-by node.
*/
Relation getGroupbySubnodeRelation() {
if (groupby == null) {
throw new RuntimeException("getGroupbySubnodeRelation called on non-group-by relation");
}
Relation vr = new Relation(this);
vr.prototype = groupbyPrototype;
vr.addConstraint(new Constraint(null, groupby, true));
return vr;
}
/**
* Return a Relation that defines the properties of a group-by node.
*/
Relation getGroupbyPropertyRelation() {
if (groupby == null) {
throw new RuntimeException("getGroupbyPropertyRelation called on non-group-by relation");
}
Relation vr = new Relation(this);
vr.prototype = groupbyPrototype;
vr.addConstraint(new Constraint(null, groupby, true));
return vr;
}
public StringBuffer getIdSelect() {
StringBuffer buf = new StringBuffer("SELECT ");
if (queryHints != null) {
buf.append(queryHints).append(" ");
}
String table = otherType.getTableName();
String idfield;
if (groupby == null) {
idfield = otherType.getIDField();
} else {
idfield = groupby;
buf.append("DISTINCT ");
}
if (idfield.indexOf('(') == -1 && idfield.indexOf('.') == -1) {
buf.append(table).append('.');
}
buf.append(idfield).append(" FROM ").append(table);
appendAdditionalTables(buf);
return buf;
}
public StringBuffer getCountSelect() {
StringBuffer buf = new StringBuffer("SELECT ");
if (otherType.isOracle() && maxSize > 0) {
buf.append("* FROM ");
} else {
if (groupby == null) {
buf.append("count(*) FROM ");
} else {
buf.append("count(DISTINCT ").append(groupby).append(") FROM ");
}
}
buf.append(otherType.getTableName());
appendAdditionalTables(buf);
return buf;
}
public StringBuffer getNamesSelect() {
// if we do a groupby query (creating an intermediate layer of groupby nodes),
// retrieve the value of that field instead of the primary key
String namefield = (groupby == null) ? accessName : groupby;
String table = otherType.getTableName();
StringBuffer buf = new StringBuffer("SELECT ");
buf.append(namefield).append(" FROM ").append(table);
appendAdditionalTables(buf);
return buf;
}
/**
* Build the second half of an SQL select statement according to this relation
* and a local object.
*/
public void buildQuery(StringBuffer q, Node home, boolean useOrder, boolean isCount)
throws SQLException, ClassNotFoundException {
buildQuery(q, home, otherType, null, useOrder, isCount);
}
/**
* Build the second half of an SQL select statement according to this relation
* and a local object.
*/
public void buildQuery(StringBuffer q, Node home, DbMapping otherDbm, String kstr,
boolean useOrder, boolean isCount)
throws SQLException, ClassNotFoundException {
String prefix = " WHERE ";
Node nonvirtual = home.getNonVirtualParent();
if (kstr != null && !isComplexReference()) {
q.append(prefix);
String accessColumn = (accessName == null) ?
otherDbm.getIDField() : accessName;
otherDbm.appendCondition(q, accessColumn, kstr);
prefix = " AND ";
}
// render the constraints and filter
renderConstraints(q, home, nonvirtual, otherDbm, prefix);
// add joined fetch constraints
ownType.addJoinConstraints(q, prefix);
// add group and order clauses
if (groupby != null) {
if (useOrder && (groupbyOrder != null)) {
q.append(" ORDER BY ").append(groupbyOrder);
}
} else if (useOrder && (order != null)) {
q.append(" ORDER BY ").append(order);
}
// apply limit and offset, but not if the query is for a single object
if (maxSize > 0 && kstr == null) {
if (otherType.isOracle()) {
// see http://www.oracle.com/technology/oramag/oracle/06-sep/o56asktom.html
String selectItem = isCount ? "count(*)" : "*";
if (offset > 0) {
q.insert(0, "SELECT " + selectItem + " FROM ( SELECT /*+ FIRST_ROWS(n) */ a.*, ROWNUM rnum FROM (");
q.append(") a WHERE ROWNUM <= ").append(offset + maxSize).append(") WHERE rnum > ").append(offset);
} else {
q.insert(0, "SELECT /*+ FIRST_ROWS(n) */ " + selectItem + " FROM (");
q.append(") WHERE ROWNUM <= ").append(maxSize);
}
} else {
q.append(" LIMIT ").append(maxSize);
if (offset > 0) {
q.append(" OFFSET ").append(offset);
}
}
}
}
protected void appendAdditionalTables(StringBuffer q) {
if (additionalTables != null) {
q.append(additionalTablesJoined ? ' ' : ',');
q.append(additionalTables);
}
}
/**
* Build the filter.
*/
protected void appendFilter(StringBuffer q, INode nonvirtual, String prefix) {
q.append(prefix);
q.append('(');
if (filterFragments == null) {
q.append(filter);
} else {
Enumeration i = filterFragments.elements();
Enumeration j = filterPropertyRefs.elements();
while (i.hasMoreElements()) {
String fragment = (String) i.nextElement();
if (fragment == null) {
// begin column version
String columnName = (String) j.nextElement();
Object value = null;
if (columnName != null) {
DbMapping dbmap = nonvirtual.getDbMapping();
String propertyName = dbmap.columnNameToProperty(columnName);
if (propertyName == null)
propertyName = columnName;
IProperty property = nonvirtual.get(propertyName);
if (property != null) {
value = property.getStringValue();
}
if (value == null) {
if (columnName.equalsIgnoreCase(dbmap.getIDField())) {
value = nonvirtual.getID();
} else if (columnName.equalsIgnoreCase(dbmap.getNameField())) {
value = nonvirtual.getName();
} else if (columnName.equalsIgnoreCase(dbmap.getPrototypeField())) {
value = dbmap.getExtensionId();
}
}
}
// end column version
if (value != null) {
q.append(DbMapping.escapeString(value.toString()));
} else {
q.append("NULL");
}
} else {
q.append(fragment);
}
}
}
q.append(')');
}
/**
* Render contraints and filter conditions to an SQL query string buffer.
*
* @param q the query string
* @param home our home node
* @param prefix the prefix to use to append to the existing query (e.g. " AND ")
*
* @throws SQLException sql related exception
* @throws ClassNotFoundException driver class not found
*/
public void renderConstraints(StringBuffer q, Node home, String prefix)
throws SQLException, ClassNotFoundException {
renderConstraints(q, home, home.getNonVirtualParent(), otherType, prefix);
}
/**
* Render contraints and filter conditions to an SQL query string buffer.
*
* @param q the query string
* @param home our home node
* @param nonvirtual our non-virtual home nod
* @param otherDbm the DbMapping of the remote Node
* @param prefix the prefix to use to append to the existing query (e.g. " AND ")
*
* @throws SQLException sql related exception
* @throws ClassNotFoundException driver class not found
*/
public void renderConstraints(StringBuffer q, Node home, Node nonvirtual,
DbMapping otherDbm, String prefix)
throws SQLException, ClassNotFoundException {
if (constraints.length > 1 && logicalOperator != AND) {
q.append(prefix);
q.append("(");
prefix = "";
}
for (int i = 0; i < constraints.length; i++) {
if (constraints[i].foreignKeyIsPrototype()) {
// if foreign key is $prototype we already have this constraint
// covered by doing the select on the proper table
continue;
}
q.append(prefix);
constraints[i].addToQuery(q, home, nonvirtual, otherDbm);
prefix = logicalOperator;
}
if (constraints.length > 1 && logicalOperator != AND) {
q.append(")");
prefix = " AND ";
}
// also take the prototype into consideration if someone
// specifies an extension of an prototype inside the brakets of
// a type.properties's collection, only nodes having this proto
// sould appear inside the collection
if (otherDbm.inheritsStorage()) {
String protoField = otherDbm.getPrototypeField();
String[] extensions = otherDbm.getExtensions();
// extensions should never be null for extension- and
// extended prototypes. nevertheless we check it here
if (extensions != null && protoField != null) {
q.append(prefix);
otherDbm.appendCondition(q, protoField, extensions);
prefix = " AND ";
}
}
if (filter != null) {
appendFilter(q, nonvirtual, prefix);
}
}
/**
* Render the constraints for this relation for use within
* a left outer join select statement for the base object.
*
* @param select the string buffer to write to
* @param isOracle create Oracle pre-9 style left outer join
*/
public void renderJoinConstraints(StringBuffer select, boolean isOracle) {
for (int i = 0; i < constraints.length; i++) {
select.append(ownType.getTableName());
select.append(".");
select.append(constraints[i].localKey);
select.append(" = ");
select.append(JOIN_PREFIX);
select.append(propName);
select.append(".");
select.append(constraints[i].foreignKey);
if (isOracle) {
// create old oracle style join - see
// http://www.praetoriate.com/oracle_tips_outer_joins.htm
select.append("(+)");
}
if (i == constraints.length-1) {
select.append(" ");
} else {
select.append(" AND ");
}
}
}
/**
* Get the order section to use for this relation
*/
public String getOrder() {
if (groupby != null) {
return groupbyOrder;
} else {
return order;
}
}
/**
* Tell wether the property described by this relation is to be handled
* as readonly/write protected.
*/
public boolean isReadonly() {
return readonly;
}
/**
* Get a copy of this relation.
* @return a clone of this relation
*/
public Relation getClone() {
Relation rel = new Relation(this);
rel.prototype = prototype;
rel.groupby = groupby;
rel.groupbyPrototype = groupbyPrototype;
rel.groupbyOrder = groupbyOrder;
return rel;
}
/**
* Check if the child node fullfills the constraints defined by this relation.
* FIXME: This always returns false if the relation has a filter value set,
* since we can't determine if the filter constraints are met without
* querying the database.
*
* @param parent the parent object - may be a virtual or group node
* @param child the child object
* @return true if all constraints are met
*/
public boolean checkConstraints(Node parent, Node child) {
// problem: if a filter property is defined for this relation,
// i.e. a piece of static SQL-where clause, we'd have to evaluate it
// in order to check the constraints. Because of this, if a filter
// is defined, we return false as soon as the modified-time is greater
// than the create-time of the child, i.e. if the child node has been
// modified since it was first fetched from the db.
if (filter != null && child.lastModified() > child.created()) {
return false;
}
// counter for constraints and satisfied constraints
int count = 0;
int satisfied = 0;
INode nonvirtual = parent.getNonVirtualParent();
DbMapping otherDbm = child.getDbMapping();
if (otherDbm == null) {
otherDbm = otherType;
}
for (int i = 0; i < constraints.length; i++) {
Constraint cnst = constraints[i];
String propname = cnst.foreignProperty(otherDbm);
if (propname != null) {
INode home = cnst.isGroupby ? parent
: nonvirtual;
String value = null;
if (cnst.localKeyIsPrimary(home.getDbMapping())) {
value = home.getID();
} else if (cnst.localKeyIsPrototype()) {
value = home.getDbMapping().getStorageTypeName();
} else if (ownType.isRelational()) {
value = home.getString(cnst.localProperty());
} else {
value = home.getString(cnst.localKey);
}
count++;
if (value != null && value.equals(child.getString(propname))) {
satisfied++;
}
}
}
// check if enough constraints are met depending on logical operator
if (logicalOperator == OR) {
return satisfied > 0;
} else if (logicalOperator == XOR) {
return satisfied == 1;
} else {
return satisfied == count;
}
}
/**
* Make sure that the child node fullfills the constraints defined by this relation by setting the
* appropriate properties
*/
public void setConstraints(Node parent, Node child) {
// if logical operator is OR or XOR we just return because we
// wouldn't know what to do anyway
if (logicalOperator != AND) {
return;
}
Node home = parent.getNonVirtualParent();
for (int i = 0; i < constraints.length; i++) {
Constraint cnst = constraints[i];
// don't set groupby constraints since we don't know if the
// parent node is the base node or a group node
if (cnst.isGroupby) {
continue;
}
// check if we update the local or the other object, depending on
// whether the primary key of either side is used.
boolean foreignIsPrimary = cnst.foreignKeyIsPrimary();
if (foreignIsPrimary || cnst.foreignKeyIsPrototype()) {
String localProp = cnst.localProperty();
if (localProp == null) {
throw new RuntimeException("Error: column " + cnst.localKey +
" must be mapped in order to be used as constraint in " +
Relation.this);
} else if (foreignIsPrimary && child.getState() == Node.TRANSIENT) {
throw new RuntimeException(propName + " set to transient object, " +
"can't derive persistent ID for " + localProp);
} else {
String value = foreignIsPrimary ?
child.getID() : child.getDbMapping().getStorageTypeName();
home.setString(localProp, value);
}
continue;
}
DbMapping otherDbm = child.getDbMapping();
if (otherDbm == null) {
otherDbm = otherType;
}
Relation crel = otherDbm.columnNameToRelation(cnst.foreignKey);
if (crel != null) {
if (cnst.localKeyIsPrimary(home.getDbMapping())) {
// only set node if property in child object is defined as reference.
if (crel.reftype == REFERENCE) {
INode currentValue = child.getNode(crel.propName);
// we set the backwards reference iff the reference is currently unset, if
// is set to a transient object, or if the new target is not transient. This
// prevents us from overwriting a persistent refererence with a transient one,
// which would most probably not be what we want.
if ((currentValue == null) ||
((currentValue != home) &&
((currentValue.getState() == Node.TRANSIENT) ||
(home.getState() != Node.TRANSIENT)))) try {
child.setNode(crel.propName, home);
} catch (Exception ignore) {
// in some cases, getNonVirtualParent() doesn't work
// correctly for transient nodes, so this may fail.
}
} else if (crel.reftype == PRIMITIVE) {
if (home.getState() == Node.TRANSIENT) {
throw new RuntimeException("Object is transient, can't derive persistent ID for " + crel);
}
child.setString(crel.propName, home.getID());
}
} else if (crel.reftype == PRIMITIVE) {
if (cnst.localKeyIsPrototype()) {
child.setString(crel.propName, home.getDbMapping().getStorageTypeName());
} else {
Property prop = home.getProperty(cnst.localProperty());
if (prop != null) {
child.set(crel.propName, prop.getValue(), prop.getType());
} else {
prop = child.getProperty(cnst.foreignProperty(child.getDbMapping()));
if (prop != null) {
home.set(cnst.localProperty(), prop.getValue(), prop.getType());
}
}
}
}
}
}
}
/**
* Unset the constraints that link two objects together.
*/
public void unsetConstraints(Node parent, INode child) {
Node home = parent.getNonVirtualParent();
for (int i = 0; i < constraints.length; i++) {
Constraint cnst = constraints[i];
// don't set groupby constraints since we don't know if the
// parent node is the base node or a group node
if (cnst.isGroupby) {
continue;
}
// check if we update the local or the other object, depending on
// whether the primary key of either side is used.
if (cnst.foreignKeyIsPrimary() || cnst.foreignKeyIsPrototype()) {
String localProp = cnst.localProperty();
if (localProp != null) {
home.setString(localProp, null);
}
continue;
}
DbMapping otherDbm = child.getDbMapping();
if (otherDbm == null) {
otherDbm = otherType;
}
Relation crel = otherDbm.columnNameToRelation(cnst.foreignKey);
if (crel != null) {
if (cnst.localKeyIsPrimary(home.getDbMapping())) {
// only set node if property in child object is defined as reference.
if (crel.reftype == REFERENCE) {
INode currentValue = child.getNode(crel.propName);
if ((currentValue == home)) {
child.setString(crel.propName, null);
}
} else if (crel.reftype == PRIMITIVE) {
child.setString(crel.propName, null);
}
} else if (crel.reftype == PRIMITIVE) {
child.setString(crel.propName, null);
}
}
}
}
/**
* Returns a map containing the key/value pairs for a specific Node
*/
public Map getKeyParts(INode home) {
Map map = new HashMap();
for (int i=0; i<constraints.length; i++) {
Constraint cnst = constraints[i];
if (cnst.localKeyIsPrimary(ownType)) {
map.put(cnst.foreignKey, home.getID());
} else if (cnst.localKeyIsPrototype()) {
map.put(cnst.foreignKey, home.getDbMapping().getStorageTypeName());
} else {
map.put(cnst.foreignKey, home.getString(cnst.localProperty()));
}
}
// add filter as pseudo-constraint
if (filter != null) {
map.put("__filter__", filter);
}
return map;
}
/**
*
*
* @return ...
*/
public String toString() {
String c = "";
String spacer = "";
if (constraints != null) {
c = " constraints: ";
for (int i = 0; i < constraints.length; i++) {
c += spacer;
c += constraints[i].toString();
spacer = ", ";
}
}
String target = otherType == null ? columnName : otherType.toString();
return "Relation " + ownType+"."+propName + " -> " + target + c;
}
/**
* The Constraint class represents a part of the where clause in the query used to
* establish a relation between database mapped objects.
*/
class Constraint {
String localKey;
String foreignKey;
boolean isGroupby;
Constraint(String local, String foreign, boolean groupby) {
localKey = local;
foreignKey = foreign;
isGroupby = groupby;
}
public void addToQuery(StringBuffer q, INode home, INode nonvirtual, DbMapping otherDbm)
throws SQLException, ClassNotFoundException {
String local;
INode ref = isGroupby ? home : nonvirtual;
if (localKeyIsPrimary(ref.getDbMapping())) {
local = ref.getID();
} else if (localKeyIsPrototype()) {
local = ref.getDbMapping().getStorageTypeName();
} else {
String homeprop = ownType.columnNameToProperty(localKey);
if (homeprop == null) {
throw new SQLException("Invalid local name '" + localKey +
"' on " + ownType);
}
local = ref.getString(homeprop);
}
String columnName;
if (foreignKeyIsPrimary()) {
columnName = otherDbm.getIDField();
} else {
columnName = foreignKey;
}
otherDbm.appendCondition(q, columnName, local);
}
public boolean foreignKeyIsPrimary() {
return (foreignKey == null) ||
"$id".equalsIgnoreCase(foreignKey) ||
foreignKey.equalsIgnoreCase(otherType.getIDField());
}
public boolean foreignKeyIsPrototype() {
return "$prototype".equalsIgnoreCase(foreignKey);
}
public boolean localKeyIsPrimary(DbMapping homeMapping) {
return (homeMapping == null) || (localKey == null) ||
"$id".equalsIgnoreCase(localKey) ||
localKey.equalsIgnoreCase(homeMapping.getIDField());
}
public boolean localKeyIsPrototype() {
return "$prototype".equalsIgnoreCase(localKey);
}
public String foreignProperty(DbMapping otherDbm) {
if (otherDbm.isRelational())
return otherDbm.columnNameToProperty(foreignKey);
return foreignKey;
}
public String localProperty() {
if (ownType.isRelational())
return ownType.columnNameToProperty(localKey);
return localKey;
}
public String toString() {
return localKey + "=" + otherType.getTypeName() + "." + foreignKey;
}
}
}