/* * #! * Ontopia Engine * #- * Copyright (C) 2001 - 2013 The Ontopia Project * #- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * !# */ package net.ontopia.persistence.query.sql; import java.sql.Types; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import net.ontopia.persistence.proxy.ClassInfoIF; import net.ontopia.persistence.proxy.FieldInfoIF; import net.ontopia.persistence.proxy.FieldUtils; import net.ontopia.persistence.proxy.ObjectAccessIF; import net.ontopia.persistence.proxy.ObjectRelationalMappingIF; import net.ontopia.persistence.query.jdo.JDOAggregateIF; import net.ontopia.persistence.query.jdo.JDOAnd; import net.ontopia.persistence.query.jdo.JDOBoolean; import net.ontopia.persistence.query.jdo.JDOCollection; import net.ontopia.persistence.query.jdo.JDOContains; import net.ontopia.persistence.query.jdo.JDOEndsWith; import net.ontopia.persistence.query.jdo.JDOEquals; import net.ontopia.persistence.query.jdo.JDOExpressionIF; import net.ontopia.persistence.query.jdo.JDOField; import net.ontopia.persistence.query.jdo.JDOFunction; import net.ontopia.persistence.query.jdo.JDOIsEmpty; import net.ontopia.persistence.query.jdo.JDOLike; import net.ontopia.persistence.query.jdo.JDONativeValue; import net.ontopia.persistence.query.jdo.JDONot; import net.ontopia.persistence.query.jdo.JDONotEquals; import net.ontopia.persistence.query.jdo.JDOObject; import net.ontopia.persistence.query.jdo.JDOOr; import net.ontopia.persistence.query.jdo.JDOOrderBy; import net.ontopia.persistence.query.jdo.JDOParameter; import net.ontopia.persistence.query.jdo.JDOPrimitive; import net.ontopia.persistence.query.jdo.JDOQuery; import net.ontopia.persistence.query.jdo.JDOSetOperation; import net.ontopia.persistence.query.jdo.JDOStartsWith; import net.ontopia.persistence.query.jdo.JDOString; import net.ontopia.persistence.query.jdo.JDOValueExpression; import net.ontopia.persistence.query.jdo.JDOValueIF; import net.ontopia.persistence.query.jdo.JDOVariable; import net.ontopia.utils.OntopiaRuntimeException; /** * INTERNAL: Class used to build SQL queries from JDO queries. */ public class SQLBuilder { protected boolean debug; protected ObjectRelationalMappingIF mapping; public SQLBuilder(ObjectRelationalMappingIF mapping) { this(mapping, false); } public SQLBuilder(ObjectRelationalMappingIF mapping, boolean debug) { this.mapping = mapping; this.debug = debug; } /** * INTERNAL: Class used to hold information collected after having * analyzed the SQL filter. */ class BuildInfo { // Object access protected ObjectAccessIF oaccess; // The JDO query protected JDOQuery jdoquery; // The SQL query protected SQLQuery sqlquery; // Mapping between table aliases and table names protected Map<String, SQLTable> tables = new HashMap<String, SQLTable>(); // { alias : SQLTable } protected Map<JDOValueIF, String> valiases = new HashMap<JDOValueIF, String>(); // { JDOVariable|JDOParameter : alias } protected int tblcount; // Mapping between values of non-identifiable type and their field infos. protected Map<JDOValueIF, FieldInfoIF> nfvals = new HashMap<JDOValueIF, FieldInfoIF>(); // { JDOVariable|JDOParameter : FieldInfoIF } // Values of non-identifiable types and their corresponding tables protected Map<JDOValueIF, String> ntvals = new HashMap<JDOValueIF, String>(); // { JDOVariable|JDOParameter : String tblname } SQLTable createNamedValueTable(JDOValueIF value, List expressions) { String valname; String prefix; switch (value.getType()) { case JDOValueIF.VARIABLE: { valname = ((JDOVariable)value).getName(); prefix = "V"; break; } case JDOValueIF.PARAMETER: { valname = ((JDOParameter)value).getName(); prefix = "P"; break; } default: throw new OntopiaRuntimeException("Non-supported named value: '" + value + "'"); } // Get or create value alias String alias = (debug ? valname : valiases.get(value)); if (alias == null) { while (true) { alias = prefix + (tblcount++); if (tables.containsKey(alias)) continue; valiases.put(value, alias); break; } } // Check to see if value table already has been registered if (tables.containsKey(alias)) { // Return existing table return tables.get(alias); } else { // Value is of non-identifiable type if (nfvals.containsKey(value)) { // Lookup field info FieldInfoIF finfo = nfvals.get(value); // Create table information // BUG: null pointer exception (tblname==null) thrown on // (O.locator.address = V). This is caused by the fact that // address is a primitive field and its parent is an // aggregate. Need to propagate the table all the way down. String tblname = finfo.getTable(); if (tblname == null) tblname = ntvals.get(value); if (tblname == null) throw new OntopiaRuntimeException("Not able to figure out table for value: '" + value + "'"); SQLTable table = new SQLTable(tblname, alias); tables.put(table.getAlias(), table); // ISSUE: do we need to add parameter expression in this // case? Probably not since it is not the parameter's own // table, but rather the adjacent. return table; } // Value is of identifiable type else { // Get value type information Class valtype = getValueType(value, this); ClassInfoIF cinfo = mapping.getClassInfo(valtype); // Create table information SQLTable table = new SQLTable(cinfo.getMasterTable(), alias); tables.put(table.getAlias(), table); // Bind parameter value to parameter table if (value.getType() == JDOValueIF.PARAMETER) { // Get hold of parameter identity field info FieldInfoIF finfo = cinfo.getIdentityFieldInfo(); // Create expression that ties the parameter value with the parameter table key SQLParameter sqlparam = new SQLParameter(valname, finfo.getColumnCount()); sqlparam.setValueType(valtype); sqlparam.setFieldHandler(finfo); expressions.add(new SQLEquals(new SQLColumns(table, finfo.getValueColumns()), sqlparam)); } return table; } } } String createTableAlias(String prefix) { if (!debug) return prefix + (tblcount++); // Create new alias String alias = prefix + (tblcount++); // Check if alias collides with variable name or existing table. if (jdoquery.hasVariableName(alias) || tables.containsKey(alias)) // FIXME: May not be necessary to do this when not in debug mode. return createTableAlias(prefix); else return alias; } // ----------------------------------------------------------------------------- // Pre-build JDO query analysis // ----------------------------------------------------------------------------- void analyze() { if (jdoquery == null) throw new OntopiaRuntimeException("JDO query not registered with SQLbuilder build info."); // TASK: We're trying to collect field info for variables of // non-identifiable type (aggregate or primitive). We do this by // matching them up with other field infos that they are used // together with. // Start analysis with JDO filter JDOExpressionIF filter = jdoquery.getFilter(); if (filter != null) analyzeExpression(filter); } protected void analyzeExpression(JDOExpressionIF jdoexpr) { // Check expression type and delegate to appropriate analyze method. switch (jdoexpr.getType()) { // SIMPLE EXPRESSIONS case JDOExpressionIF.EQUALS: // Compare left and right values JDOEquals eq = (JDOEquals)jdoexpr; analyzeCompatible(eq.getLeft(), eq.getRight()); break; case JDOExpressionIF.CONTAINS: JDOContains contains = (JDOContains)jdoexpr; analyzeCompatible(contains.getLeft(), contains.getRight()); break; case JDOExpressionIF.NOT_EQUALS: JDONotEquals neq = (JDONotEquals)jdoexpr; analyzeCompatible(neq.getLeft(), neq.getRight()); break; case JDOExpressionIF.IS_EMPTY: // Nothing to compare with break; case JDOExpressionIF.STARTS_WITH: JDOStartsWith swith = (JDOStartsWith)jdoexpr; analyzeCompatible(swith.getLeft(), swith.getRight()); break; case JDOExpressionIF.ENDS_WITH: JDOEndsWith ewith = (JDOEndsWith)jdoexpr; analyzeCompatible(ewith.getLeft(), ewith.getRight()); break; case JDOExpressionIF.LIKE: JDOLike like = (JDOLike)jdoexpr; analyzeCompatible(like.getLeft(), like.getRight()); break; // LOGICAL EXPRESSIONS case JDOExpressionIF.AND: // Analyze nested expression analyzeExpression(((JDOAnd)jdoexpr).getExpressions()); break; case JDOExpressionIF.OR: analyzeExpression(((JDOOr)jdoexpr).getExpressions()); break; case JDOExpressionIF.NOT: analyzeExpression(((JDONot)jdoexpr).getExpression()); break; case JDOExpressionIF.BOOLEAN: // no-op break; case JDOExpressionIF.VALUE_EXPRESSION: { // TODO: should verify that expression is boolean // Make sure that comparison functions get analyzed properly JDOValueIF value = ((JDOValueExpression)jdoexpr).getValue(); if (value.getType() == JDOValueIF.FUNCTION) { JDOFunction func = (JDOFunction)value; String fname = func.getName(); if (fname.equals(">") || fname.equals(">=") || fname.equals("<") || fname.equals("<=") || fname.equals("substring")) { JDOValueIF[] args = func.getArguments(); analyzeCompatible(args[0], args[1]); } } break; } // SET OPERATIONS case JDOExpressionIF.SET_OPERATION: break; default: throw new OntopiaRuntimeException("Invalid expression: '" + jdoexpr + "'"); } } protected void analyzeExpression(JDOExpressionIF[] exprs) { // Loop over JDO expressions and analyze them individually for (int i=0; i < exprs.length; i++) { analyzeExpression(exprs[i]); } } //! protected void analyzeCompatible(JDOValueIF[] values) { //! if (values.length < 2) return; //! JDOValueIF pv = values[0]; //! for (int i=1; i < values.length; i++) { //! analyzeCompatible(pv, values[i]); //! pv = values[i]; //! } //! } protected JDOValueIF extractValue(JDOValueIF jdovalue) { // Extract variable from value switch (jdovalue.getType()) { case JDOValueIF.FIELD: case JDOValueIF.VARIABLE: case JDOValueIF.PARAMETER: case JDOValueIF.PRIMITIVE: case JDOValueIF.OBJECT: case JDOValueIF.STRING: case JDOValueIF.COLLECTION: case JDOValueIF.NULL: return (JDOValueIF)jdovalue; case JDOValueIF.FUNCTION: { return ((JDOFunction)jdovalue).getArguments()[0]; } case JDOValueIF.NATIVE_VALUE: return ((JDONativeValue)jdovalue).getRoot(); default: throw new OntopiaRuntimeException("Cannot extract root from unknown JDOValueIF: " + jdovalue); } } protected JDOValueIF extractRootValue(JDOValueIF jdovalue) { // Extract variable from value switch (jdovalue.getType()) { case JDOValueIF.FIELD: return extractRootValue(((JDOField)jdovalue).getRoot()); case JDOValueIF.VARIABLE: case JDOValueIF.PARAMETER: case JDOValueIF.PRIMITIVE: case JDOValueIF.OBJECT: case JDOValueIF.STRING: case JDOValueIF.COLLECTION: case JDOValueIF.NULL: return (JDOValueIF)jdovalue; case JDOValueIF.FUNCTION: { //! return (JDOValueIF)jdovalue; //! System.out.println("FOO: " + Arrays.asList(((JDOFunction)jdovalue).getArguments())); //! System.out.println("BAR: " + extractRootValue(((JDOFunction)jdovalue).getArguments()[0])); return extractRootValue(((JDOFunction)jdovalue).getArguments()[0]); } case JDOValueIF.NATIVE_VALUE: return extractRootValue(((JDONativeValue)jdovalue).getRoot()); default: throw new OntopiaRuntimeException("Cannot extract root from unknown JDOValueIF: " + jdovalue); } } protected void analyzeCompatible(JDOValueIF value1, JDOValueIF value2) { // TASK: We're trying to collect field info for variables of // aggregate[1] or alias type. // extract leaf values value1 = extractValue(value1); value2 = extractValue(value2); // extract root values JDOValueIF rvalue1 = extractRootValue(value1); JDOValueIF rvalue2 = extractRootValue(value2); //! System.out.println("\nROOT: " + value1 + "/" + rvalue1 + " " + value2 + "/" + rvalue2); // Return if a value does not reference a root value or they're // referencing the same value. if (rvalue1 == null || rvalue2 == null || rvalue1 == rvalue2) return; // Figure out if value types are identifiable boolean identifiable1 = isIdentifiableValueType(rvalue1, this); boolean identifiable2 = isIdentifiableValueType(rvalue2, this); // Ignore analysis if both are identifiable. int rvtype1 = rvalue1.getType(); int rvtype2 = rvalue2.getType(); //! System.out.println("1: " + (rvalue1 == value1) + " " + //! getIdentifiableValueType(rvalue1, this) + " " + getValueType(rvalue1, this)); //! System.out.println("2: " + (rvalue2 == value2) + " " //! + getIdentifiableValueType(rvalue2, this) + " " + getValueType(rvalue2, this)); if (identifiable1 && !identifiable2 && (rvtype2 == JDOValueIF.VARIABLE || rvtype2 == JDOValueIF.PARAMETER)) analyzeMatchingNonIdentifiableValue(rvalue2, value1); else if (identifiable2 && !identifiable1 && (rvtype1 == JDOValueIF.VARIABLE || rvtype1 == JDOValueIF.PARAMETER)) analyzeMatchingNonIdentifiableValue(rvalue1, value2); else if (!identifiable1 && !identifiable2) { // figure out table if value 1 FieldInfoIF finfo1 = getFieldInfo(value1, this); if (finfo1 == null) finfo1 = nfvals.get(value1); String table1 = (finfo1 != null && finfo1.getTable() != null ? finfo1.getTable() : null); // figure out table if value 2 FieldInfoIF finfo2 = getFieldInfo(value2, this); if (finfo2 == null) finfo2 = nfvals.get(value2); String table2 = (finfo2 != null && finfo2.getTable() != null ? finfo2.getTable() : null); // Analyze left value if (!nfvals.containsKey(value1)) { // Figure out field info if (finfo1 != null) { nfvals.put(value1, finfo1); } else if (finfo2 != null) { nfvals.put(value1, finfo2); } // Figure out table name if (table1 != null) { ntvals.put(value1, table1); } else if (table2 != null) { ntvals.put(value1, table2); } else { if (nfvals.containsKey(rvalue1)) { FieldInfoIF finfo = nfvals.get(rvalue1); if (finfo.getTable() != null) ntvals.put(value1, finfo.getTable()); } if (!ntvals.containsKey(value1) && nfvals.containsKey(rvalue2)) { FieldInfoIF finfo = nfvals.get(rvalue2); if (finfo.getTable() != null) ntvals.put(value1, finfo.getTable()); } } } // Analyze right value if (!nfvals.containsKey(value2)) { // Figure out field info if (finfo2 != null) { nfvals.put(value2, finfo2); } else if (finfo1 != null) { nfvals.put(value2, finfo1); } // Figure out table name if (table2 != null) { ntvals.put(value2, table2); } else if (table1 != null) { ntvals.put(value2, table1); } else { if (nfvals.containsKey(rvalue2)) { FieldInfoIF finfo = nfvals.get(rvalue2); if (finfo.getTable() != null) ntvals.put(value2, finfo.getTable()); } if (!ntvals.containsKey(value2) && nfvals.containsKey(rvalue1)) { FieldInfoIF finfo = nfvals.get(rvalue1); if (finfo.getTable() != null) ntvals.put(value2, finfo.getTable()); } } } } } protected void analyzeMatchingNonIdentifiableValue(JDOValueIF value1, JDOValueIF value2) { // Check that value1 is either a variable or parameter // NOTE: this check is not really neccessary switch (value1.getType()) { case JDOValueIF.VARIABLE: case JDOValueIF.PARAMETER: break; default: throw new OntopiaRuntimeException("Non-supported named value: '" + value1 + "'"); } // Ignore when entry already exists if (nfvals.containsKey(value1)) return; // Get field info of secondary value FieldInfoIF finfo = getFieldInfo(value2, this); // Register matching field info nfvals.put(value1, finfo); //! System.out.println("==> NF: " + value1 + " " + finfo + " " + finfo.getTable() + " from " + value2); // If field info have no associated table get table of identifiable parent if (!finfo.isIdentityField()) { if (finfo.getTable() == null) { Class ctype = getIdentifiableValueType(value2, this); // Register table of identifiable parent ntvals.put(value1, mapping.getClassInfo(ctype).getMasterTable()); } else { // Register table of identifiable parent ntvals.put(value1, finfo.getTable()); } } } } // ----------------------------------------------------------------------------- // Build SQL query object // ----------------------------------------------------------------------------- public SQLQuery makeQuery(JDOQuery jdoquery, ObjectAccessIF oaccess) { // Initialize SQL query object SQLQuery sqlquery = new SQLQuery(); sqlquery.setDistinct(jdoquery.getDistinct()); sqlquery.setOffset(jdoquery.getOffset()); sqlquery.setLimit(jdoquery.getLimit()); // Create build info instance BuildInfo info = new BuildInfo(); // Register object access info.oaccess = oaccess; // Register queries info.jdoquery = jdoquery; info.sqlquery = sqlquery; // Analyze JDO query info.analyze(); // Is query a set operation query? if (!jdoquery.isSetQuery()) { // Get JDO filter List<SQLExpressionIF> expressions = new ArrayList<SQLExpressionIF>(); JDOExpressionIF filter = jdoquery.getFilter(); if (filter != null) produceExpression(filter, expressions, info); // Add select clauses List<Object> select = jdoquery.getSelect(); int select_length = select.size(); for (int i=0; i < select_length; i++) { Object value = select.get(i); // create alias String alias = String.valueOf((char)(97+i)); if (value instanceof JDOValueIF) { // value SQLValueIF sval = produceSelectSQLValueIF((JDOValueIF)value, expressions, info); sval.setAlias(alias); sqlquery.addSelect(sval); } else { // aggregate SQLAggregateIF sagg = produceSelectSQLAggregateIF((JDOAggregateIF)value, expressions, info); sagg.setAlias(alias); sqlquery.addSelect(sagg); } } // Add order by clauses List<JDOOrderBy> orderby = jdoquery.getOrderBy(); int orderby_length = orderby.size(); for (int i=0; i < orderby_length; i++) { sqlquery.addOrderBy(produceSQLOrderBy(orderby.get(i), expressions, info)); } // Make sql expressions and register filter if (!expressions.isEmpty()) sqlquery.setFilter(makeAndExpression(expressions)); // Return produced SQL query return sqlquery; } else { // Produce set operation filter JDOSetOperation jdoset = (JDOSetOperation)jdoquery.getFilter(); SQLSetOperation sqlset = produceSetOperation(jdoset, info); sqlquery.setFilter(sqlset); // Copy select clauses from first sub set and add aggregation if specified. SQLQuery _first = getFirstSQLQuery(sqlset); Map<Object, Object> valuemap = new HashMap<Object, Object>(); List<Object> jdoselect = jdoquery.getSelect(); List<Object> sqlselect = _first.getSelect(); //! System.out.println("JS: " + jdoselect + " SS: " + sqlselect); for (int i=0; i < jdoselect.size(); i++) { Object jdoval = jdoselect.get(i); SQLValueIF sqlval = (SQLValueIF)sqlselect.get(i); // FIXME: always SQLValueIF? // create alias String alias = String.valueOf((char)(97+i)); // copy values if (jdoval instanceof JDOValueIF) { sqlval.setAlias(alias); SQLValueIF pval = new SQLValueReference(sqlval); sqlquery.addSelect(pval); valuemap.put(jdoval, pval); } else { // selected is aggregate sqlval.setAlias(alias); int aggtype = ((JDOAggregateIF)jdoval).getType(); SQLAggregateIF pval = new SQLAggregateReference(wrapAggregate(aggtype, sqlval)); sqlquery.addSelect(pval); valuemap.put(jdoval, pval); } } // Add order by clauses. List<JDOOrderBy> jdoorderby = jdoquery.getOrderBy(); for (int i=0; i < jdoorderby.size(); i++) { JDOOrderBy jdoob = jdoorderby.get(i); int sqlorder = getSQLOrder(jdoob); // TODO: USE ALIASES INSTEAD if (jdoob.isAggregate()) { JDOAggregateIF jdoagg = jdoob.getAggregate(); SQLAggregateIF sqlagg = (SQLAggregateIF)valuemap.get(jdoagg); if (sqlagg == null) throw new OntopiaRuntimeException("SQL aggregate for JDO aggregate not found: " + jdoagg); sqlquery.addOrderBy(new SQLOrderBy(sqlagg, sqlorder)); } else { JDOValueIF jdoval = jdoob.getValue(); SQLValueIF sqlval = (SQLValueIF)valuemap.get(jdoval); if (sqlval == null) throw new OntopiaRuntimeException("SQL value for JDO value not found: " + jdoval); sqlquery.addOrderBy(new SQLOrderBy(sqlval, sqlorder)); } } return sqlquery; } } protected SQLQuery getFirstSQLQuery(SQLSetOperation sqlset) { Object first = sqlset.getSets().get(0); if (first instanceof SQLQuery) return (SQLQuery)first; else return getFirstSQLQuery((SQLSetOperation)first); } protected SQLOrderBy produceSQLOrderBy(JDOOrderBy orderby, List<SQLExpressionIF> expressions, BuildInfo info) { int order = getSQLOrder(orderby); if (orderby.isAggregate()) return new SQLOrderBy(produceSelectSQLAggregateIF(orderby.getAggregate(), expressions, info), order); else return new SQLOrderBy(produceSelectSQLValueIF(orderby.getValue(), expressions, info), order); } protected int getSQLOrder(JDOOrderBy orderby) { if (orderby.getOrder() == JDOOrderBy.ASCENDING) return SQLOrderBy.ASCENDING; else return SQLOrderBy.DESCENDING; } protected SQLValueIF produceSelectSQLValueIF(JDOValueIF value, List<SQLExpressionIF> expressions, BuildInfo info) { // Note: all table objects have been created at this time switch (value.getType()) { case JDOValueIF.PARAMETER: case JDOValueIF.VARIABLE: { // Get value type Class valtype = getValueType(value, info); // Note: aggregate values use special field infos FieldInfoIF finfo; if (isAggregateType(valtype) || isPrimitiveType(valtype)) // Non-identifiable value type finfo = (FieldInfoIF)info.nfvals.get(value); else // Identifiable or primitive value type finfo = getFieldInfo(value, info); // Register selected tuple SQLTable table = info.createNamedValueTable(value, expressions); SQLColumns columns = new SQLColumns(table, finfo.getValueColumns()); columns.setValueType(valtype); columns.setFieldHandler(finfo); return columns; } case JDOValueIF.FIELD: { // FIXME: Field root must be a variable in this case. // FIXME: Should select all inline columns of this table. JDOField field = (JDOField)value; // NOTE: Joins produced to get at selected columns are registered. // FIXME: The following only works for M:M fields. // FIXME: Use following code to identify SQLColumns table. Later // use that information to select _all_ needed data columns in // that table. // FIXME: Should we maintain a map of field objects that have been resolved/joined? // FIXME: Error if field is N-ary Values values = produceFieldValues(field, null, expressions, info); //! System.out.println("=> " + field + " - " + values + " | " + values.finfo); // Select either keys or inline values //! boolean keyonly = false; //! if (finfo.isReferenceField() && !keyonly) { //! String[] vcols = (keyonly ? finfo.getJoinKeys() : getInlineColumns(finfo)); //! //! } else { SQLColumns columns = (SQLColumns)values.vcols; columns.setValueType(getValueType(field, info)); columns.setFieldHandler(getFieldInfo(field, info)); return columns; //! } } case JDOValueIF.NULL: { return new SQLNull(); } default: throw new OntopiaRuntimeException("Non-supported select value: '" + value + "'"); } } protected String[] getKeyColumns(FieldInfoIF finfo) { return finfo.getValueClassInfo().getIdentityFieldInfo().getValueColumns(); } protected String[] getInlineColumns(FieldInfoIF finfo) { ClassInfoIF cinfo = finfo.getValueClassInfo(); List<String> vcols = new ArrayList<String>(); FieldUtils.addColumns(cinfo.getIdentityFieldInfo(), vcols); FieldUtils.addColumns(cinfo.getOne2OneFieldInfos(), vcols); return FieldUtils.toStringArray(vcols); } protected SQLAggregateIF produceSelectSQLAggregateIF(JDOAggregateIF aggregate, List<SQLExpressionIF> expressions, BuildInfo info) { // Produce sql value SQLValueIF sqlvalue = produceSelectSQLValueIF(aggregate.getValue(), expressions, info); // Then wrap in aggregate return wrapAggregate(aggregate.getType(), sqlvalue); } protected SQLAggregateIF wrapAggregate(int aggtype, SQLValueIF sqlvalue) { //wrap SQL value in aggregate switch (aggtype) { case JDOAggregateIF.COUNT: // Wrap sql value in aggregate function return new SQLAggregate(sqlvalue, SQLAggregateIF.COUNT); default: throw new OntopiaRuntimeException("Invalid aggregate function type: '" + aggtype + "'"); } } protected SQLExpressionIF makeAndExpression(List<SQLExpressionIF> expressions) { if (expressions.size() > 1) { SQLExpressionIF[] exprlist = new SQLExpressionIF[expressions.size()]; expressions.toArray(exprlist); return new SQLAnd(exprlist); } else if (expressions.size() == 1) return expressions.get(0); else throw new OntopiaRuntimeException("No expressions were found."); } protected SQLExpressionIF makeOrExpression(SQLExpressionIF[] expressions) { if (expressions.length > 1) { return new SQLOr(expressions); } else if (expressions.length == 1) return expressions[0]; else throw new OntopiaRuntimeException("No expressions were found."); } protected void produceExpression(JDOExpressionIF jdoexpr, List<SQLExpressionIF> expressions, BuildInfo info) { // Check expression type and delegate to appropriate produce method. switch (jdoexpr.getType()) { // method calls case JDOExpressionIF.EQUALS: { JDOEquals expr = (JDOEquals)jdoexpr; checkCompatibility(expr.getLeft(), expr.getRight(), info); produceEquals(expr.getLeft(), expr.getRight(), expressions, info); return; } case JDOExpressionIF.NOT_EQUALS: { JDONotEquals expr = (JDONotEquals)jdoexpr; checkCompatibility(expr.getLeft(), expr.getRight(), info); produceNotEquals(expr.getLeft(), expr.getRight(), expressions, info); return; } case JDOExpressionIF.CONTAINS: { JDOContains expr = (JDOContains)jdoexpr; checkCompatibility(expr.getLeft(), Collection.class, info); // FIXME: The following does not work. //! Class vtype = checkCompatibility(expr.getLeft(), expr.getRight(), info); produceContains(expr.getLeft(), expr.getRight(), expressions, info); return; } case JDOExpressionIF.IS_EMPTY: { JDOIsEmpty expr = (JDOIsEmpty)jdoexpr; checkCompatibility(expr.getValue(), Collection.class, info); produceIsEmpty(expr.getValue(), expressions, info); return; } case JDOExpressionIF.STARTS_WITH: { JDOStartsWith expr = (JDOStartsWith)jdoexpr; checkCompatibility(expr.getLeft(), expr.getRight(), info); produceStartsWith(expr.getLeft(), expr.getRight(), expressions, info); return; } case JDOExpressionIF.ENDS_WITH: { JDOEndsWith expr = (JDOEndsWith)jdoexpr; checkCompatibility(expr.getLeft(), expr.getRight(), info); produceEndsWith(expr.getLeft(), expr.getRight(), expressions, info); return; } case JDOExpressionIF.LIKE: { JDOLike expr = (JDOLike)jdoexpr; checkCompatibility(expr.getLeft(), expr.getRight(), info); produceLike(expr.getLeft(), expr.getRight(), expr.getCaseSensitive(), expressions, info); return; } // logical operators case JDOExpressionIF.AND: { produceAnd((JDOAnd)jdoexpr, expressions, info); return; } case JDOExpressionIF.OR: { produceOr((JDOOr)jdoexpr, expressions, info); return; } case JDOExpressionIF.NOT: { produceNot((JDONot)jdoexpr, expressions, info); return; } case JDOExpressionIF.BOOLEAN: { produceBoolean((JDOBoolean)jdoexpr, expressions, info); return; } case JDOExpressionIF.VALUE_EXPRESSION: { // produce expression value produceValueExpression((JDOValueExpression)jdoexpr, expressions, info); return; } //! // set operators //! case JDOExpressionIF.SET_OPERATION: { //! produceSetOperation((JDOSetOperation)jdoexpr, expressions, info); //! return; //! } default: throw new OntopiaRuntimeException("Expression is of unknown type: '" + jdoexpr + "'"); } } protected SQLSetOperation produceSetOperation(JDOSetOperation setop_expr, BuildInfo info) { List<Object> jdosets = setop_expr.getSets(); List<Object> sqlsets = new ArrayList<Object>(jdosets.size()); int length = jdosets.size(); for (int i=0; i < length; i++) { Object set = jdosets.get(i); if (set instanceof JDOQuery) sqlsets.add(makeQuery((JDOQuery)set, info.oaccess)); else sqlsets.add(produceSetOperation((JDOSetOperation)set, info)); } int optype; switch (setop_expr.getOperator()) { case JDOSetOperation.UNION: optype = SQLSetOperation.UNION; break; case JDOSetOperation.UNION_ALL: optype = SQLSetOperation.UNION_ALL; break; case JDOSetOperation.INTERSECT: optype = SQLSetOperation.INTERSECT; break; case JDOSetOperation.EXCEPT: optype = SQLSetOperation.EXCEPT; break; default: throw new OntopiaRuntimeException("Unsupported set operator: '" + setop_expr.getOperator() + "'"); } return new SQLSetOperation(sqlsets, optype); } protected void produceBoolean(JDOBoolean boolean_expr, List<SQLExpressionIF> expressions, BuildInfo info) { SQLValueIF value = new SQLPrimitive(new Integer(0), Types.INTEGER); if (boolean_expr.getValue()) expressions.add(new SQLEquals(value, value)); else expressions.add(new SQLNotEquals(value, value)); } protected void produceValueExpression(JDOValueExpression jdoexpr, List<SQLExpressionIF> expressions, BuildInfo info) { expressions.add(new SQLValueExpression(produceValue(jdoexpr.getValue(), expressions, info))); } protected void produceAnd(JDOAnd and_expr, List<SQLExpressionIF> expressions, BuildInfo info) { expressions.add(new SQLAnd(produceExpressions(and_expr.getExpressions(), info))); } protected void produceNot(JDONot not_expr, List<SQLExpressionIF> expressions, BuildInfo info) { JDOExpressionIF jdoexpr = not_expr.getExpression(); List<SQLExpressionIF> templist = new ArrayList<SQLExpressionIF>(); produceExpression(jdoexpr, templist, info); expressions.add(new SQLNot(new SQLExists(makeAndExpression(templist)))); } protected void produceOr(JDOOr or_expr, List<SQLExpressionIF> expressions, BuildInfo info) { // TODO: Break expressions into SQLExists if they are complex enough. // ISSUE: When are they complex enough? Only when new variables // are introduced? //! expressions.add(new SQLOr(produceExpressions(or_expr.getExpressions(), info))); JDOExpressionIF[] jdoexprs = or_expr.getExpressions(); SQLExpressionIF[] sqlexprs = new SQLExpressionIF[jdoexprs.length]; for (int i=0; i < jdoexprs.length; i++) { List<SQLExpressionIF> templist = new ArrayList<SQLExpressionIF>(); produceExpression(jdoexprs[i], templist, info); sqlexprs[i] = new SQLExists(makeAndExpression(templist)); } expressions.add(makeOrExpression(sqlexprs)); } protected SQLExpressionIF[] produceExpressions(JDOExpressionIF[] jdoexprs, BuildInfo info) { // Loop over JDO expression and produce SQL expression List<SQLExpressionIF> expressions = new ArrayList<SQLExpressionIF>(); for (int i=0; i < jdoexprs.length; i++) { produceExpression(jdoexprs[i], expressions, info); } SQLExpressionIF[] result = new SQLExpressionIF[expressions.size()]; expressions.toArray(result); return result; } // ----------------------------------------------------------------------------- // SQL expression // ----------------------------------------------------------------------------- protected void produceEquals(JDOValueIF left, JDOValueIF right, List<SQLExpressionIF> expressions, BuildInfo info) { // ----------------------------------------------------------------------------- // EXPRESSION: variable1.field == variable2 // - join variable1 field value columns with identity field columns of variable2 // : A1.type == T1 - > A1.type = T1.id [not an ordinary join / no join object] // EXPRESSION: variable.field == parameter // - compare variable field value columns with parameter values // : A1.type == P1 - > A1.type = ?P.id // EXPRESSION: variable1.field == variable2.field // - join variable1 field value columns with variable2 field value columns // : A1.type == A2.type - > A1.type = A2.type // EXPRESSION: variable == parameter // - compare identity field columns of variable with parameter values // : A1 == P1 - > A1.id = ?P.id // ----------------------------------------------------------------------------- // Produce values for left and right JDO value and wrap in equals expression expressions.add(new SQLEquals(produceValue(left, expressions, info), produceValue(right, expressions, info))); } protected void produceNotEquals(JDOValueIF left, JDOValueIF right, List<SQLExpressionIF> expressions, BuildInfo info) { // Produce values for left and right JDO value and wrap in equals expression expressions.add(new SQLNotEquals(produceValue(left, expressions, info), produceValue(right, expressions, info))); } protected void produceContains(JDOValueIF left, JDOValueIF right, List<SQLExpressionIF> expressions, BuildInfo info) { // ----------------------------------------------------------------------------- // EXPRESSION: variable1.field[OM].contains(variable2.field) // EXPRESSION: variable1.field[MM].contains(variable2.field) // EXPRESSION: variable1.field<collection>.contains(variable2.field) // EXPRESSION: parameter<collection>.contains(variable) // EXPRESSION: parameter<collection>.contains(variable.field) // EXPRESSION: object.field[OM].contains(...) // EXPRESSION: object.field[MM].contains(...) // EXPRESSION: variable1.field<collection>.contains(...) // EXPRESSION: variable1<collection>.contains(...) // EXPRESSION: <collection>.contains(...) // - ... // // COMMON: // - register all variable tables using variable name as the table alias // ----------------------------------------------------------------------------- // Check left value type switch (left.getType()) { case JDOValueIF.FIELD: { // -- left // ----------------------------------------------------------------------------- // EXPRESSION: X.field.contains(variable) // ----------------------------------------------------------------------------- JDOField field = (JDOField)left; // Complain if not collection field FieldInfoIF finfo = getFieldInfo(field, info); if (!finfo.isCollectionField() && (finfo.getValueClassInfo() != null && finfo.getValueClassInfo().getStructure() != ClassInfoIF.STRUCTURE_COLLECTION)) throw new OntopiaRuntimeException("contains's left field must be a collection field: '" + left + "'"); // Check right value type SQLTable endtable = null; switch (right.getType()) { case JDOValueIF.PARAMETER: case JDOValueIF.VARIABLE: { // -- right // Get right value table endtable = info.createNamedValueTable(right, expressions); break; } case JDOValueIF.OBJECT: { break; } default: throw new OntopiaRuntimeException("Unsupported contains right value: '" + right + "'"); } // Figure out if the field value is a collection JDOValueIF root = field.getRoot(); switch (root.getType()) { //! case JDOValueIF.PARAMETER: { //! TODO: not yet supported //! } case JDOValueIF.PARAMETER: case JDOValueIF.VARIABLE: { // Produce field values [and join many-tables] //! Values lvalues = produceFieldValues(field, endtable, expressions, info); Values lvalues = produceVariableFieldValues(root, field.getPath(), endtable, expressions, info); SQLValueIF vcols = lvalues.vcols; // TODO: If left value evaluates to a collection at runtime we // have to invert the operation. If collection is empty the // expression is false. // If M:M field we also have to join with rvalue table (if different). if (finfo.getCardinality() == FieldInfoIF.MANY_TO_MANY || finfo.getCardinality() == FieldInfoIF.ONE_TO_MANY) { // Produce right value SQLValueIF rvalue = produceValue(right, expressions, info); // Compare M:M many-keys with right value //! System.out.println("Joining: MM2 " + vcols + " <-> " + rvalue); expressions.add(new SQLEquals(vcols, rvalue)); } // Need extra join if field value is of collection structure else if (finfo.getValueClassInfo() != null && finfo.getValueClassInfo().getStructure() == ClassInfoIF.STRUCTURE_COLLECTION) { ClassInfoIF vcinfo = finfo.getValueClassInfo(); SQLTable table = new SQLTable(vcinfo.getMasterTable(), info.createTableAlias("T")); SQLColumns rvalue = new SQLColumns(table, vcinfo.getIdentityFieldInfo().getValueColumns()); // Add join between field table and collection structure table expressions.add(new SQLEquals(vcols, rvalue)); // Produce right value (same as in last if) SQLValueIF rvalue1 = produceValue(right, expressions, info); SQLValueIF rvalue2 = new SQLColumns(table, vcinfo.getFieldInfoByName("element").getValueColumns()); expressions.add(new SQLEquals(rvalue1, rvalue2)); } else { throw new OntopiaRuntimeException("contains's left field of unknown collection type: '" + left + "'"); } break; } case JDOValueIF.OBJECT: { //! System.out.println("ROOT is object: " + root); // TODO: complain when both sides are compile-time collections // TODO: use SQLIn // Produce field values Values lvalues = produceObjectFieldValues((JDOObject)root, field.getPath(), info); if (!(lvalues.finfo.isCollectionField() || (finfo.getValueClassInfo() != null && finfo.getValueClassInfo().getStructure() == ClassInfoIF.STRUCTURE_COLLECTION))) throw new OntopiaRuntimeException("contains's left field is not of collection type: '" + left + "'"); //! System.out.println("L=> " + lvalues.vcols); // Expression is false when left field value is empty or null if (lvalues.vcols.getType() == SQLValueIF.NULL || (lvalues.vcols.getType() == SQLValueIF.TUPLE && ((SQLTuple)lvalues.vcols).getArity() == 0)) { expressions.add(SQLFalse.getInstance()); } else { // Produce right side value SQLValueIF rval = produceValue(right, expressions, info); //! System.out.println("R=> " + rval); //! if (lvalues.vcols.getType() == SQLValueIF.TUPLE) { if (rval.getArity() == 1 && lvalues.vcols.getType() == SQLValueIF.TUPLE && lvalues.vcols.getArity() > 1) { // Loop over field value tuples and compare with right-hand value SQLValueIF[] vals = ((SQLTuple)lvalues.vcols).getValues(); for (int i=0; i < vals.length; i++) { expressions.add(new SQLEquals(vals[i], rval)); } } else { expressions.add(new SQLEquals(lvalues.vcols, rval)); } } break; } default: throw new OntopiaRuntimeException("Unsupported contains left value: '" + left + "'"); } return; } case JDOValueIF.VARIABLE: { // -- left // ----------------------------------------------------------------------------- // EXPRESSION: variable<collection>.contains(...) // ----------------------------------------------------------------------------- // throw new OntopiaRuntimeException("variable<collection>.contains(...) not yet supported."); JDOVariable var = (JDOVariable)left; FieldInfoIF finfo = getFieldInfo(var, info); ClassInfoIF cinfo = finfo.getParentClassInfo(); if (cinfo.getStructure() == ClassInfoIF.STRUCTURE_COLLECTION) { // Get value table SQLTable table = info.createNamedValueTable(var, expressions); // Produce values SQLValueIF lvalue = new SQLColumns(table, cinfo.getFieldInfoByName("element").getValueColumns()); SQLValueIF rvalue = produceValue(right, expressions, info); expressions.add(new SQLEquals(lvalue, rvalue)); return; } else throw new OntopiaRuntimeException("variable.contains(...) expression must be of type with collection structure: " + var); } case JDOValueIF.PARAMETER: { // -- left //! FIXME: Collection values are not yet properly supported. JDOParameter param = (JDOParameter)left; String parname = param.getName(); // Check right value type switch (right.getType()) { case JDOValueIF.VARIABLE: { // -- right JDOVariable var = (JDOVariable)right; // ----------------------------------------------------------------------------- // EXPRESSION: parameter<collection>.contains(variable) // ----------------------------------------------------------------------------- // - morph into IN expression // : P.contains(T1) -> T1.id in (P1, ..., Pn) // Note: parameter must be of the same type as the variable // so get identity field of variable FieldInfoIF finfo = getFieldInfo(var, info); // Note: identity field int arity = finfo.getColumnCount(); if (arity != 1) throw new OntopiaRuntimeException("parameter<collection>.contains(variable) requires a value arity of exactly 1: " + var); // Create parameter SQLParameter sqlparam = new SQLParameter(parname, arity); sqlparam.setValueType(info.jdoquery.getParameterType(parname)); sqlparam.setFieldHandler(finfo); expressions.add(new SQLIn(produceValue(var, expressions, info), sqlparam)); return; } default: throw new OntopiaRuntimeException("Unsupported contains right value: '" + right + "'"); } } case JDOValueIF.COLLECTION: { // -- left // Add expression expressions.add(new SQLIn(produceValue(right, expressions, info), produceCollection((JDOCollection)left, info))); return; } default: // -- left throw new OntopiaRuntimeException("Unsupported contains left value: '" + left + "'"); } } protected void produceIsEmpty(JDOValueIF value, List<SQLExpressionIF> expressions, BuildInfo info) { // Check value type switch (value.getType()) { case JDOValueIF.FIELD: { JDOField field = (JDOField)value; // Complain if not collection field FieldInfoIF finfo = getFieldInfo(field, info); if (!finfo.isCollectionField() && (finfo.getValueClassInfo() != null && finfo.getValueClassInfo().getStructure() != ClassInfoIF.STRUCTURE_COLLECTION)) throw new OntopiaRuntimeException("isEmpty's field must be a collection field: '" + field + "'"); // ----------------------------------------------------------------------------- // EXPRESSION: variable.field[OM].isEmpty() // EXPRESSION: variable.field[MM].isEmpty() // EXPRESSION: variable.field<collection>.isEmpty() // ----------------------------------------------------------------------------- // JDO: T1.topicmap = M1 & T1.roles.isEmpty() & T1.types.isEmpty() List<SQLExpressionIF> lexpressions = new ArrayList<SQLExpressionIF>(); // Produce left value Values lvalues = produceFieldValues(field, null, lexpressions, info); // Need extra join if field value is of collection structure if (finfo.getValueClassInfo() != null && finfo.getValueClassInfo().getStructure() == ClassInfoIF.STRUCTURE_COLLECTION) { ClassInfoIF vcinfo = finfo.getValueClassInfo(); SQLTable table = new SQLTable(vcinfo.getMasterTable(), info.createTableAlias("T")); SQLColumns rvalue = new SQLColumns(table, vcinfo.getIdentityFieldInfo().getValueColumns()); // Add join between field table and collection structure table lexpressions.add(new SQLEquals(lvalues.vcols, rvalue)); } // Make sure that the local expressions go into the "not exists" expressions.add(new SQLNot(new SQLExists(makeAndExpression(lexpressions)))); return; } case JDOValueIF.VARIABLE: { // ----------------------------------------------------------------------------- // EXPRESSION: variable<collection>.isEmpty() // ----------------------------------------------------------------------------- throw new OntopiaRuntimeException("variable<collection>.isEmpty() not yet supported."); } //! case JDOValueIF.COLLECTION: { //! TODO: evaluate Collection.isEmpty(). //! TODO: same applies to Object<collection>.isEmpty() //! } default: throw new OntopiaRuntimeException("Unsupported isEmpty value: '" + value + "'"); } } protected void produceStartsWith(JDOValueIF left, JDOValueIF right, List<SQLExpressionIF> expressions, BuildInfo info) { produceLikeWithPattern(left, right, false, expressions, true, info); } protected void produceEndsWith(JDOValueIF left, JDOValueIF right, List<SQLExpressionIF> expressions, BuildInfo info) { produceLikeWithPattern(left, right, false, expressions, false, info); } protected void produceLike(JDOValueIF left, JDOValueIF right, boolean caseSensitive, List<SQLExpressionIF> expressions, BuildInfo info) { expressions.add(new SQLLike(produceValue(left, expressions, info), produceValue(right, expressions, info), caseSensitive)); } protected void produceLikeWithPattern(JDOValueIF left, JDOValueIF right, boolean caseSensitive, List<SQLExpressionIF> expressions, boolean starts_not_ends, BuildInfo info) { //! // Make sure that the value type is of string type //! if (!java.lang.String.class.equals(vtype)) //! throw new OntopiaRuntimeException("Invalid value type for startsWith expression: " + vtype); // Produce SQL value for left JDO value SQLValueIF lvalue = produceValue(left, expressions, info); // Check left value arity: int arity = lvalue.getArity(); if (arity != 1) if (starts_not_ends) throw new OntopiaRuntimeException("Arity of left String.startsWith value is not 1: " + arity); else throw new OntopiaRuntimeException("Arity of left String.endsWith value is not 1: " + arity); // Create SQL like expression switch (right.getType()) { case JDOValueIF.STRING: case JDOValueIF.PRIMITIVE: String value = ((JDOString)right).getValue(); if (starts_not_ends) expressions.add(new SQLLike(lvalue, new SQLPrimitive(value + "%", Types.VARCHAR), caseSensitive)); else expressions.add(new SQLLike(lvalue, new SQLPrimitive("%" + value, Types.VARCHAR), caseSensitive)); return; default: // FIXME: this doesn't work if value is parameter // FIXME: this doesn't work if right produces multiple (something it shouldn't really do methinks) expressions.add(new SQLLike(lvalue, produceValue(right, expressions, info), caseSensitive)); return; } } // ----------------------------------------------------------------------------- // SQL values // ----------------------------------------------------------------------------- //! protected int getArity(SQLValueIF[] values) { //! int arity = 0; //! for (int i=0; i < values.length; i++) { //! arity = arity + values[i].getArity(); //! } //! return arity; //! } protected SQLValueIF[] produceValues(JDOValueIF[] values, List<SQLExpressionIF> expressions, BuildInfo info) { SQLValueIF[] retval = new SQLValueIF[values.length]; for (int i=0; i < values.length; i++) { retval[i] = produceValue(values[i], expressions, info); } return retval; } protected SQLValueIF produceValue(JDOValueIF value, List<SQLExpressionIF> expressions, BuildInfo info) { // FIXME: Sometimes need both left and right values in orcer to // correctly assert value type. This is especially applies to // parameters and aggregate variables. // Check value type switch (value.getType()) { case JDOValueIF.FIELD: return produceField((JDOField)value, null, expressions, info); case JDOValueIF.VARIABLE: return produceVariable((JDOVariable)value, expressions, info); case JDOValueIF.PARAMETER: return produceParameter((JDOParameter)value, expressions, info); case JDOValueIF.PRIMITIVE: return producePrimitive((JDOPrimitive)value, info); case JDOValueIF.OBJECT: return produceObject((JDOObject)value, info); case JDOValueIF.STRING: return new SQLPrimitive(((JDOString)value).getValue(), Types.VARCHAR); case JDOValueIF.COLLECTION: return produceCollection((JDOCollection)value, info); case JDOValueIF.NULL: return new SQLNull(); case JDOValueIF.NATIVE_VALUE: return produceNativeValue((JDONativeValue)value, expressions, info); case JDOValueIF.FUNCTION: return produceFunction((JDOFunction)value, expressions, info); default: throw new OntopiaRuntimeException("Unsupported value: '" + value + "'"); } } protected SQLValueIF produceField(JDOField field, SQLTable endtable, List<SQLExpressionIF> expressions, BuildInfo info) { return produceFieldValues(field, endtable, expressions, info).vcols; } static class Values { // [vcols, jcols] Values prev; SQLValueIF vcols; // value columns SQLValueIF jcols; // join columns Class vtype; FieldInfoIF finfo; Values getFirst() { if (prev != null) return prev.getFirst(); else return this; } public String toString() { return "[" + vcols + ", " + jcols + "]"; } } protected Values produceFieldValues(JDOField field, SQLTable endtable, List<SQLExpressionIF> expressions, BuildInfo info) { // TODO: Add flag indicating that only key value or inline values // should be used as vcols. Note that this may only make sense for // 1:M and M:M fields? // FIXME: Do not join tables for navigated field if they have // already been navigated. // FIXME: We may have to maintain list of values produced for each // navigation step, since we don't want F.foo.bar to produce an // extra join when F.foo.bar.baz has already been traversed and // joined. // // 1. check F.foo.bar.baz // 2. then F.foo.bar // 3. then F.foo // 4. build new field values for F.foo // 5. build new field values for F.foo.bar // 6. build new field values for F.foo.bar.baz JDOValueIF root = field.getRoot(); // If the root is an object we are able to figure out the field // value by traversing the object's field and embedding the actual // value(s) in the generated query. switch (root.getType()) { case JDOValueIF.VARIABLE: case JDOValueIF.PARAMETER: return produceVariableFieldValues(root, field.getPath(), endtable, expressions, info); case JDOValueIF.OBJECT: return produceObjectFieldValues((JDOObject)root, field.getPath(), info); default: throw new OntopiaRuntimeException("Only variables are supported field roots at this time: '" + root + "'"); } } protected Values produceVariableFieldValues(JDOValueIF root, String[] path, SQLTable endtable, List<SQLExpressionIF> expressions, BuildInfo info) { // FIXME: Reuse the same Values instance for every loop. Values pvalues = null; // parent values Values cvalues = null; // child values // Get parent cols for variable pvalues = new Values(); pvalues.vcols = produceValue(root, expressions, info); pvalues.vtype = getValueType(root, info); pvalues.finfo = null; // Traverse navigation path one step at a time; join tables and // produce field values. for (int i=0; i < path.length; i++) { String fname = path[i]; // Complain if field does not have cardinality of 1:1 // Note: this will only occur when 1:M and M:M fields are followed by another field. if (pvalues.finfo != null && pvalues.finfo.getCardinality() != FieldInfoIF.ONE_TO_ONE) throw new OntopiaRuntimeException("Field navigation can only be used with single value fields."); // Get field info ClassInfoIF pcinfo = mapping.getClassInfo(pvalues.vtype); FieldInfoIF finfo = pcinfo.getFieldInfoByName(fname); if (finfo == null) throw new OntopiaRuntimeException("'" + pvalues.vtype + "' does not have a field called '" + fname + "'."); // Only set cvalues if not set (applies on first iteration only) cvalues = new Values(); cvalues.prev = pvalues; cvalues.vtype = finfo.getValueClass(); cvalues.finfo = finfo; // Keep previous vcols SQLValueIF pvalues_vcols = pvalues.vcols; switch (finfo.getCardinality()) { case FieldInfoIF.ONE_TO_ONE: { String tblname = finfo.getTable(); if (tblname == null || (pvalues_vcols instanceof SQLColumns && tblname.equals(((SQLColumns)pvalues_vcols).getTable().getName()))) { // ---------------------------------------------------------------------- // SAME TABLE cvalues.vcols = new SQLColumns(((SQLColumns)pvalues_vcols).getTable(), finfo.getValueColumns()); cvalues.jcols = null; // ain't joining here, since the tables are the same. } else { // ---------------------------------------------------------------------- // DIFFERENT TABLE // FIXME: May want to share table aliases with other external fields // Create new table alias. SQLTable table = new SQLTable(tblname, info.createTableAlias("T")); if (!mapping.isDeclared(cvalues.vtype)) { // ---------------------------------------------------------------------- // Primitive field value // ---------------------------------------------------------------------- cvalues.vcols = new SQLColumns(table, finfo.getValueColumns()); cvalues.jcols = pvalues_vcols; // FIXME: Primitive fields can also be external to the master table. } else { // Field is of declared type ClassInfoIF vcinfo = mapping.getClassInfo(cvalues.vtype); // ---------------------------------------------------------------------- // Aggregate field value // ---------------------------------------------------------------------- if (vcinfo.isAggregate()) { cvalues.vcols = new SQLColumns(table, finfo.getValueColumns()); cvalues.jcols = new SQLColumns(table, pcinfo.getIdentityFieldInfo().getValueColumns()); } // ---------------------------------------------------------------------- // Identifiable field value // ---------------------------------------------------------------------- else { // Update id cols [FIXME: id colnames not neccessarily the same in external table] cvalues.vcols = new SQLColumns(table, finfo.getValueColumns()); cvalues.jcols = pvalues_vcols; } } // Join 1:1 field navigation tables (different tables only) //! System.out.println("Joining: OO " + pvalues_vcols + " <-> " + cvalues.jcols); //! expressions.add(new SQLJoin(pvalues_vcols, cvalues.jcols)); //! if (!pvalues_vcols.equals(cvalues.jcols)) expressions.add(new SQLEquals(pvalues_vcols, cvalues.vcols)); } break; } case FieldInfoIF.ONE_TO_MANY: { SQLTable table; String tblname = finfo.getJoinTable(); // The table is guaranteed to always be the last. if (endtable != null) { table = endtable; if (!tblname.equals(endtable.getName())) throw new OntopiaRuntimeException("Incompatible tables: '" + tblname + "' <-> '" + endtable.getName() + "'."); } else table = new SQLTable(tblname, info.createTableAlias("O")); // join and value columns ClassInfoIF _cinfo = finfo.getValueClassInfo(); cvalues.jcols = new SQLColumns(table, finfo.getJoinKeys()); if (_cinfo.isAggregate()) // Aggregate cvalues.vcols = new SQLColumns(table, getInlineColumns(finfo)); else // Identifiable cvalues.vcols = new SQLColumns(table, _cinfo.getIdentityFieldInfo().getValueColumns()); // Join 1:M PARENT-TABLE and JOIN-TABLE //! System.out.println("Joining: OM " + pvalues_vcols + " <-> " + cvalues.jcols); //! expressions.add(new SQLJoin(pvalues_vcols, cvalues.jcols)); expressions.add(new SQLEquals(pvalues_vcols, cvalues.jcols)); break; } case FieldInfoIF.MANY_TO_MANY: { // FIXME: End table is not used. ClassInfoIF _cinfo = finfo.getValueClassInfo(); String tblname = _cinfo.getMasterTable(); // The table is guaranteed to always be the last. if (endtable != null) { if (!tblname.equals(endtable.getName())) throw new OntopiaRuntimeException("Incompatible tables: '" + tblname + "' <-> '" + endtable.getName() + "'."); } // Join and value columns SQLTable j_table = new SQLTable(finfo.getJoinTable(), info.createTableAlias("M")); cvalues.vcols = new SQLColumns(j_table, finfo.getManyKeys()); cvalues.jcols = new SQLColumns(j_table, finfo.getJoinKeys()); // Join M:M JOIN-TABLE and VALUE-TABLE //! System.out.println("Joining: MM1 " + pvalues_vcols + " <-> " + cvalues.jcols); //!expressions.add(new SQLJoin(pvalues_vcols, cvalues.jcols)); expressions.add(new SQLEquals(pvalues_vcols, cvalues.jcols)); break; } default: throw new OntopiaRuntimeException("Invalid field cardinality: '" + finfo.getCardinality() + "'"); } // Make child values the parent values of the next iteration. pvalues = cvalues; } // -- end traverse navigation path // Return the last SQL value created return cvalues; } protected Values produceObjectFieldValues(JDOObject obj, String[] path, BuildInfo info) { // TODO: Only 1:1 fields supported. // Get object field value Object value = obj.getValue(); Class ctype = value.getClass(); FieldInfoIF finfo = null; for (int i=0; i < path.length; i++) { // Make sure that field parent is of declared type if (mapping.isDeclared(ctype)) { ClassInfoIF cinfo = mapping.getClassInfo(ctype); finfo = cinfo.getFieldInfoByName(path[i]); if (finfo == null) throw new OntopiaRuntimeException("Parent '" + ctype + "' do not have field called '" + path[i] + "'"); if (cinfo.isIdentifiable()) { value = info.oaccess.getValue(value, finfo); } else { try { value = finfo.getValue(value); } catch (Exception e) { throw new OntopiaRuntimeException(e); } } ctype = finfo.getValueClass(); } else throw new OntopiaRuntimeException("Parent of field '" + path[i] + "' of undeclared type: '" + ctype + "'"); } Values values = new Values(); values.finfo = finfo; if (value != null) { if (finfo.isCollectionField()|| (finfo.getValueClassInfo() != null && finfo.getValueClassInfo().getStructure() == ClassInfoIF.STRUCTURE_COLLECTION)) { Collection cvalue = (Collection)value; List<SQLValueIF> tuples = new ArrayList<SQLValueIF>(cvalue.size()); Iterator iter = cvalue.iterator(); while (iter.hasNext()) { List<SQLValueIF> list = new ArrayList<SQLValueIF>(finfo.getColumnCount()); finfo.retrieveSQLValues(iter.next(), list); tuples.add((list.size() == 1 ? list.get(0) : new SQLTuple(list))); } values.vcols = (tuples.size() == 1 ? tuples.get(0) : new SQLTuple(tuples)); } else { List<SQLValueIF> list = new ArrayList<SQLValueIF>(); finfo.retrieveSQLValues(value, list); values.vcols = (list.size() == 1 ? list.get(0) : new SQLTuple(list)); } } else { // FIXME: Arity might not be 1. Use FieldHandlerIF.getColumnCount()? values.vcols = new SQLNull(); } return values; } protected SQLValueIF produceVariable(JDOVariable var, List<SQLExpressionIF> expressions, BuildInfo info) { String varname = var.getName(); // FIXME: Handle aggregate or alias type variables properly. // FIXME: Handle primitive variables properly // Identifiable type if (isIdentifiableVariable(varname, info.jdoquery)) { // Get variable table FieldInfoIF finfo = getFieldInfo(var, info); // Note: identity field SQLTable table = info.createNamedValueTable(var, expressions); // Produce values return new SQLColumns(table, finfo.getValueColumns()); } // Aggregate type or Primitive type else if (isAggregateVariable(varname, info.jdoquery) || isPrimitiveVariable(varname, info.jdoquery)) { // Get variable table FieldInfoIF finfo = info.nfvals.get(var); SQLTable table = info.createNamedValueTable(var, expressions); // Produce values return new SQLColumns(table, finfo.getValueColumns()); } // Unknown type else { throw new OntopiaRuntimeException("Variable '" + varname + "' of unknown type." + info.jdoquery.getVariableNames() + " " + info.jdoquery.getVariableType(varname)); } } protected SQLValueIF produceParameter(JDOParameter par, List<SQLExpressionIF> expressions, BuildInfo info) { String parname = par.getName(); // FIXME: Handle aggregate or alias type parameters properly. // FIXME: Handle primitive parameters properly // Identifiable type if (isIdentifiableParameter(parname, info.jdoquery)) { // Get parameter table FieldInfoIF finfo = getFieldInfo(par, info); // Note: identity field SQLTable table = info.createNamedValueTable(par, expressions); // Produce values return new SQLColumns(table, finfo.getValueColumns()); } // Aggregate type or Primitive type else if (isAggregateParameter(parname, info.jdoquery) || isPrimitiveParameter(parname, info.jdoquery)) { // Get parameter table FieldInfoIF finfo = info.nfvals.get(par); SQLTable table = info.createNamedValueTable(par, expressions); // Produce values return new SQLColumns(table, finfo.getValueColumns()); } // Unknown type else { throw new OntopiaRuntimeException("Parameter '" + parname + "' of unknown type. " + info.jdoquery.getParameterNames() + " " + info.jdoquery.getParameterType(parname)); } } protected SQLValueIF producePrimitive(JDOPrimitive primitive, BuildInfo info) { // Map primitive type correctly switch (primitive.getPrimitiveType()) { case JDOPrimitive.INTEGER: return new SQLPrimitive(primitive.getValue(), Types.INTEGER); case JDOPrimitive.LONG: return new SQLPrimitive(primitive.getValue(), Types.BIGINT); case JDOPrimitive.SHORT: return new SQLPrimitive(primitive.getValue(), Types.SMALLINT); case JDOPrimitive.FLOAT: return new SQLPrimitive(primitive.getValue(), Types.REAL); case JDOPrimitive.DOUBLE: return new SQLPrimitive(primitive.getValue(), Types.DOUBLE); case JDOPrimitive.BOOLEAN: return new SQLPrimitive(primitive.getValue(), Types.BIT); case JDOPrimitive.BYTE: return new SQLPrimitive(primitive.getValue(), Types.TINYINT); case JDOPrimitive.BIGDECIMAL: return new SQLPrimitive(primitive.getValue(), Types.DECIMAL); case JDOPrimitive.BIGINTEGER: return new SQLPrimitive(primitive.getValue(), Types.NUMERIC); default: throw new OntopiaRuntimeException("Unsupported primitive type: '" + primitive.getPrimitiveType() + "'"); } } protected SQLValueIF produceNativeValue(JDONativeValue field, List<SQLExpressionIF> expressions, BuildInfo info) { // TODO: Add support for other things than column references JDOVariable var = field.getRoot(); SQLColumns varcols = (SQLColumns)produceVariable(var, expressions, info); return new SQLColumns(varcols.getTable(), field.getArguments()); } protected SQLValueIF produceFunction(JDOFunction func, List<SQLExpressionIF> expressions, BuildInfo info) { return new SQLFunction(func.getName(), produceValues(func.getArguments(), expressions, info)); } protected SQLValueIF produceObject(JDOObject object, BuildInfo info) { // retrieve field values List<SQLValueIF> values = new ArrayList<SQLValueIF>(); FieldInfoIF id_finfo = getFieldInfo(object, info); id_finfo.retrieveSQLValues(object.getValue(), values); if (values.size() == 1) return values.get(0); else return new SQLTuple(values); } protected SQLValueIF produceCollection(JDOCollection coll, BuildInfo info) { // loop over collection elements and retrieve field values List<SQLValueIF> values = new ArrayList<SQLValueIF>(); FieldInfoIF id_finfo = getFieldInfo(coll, info); Collection _coll = coll.getValue(); Iterator iter = _coll.iterator(); while (iter.hasNext()) { id_finfo.retrieveSQLValues(iter.next(), values); } if (values.size() == 1) return values.get(0); else return new SQLTuple(values); } // ----------------------------------------------------------------------------- // Field declarations (FieldInfoIFs) // ----------------------------------------------------------------------------- protected FieldInfoIF getFieldInfo(JDOValueIF jdovalue, BuildInfo info) { switch (jdovalue.getType()) { case JDOValueIF.FIELD: return getFieldInfo((JDOField)jdovalue, info); case JDOValueIF.VARIABLE: return getFieldInfo((JDOVariable)jdovalue, info); case JDOValueIF.PARAMETER: return getFieldInfo((JDOParameter)jdovalue, info); case JDOValueIF.OBJECT: return getFieldInfo((JDOObject)jdovalue, info); case JDOValueIF.COLLECTION: return getFieldInfo((JDOCollection)jdovalue, info); default: return null; } } protected FieldInfoIF getFieldInfo(JDOVariable var, BuildInfo info) { // TODO: Need to check if primitive or aggregate type //! if (info.nfvals.containsKey(var)) //! return (FieldInfoIF)info.nfvals.get(var); //! else { Class vtype = info.jdoquery.getVariableType(var.getName()); if (mapping.isDeclared(vtype)) { ClassInfoIF cinfo = mapping.getClassInfo(vtype); return cinfo.getIdentityFieldInfo(); } else return null; //! } } protected FieldInfoIF getFieldInfo(JDOParameter param, BuildInfo info) { Class ptype = info.jdoquery.getParameterType(param.getName()); ClassInfoIF cinfo = mapping.getClassInfo(ptype); return cinfo.getIdentityFieldInfo(); } protected FieldInfoIF getFieldInfo(JDOObject object, BuildInfo info) { Class otype = object.getValueType(); ClassInfoIF cinfo = mapping.getClassInfo(otype); return cinfo.getIdentityFieldInfo(); } protected FieldInfoIF getFieldInfo(JDOCollection coll, BuildInfo info) { Class eltype = coll.getElementType(); if (isPrimitiveType(eltype)) return null; ClassInfoIF cinfo = mapping.getClassInfo(eltype); return cinfo.getIdentityFieldInfo(); } protected FieldInfoIF getFieldInfo(JDOField field, BuildInfo info) { // Note: returns the field info of the outer most field, not the field info of the root // Note: primitive and aggregate fields should not get this far // Get value type of field root Class ctype = getValueType(field.getRoot(), info); // Get the field info of the outermost field String[] path = field.getPath(); FieldInfoIF finfo = null; for (int i=0; i < path.length; i++) { // Make sure that field parent is of declared type if (mapping.isDeclared(ctype)) { ClassInfoIF cinfo = mapping.getClassInfo(ctype); finfo = cinfo.getFieldInfoByName(path[i]); if (finfo == null) throw new OntopiaRuntimeException("Parent '" + ctype + "' do not have field called '" + path[i] + "'"); ctype = finfo.getValueClass(); } else throw new OntopiaRuntimeException("Parent of field '" + path[i] + "' of undeclared type: '" + ctype + "'"); } return finfo; } // ----------------------------------------------------------------------------- // JDOValueIF compatiblity // ----------------------------------------------------------------------------- protected Class checkCompatibility(JDOValueIF value1, JDOValueIF value2, BuildInfo info) { // TODO: Make sure assignable types are considered compatible (use // interface declarations from object relational mapping) // Get types for both values Class type1 = getValueType(value1, info); Class type2 = getValueType(value2, info); // FIXME: Complain if one is primitive and the other is null? // FIXME: If one is null they're compatible if (type1 == null) return type2; if (type2 == null) return type1; // Check that types are compatible //! if (!(type1.isAssignableFrom(type2) || type2.isAssignableFrom(type1))) if (type1 != type2) { // HACK: string/reader compatibility hack if (type1 == String.class && type2 == java.io.Reader.class) return String.class; else if (type1 == java.io.Reader.class && type2 == String.class) return String.class; else throw new OntopiaRuntimeException("Values '" + value1 + "' (" + type1 + ") and '" + value2 + "' (" + type2 + ") are not compatible."); } // Return the [first [matching]] value type return type1; } protected Class checkCompatibility(JDOValueIF value, Class type, BuildInfo info) { Class vtype = getValueType(value, info); if (!type.isAssignableFrom(vtype)) throw new OntopiaRuntimeException("Value '" + value + "' (" + vtype + ") is not compatible with type " + type + "."); return vtype; } // ----------------------------------------------------------------------------- // Declared JDOValueIF class types // ----------------------------------------------------------------------------- protected Class getValueType(JDOValueIF value, BuildInfo info) { // TODO: Figure out the type of the value. switch (value.getType()) { case JDOValueIF.FIELD: return getValueType((JDOField)value, info); case JDOValueIF.VARIABLE: return info.jdoquery.getVariableType(((JDOVariable)value).getName()); case JDOValueIF.PARAMETER: return info.jdoquery.getParameterType(((JDOParameter)value).getName()); case JDOValueIF.PRIMITIVE: return ((JDOPrimitive)value).getValueType(); case JDOValueIF.OBJECT: return ((JDOObject)value).getValueType(); case JDOValueIF.STRING: return java.lang.String.class; case JDOValueIF.COLLECTION: return ((JDOCollection)value).getValueType(); case JDOValueIF.NATIVE_VALUE: return ((JDONativeValue)value).getValueType(); case JDOValueIF.NULL: return null; case JDOValueIF.FUNCTION: return ((JDOFunction)value).getValueType(); default: throw new OntopiaRuntimeException("Invalid value: '" + value + "'"); } } protected Class getValueType(JDOField field, BuildInfo info) { FieldInfoIF finfo = getFieldInfo(field, info); // Check to see if field exists if (finfo == null) throw new OntopiaRuntimeException("Unknown field: '" + field + "'"); // Return the field value class if (finfo.isCollectionField()) return Collection.class; else { Class klass = finfo.getValueClass(); // HACK: string/reader compatibility hack return (klass == java.io.Reader.class ? String.class : klass); } } protected boolean isIdentifiableValueType(JDOValueIF jdovalue, BuildInfo info) { return (getIdentifiableValueType(jdovalue, info) != null); } protected Class getIdentifiableValueType(JDOValueIF jdovalue, BuildInfo info) { // Returns a Class that is identifiable. This is not neccessarily // the actual type of "jdovalue", but can be any of its parent // field infos if jdovalue is a field. Class ctype = null; switch (jdovalue.getType()) { case JDOValueIF.FIELD: return getIdentifiableValueType((JDOField)jdovalue, info); case JDOValueIF.VARIABLE: ctype = getValueType((JDOVariable)jdovalue, info); break; case JDOValueIF.PARAMETER: ctype = getValueType((JDOParameter)jdovalue, info); break; case JDOValueIF.OBJECT: ctype = getValueType((JDOObject)jdovalue, info); break; case JDOValueIF.COLLECTION: ctype = getValueType((JDOCollection)jdovalue, info); break; default: return null; } return (isIdentifiableType(ctype) ? ctype : null); } protected Class getIdentifiableValueType(JDOField field, BuildInfo info) { // Note: returns the field info of the outer most field, not the field info of the root // Note: primitive and aggregate fields should not get this far // Get value type of field root Class ctype = getValueType(field.getRoot(), info); // Get the field info of the outermost field String[] path = field.getPath(); FieldInfoIF finfo = null; for (int i=0; i < path.length; i++) { // Make sure that field parent is of declared type if (mapping.isDeclared(ctype)) { ClassInfoIF cinfo = mapping.getClassInfo(ctype); finfo = cinfo.getFieldInfoByName(path[i]); if (finfo == null) throw new OntopiaRuntimeException("Parent '" + ctype + "' do not have field called '" + path[i] + "'"); Class _ctype = finfo.getValueClass(); // Stop when next path item is non-identifiable //! System.out.println("ID: " + path[i] + " " + isIdentifiableType(_ctype) + " " + _ctype); if (isIdentifiableType(_ctype)) ctype = _ctype; else break; } else throw new OntopiaRuntimeException("Parent of field '" + path[i] + "' of undeclared type: '" + ctype + "'"); } return (isIdentifiableType(ctype) ? ctype : null); } // ----------------------------------------------------------------------------- // Identifiable, aggregate or primtive class types // ----------------------------------------------------------------------------- // FIXME: Add isAliasType(Class) and rename BuildInfo.nfvals to alias_fields protected boolean isIdentifiableVariable(String var, JDOQuery jdoquery) { // Get variable class type and check if it's identifiable return isIdentifiableType(jdoquery.getVariableType(var)); } protected boolean isIdentifiableParameter(String param, JDOQuery jdoquery) { // Get parameter class type and check if it's identifiable return isIdentifiableType(jdoquery.getParameterType(param)); } protected boolean isIdentifiableType(Class type) { // Lookup variable type class info if (mapping.isDeclared(type)) { ClassInfoIF cinfo = mapping.getClassInfo(type); return cinfo.isIdentifiable(); } else return false; } protected boolean isAggregateVariable(String var, JDOQuery jdoquery) { // Get variable class type and check if it's aggregate return isAggregateType(jdoquery.getVariableType(var)); } protected boolean isAggregateParameter(String param, JDOQuery jdoquery) { // Get parameter class type and check if it's aggregate return isAggregateType(jdoquery.getParameterType(param)); } protected boolean isAggregateType(Class type) { // Lookup variable type class info if (mapping.isDeclared(type)) { ClassInfoIF cinfo = mapping.getClassInfo(type); //if (cinfo == null) return false; // Type is aggregate if class descriptor is aggregate return cinfo.isAggregate(); } else return false; } protected boolean isPrimitiveVariable(String var, JDOQuery jdoquery) { // Get variable class type and check if it's primitive return isPrimitiveType(jdoquery.getVariableType(var)); } protected boolean isPrimitiveParameter(String param, JDOQuery jdoquery) { // Get parameter class type and check if it's primitive return isPrimitiveType(jdoquery.getParameterType(param)); } protected boolean isPrimitiveType(Class type) { // TODO: Fix this code! // Should probably delegate to JDOPrimitive or ObjectRelationalMappingIF? if (type == java.lang.String.class || type == java.io.Reader.class || type == java.lang.Integer.class || type == java.lang.Float.class || type == java.lang.Long.class) { return true; } return false; } }