001: package org.apache.ojb.jdo.jdoql;
002:
003: /* Copyright 2003-2005 The Apache Software Foundation
004: *
005: * Licensed under the Apache License, Version 2.0 (the "License");
006: * you may not use this file except in compliance with the License.
007: * You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017:
018: import java.math.BigDecimal;
019: import java.math.BigInteger;
020: import java.util.Collection;
021: import java.util.Date;
022: import java.util.HashMap;
023: import java.util.Iterator;
024: import java.util.Map;
025:
026: import javax.jdo.JDOUserException;
027:
028: import org.apache.ojb.broker.metadata.*;
029: import org.apache.ojb.broker.util.ClassHelper;
030: import org.apache.ojb.jdo.QueryImpl;
031:
032: /**
033: * Resolves and checks the query trees. This involves the following things:<br/>
034: * <ul>
035: * <li>Replace {@link NameExpression NameExpression} with {@link LocalVariableAccess VariableAccess},
036: * {@link FieldAccess FieldAccess}, or {@link TypeAccess TypeAccess}</li>
037: * <li>Resolve types referenced by {@link Type Type} and {@link TypeAccess TypeAccess}</li>
038: * <li>Resolve features/variables referenced by {@link LocalVariableAccess VariableAccess},
039: * {@link FieldAccess FieldAccess}, and {@link MethodInvocation MethodInvocation}</li>
040: * <li>Check that expression types are valid, e.g. that the types of the two sides of a binary
041: * expression are compatible to each other and that this binary operator can be applied
042: * to them</li>
043: * <li>Check that the accessed methods are allowed to be accessed as per JDO spec (or
044: * extensions allowed by OJB)</li>
045: * <li>Check that for each accessed variable there is a variable or parameter defined</li>
046: * <li>Check that each accessed field is persistent</li>
047: * </ul>
048: *
049: * @author <a href="mailto:tomdz@apache.org">Thomas Dudziak</a>
050: */
051: public class QueryTreeResolver extends VisitorBase {
052: // TODO: Integrate position info into the exceptions
053:
054: /** Defines the ordering of primitive types */
055: private static HashMap _primitiveTypes = new HashMap();
056:
057: static {
058: _primitiveTypes.put(byte.class, new Integer(0));
059: _primitiveTypes.put(char.class, new Integer(1));
060: _primitiveTypes.put(short.class, new Integer(2));
061: _primitiveTypes.put(int.class, new Integer(3));
062: _primitiveTypes.put(long.class, new Integer(4));
063: _primitiveTypes.put(BigInteger.class, new Integer(5));
064: _primitiveTypes.put(float.class, new Integer(6));
065: _primitiveTypes.put(double.class, new Integer(7));
066: _primitiveTypes.put(BigDecimal.class, new Integer(8));
067: }
068:
069: /** The currently resolved query */
070: private QueryImpl _query;
071:
072: /**
073: * Resolves and checks the given query. Results from a previous resolving are
074: * overwritten.
075: *
076: * @param query The query
077: * @exception JDOUserException If the query is invalid
078: */
079: public void resolveAndCheck(QueryImpl query)
080: throws JDOUserException {
081: _query = query;
082: try {
083: checkImports(query.getImports());
084: visit(query.getVariables());
085: visit(query.getParameters());
086: if (query.getFilterExpression() != null) {
087: query.getFilterExpression().accept(this );
088: }
089: visit(query.getOrderings());
090: } finally {
091: _query = null;
092: }
093: }
094:
095: /**
096: * Checks the imports. We're a bit more foregiving as the JDO Spec because
097: * we don't demand that all imported types/packages exists as long as they
098: * are not used.
099: *
100: * @param imports The imports to check
101: * @throws JDOUserException If two direct imports imports types with the same
102: * short name
103: */
104: private void checkImports(Collection imports)
105: throws JDOUserException {
106: // It is an error if two direct imports import classes with the same short name
107: HashMap directlyImportedClasses = new HashMap();
108: Import importDecl;
109: String shortName;
110:
111: for (Iterator it = imports.iterator(); it.hasNext();) {
112: importDecl = (Import) it.next();
113: if (!importDecl.isOnDemand()) {
114: shortName = importDecl.getSpec().substring(
115: importDecl.getSpec().lastIndexOf('.'));
116: if (directlyImportedClasses.containsKey(shortName)) {
117: throw new JDOUserException(
118: "Multiple direct imports of classes with the same unqualified name "
119: + shortName);
120: } else {
121: directlyImportedClasses.put(shortName, null);
122: }
123: }
124: }
125: }
126:
127: /**
128: * Visits all elements of the given map.
129: *
130: * @param objects The objects
131: * @exception JDOUserException If one of the elements is invalid
132: */
133: private void visit(Map objects) throws JDOUserException {
134: if (objects != null) {
135: for (Iterator it = objects.values().iterator(); it
136: .hasNext();) {
137: ((Acceptor) it.next()).accept(this );
138: }
139: }
140: }
141:
142: /**
143: * Visits all elements of the given collection.
144: *
145: * @param objects The objects
146: * @exception JDOUserException If one of the elements is invalid
147: */
148: private void visit(Collection objects) throws JDOUserException {
149: if (objects != null) {
150: for (Iterator it = objects.iterator(); it.hasNext();) {
151: ((Acceptor) it.next()).accept(this );
152: }
153: }
154: }
155:
156: /* (non-Javadoc)
157: * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.BinaryExpression)
158: */
159: public void visit(BinaryExpression binExpr) throws JDOUserException {
160: super .visit(binExpr);
161:
162: // for the types of the left and right expressions one of the following must hold:
163: // * the operator is '+' and both are String
164: // * the operator is arithmethic and both are numeric (Character, Long or Double)
165: // * the operator is bitwise and both are Character or Long
166: // * the operator is logic and both are Boolean
167: // * the operator is comparative and the both are String, or both are Date, or both are Numeric
168: Class leftType = binExpr.getLeftSide().getType();
169: Class rightType = binExpr.getRightSide().getType();
170: boolean typeWasSet = false;
171:
172: switch (binExpr.getOperator()) {
173: case BinaryExpression.OPERATOR_MULTIPLY:
174: case BinaryExpression.OPERATOR_DIVIDE:
175: case BinaryExpression.OPERATOR_PLUS:
176: case BinaryExpression.OPERATOR_MINUS:
177: if (binExpr.getOperator() == BinaryExpression.OPERATOR_PLUS) {
178: if ((leftType == String.class)
179: && (rightType == String.class)) {
180: binExpr.setType(String.class);
181: typeWasSet = true;
182: break;
183: }
184: }
185: if (isNumeric(leftType) && isNumeric(rightType)) {
186: binExpr.setType(getBroaderType(leftType, rightType));
187: typeWasSet = true;
188: }
189: break;
190: case BinaryExpression.OPERATOR_BITWISE_AND:
191: case BinaryExpression.OPERATOR_BITWISE_XOR:
192: case BinaryExpression.OPERATOR_BITWISE_OR:
193: if (isInteger(leftType) && isInteger(rightType)) {
194: binExpr.setType(getBroaderType(leftType, rightType));
195: typeWasSet = true;
196: } else if ((leftType == boolean.class)
197: && (rightType == boolean.class)) {
198: binExpr.setType(boolean.class);
199: typeWasSet = true;
200: }
201: break;
202: case BinaryExpression.OPERATOR_LOWER:
203: case BinaryExpression.OPERATOR_GREATER:
204: case BinaryExpression.OPERATOR_LOWER_OR_EQUAL:
205: case BinaryExpression.OPERATOR_GREATER_OR_EQUAL:
206: if (isNumeric(leftType) && isNumeric(rightType)) {
207: binExpr.setType(boolean.class);
208: typeWasSet = true;
209: } else if ((leftType == String.class)
210: && (rightType == String.class)) {
211: binExpr.setType(boolean.class);
212: typeWasSet = true;
213: } else if ((leftType == Date.class)
214: && (rightType == Date.class)) {
215: binExpr.setType(boolean.class);
216: typeWasSet = true;
217: }
218: break;
219: case BinaryExpression.OPERATOR_EQUAL:
220: case BinaryExpression.OPERATOR_NOT_EQUAL:
221: if (isNumeric(leftType) && isNumeric(rightType)) {
222: binExpr.setType(boolean.class);
223: typeWasSet = true;
224: } else if ((leftType == boolean.class)
225: && (rightType == boolean.class)) {
226: binExpr.setType(boolean.class);
227: typeWasSet = true;
228: } else if (!leftType.isPrimitive()
229: && !rightType.isPrimitive()) {
230: binExpr.setType(boolean.class);
231: typeWasSet = true;
232: }
233: break;
234: case BinaryExpression.OPERATOR_AND:
235: case BinaryExpression.OPERATOR_OR:
236: if ((leftType == boolean.class)
237: && (rightType == boolean.class)) {
238: binExpr.setType(boolean.class);
239: typeWasSet = true;
240: }
241: break;
242: }
243: if (!typeWasSet) {
244: throw new JDOUserException(
245: "Binary expression cannot be applied to expressions of types "
246: + leftType.getName() + " and "
247: + rightType.getName());
248: }
249: }
250:
251: /* (non-Javadoc)
252: * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.MethodInvocation)
253: */
254: public void visit(MethodInvocation methodInvoc)
255: throws JDOUserException {
256: super .visit(methodInvoc);
257:
258: Class type = methodInvoc.getBaseExpression().getType();
259: String name = methodInvoc.getName();
260:
261: if (Collection.class.isAssignableFrom(type)) {
262: if ("contains".equals(name)) {
263: if (methodInvoc.getArguments().size() != 1) {
264: throw new JDOUserException(
265: "Illegal number of arguments to method Collection.contains");
266: }
267: } else if ("isEmpty".equals(name)) {
268: if (!methodInvoc.getArguments().isEmpty()) {
269: throw new JDOUserException(
270: "Illegal number of arguments to method Collection.isEmpty");
271: }
272: } else {
273: throw new JDOUserException(
274: "Only methods 'contains' and 'isEmpty' are allowed to be called at collection objects");
275: }
276: methodInvoc.setType(boolean.class);
277: } else if (type == String.class) {
278: if (!"startsWith".equals(name) && !"endsWith".equals(name)) {
279: throw new JDOUserException(
280: "Only methods 'contains' and 'isEmpty' are allowed to be called at collection objects");
281: }
282: if (methodInvoc.getArguments().size() != 1) {
283: throw new JDOUserException(
284: "Illegal number of arguments to method String."
285: + name);
286: }
287: if (((Expression) methodInvoc.getArguments().get(0))
288: .getType() != String.class) {
289: throw new JDOUserException(
290: "Illegal argument to method Collection." + name);
291: }
292: methodInvoc.setType(boolean.class);
293: } else {
294: throw new JDOUserException("Invocation of method "
295: + methodInvoc.getName() + " at type "
296: + type.getName() + " is not allowed");
297: }
298: }
299:
300: /* (non-Javadoc)
301: * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.NameExpression)
302: */
303: public void visit(NameExpression nameExpr) throws JDOUserException {
304: super .visit(nameExpr);
305:
306: Expression newExpr = null;
307: Class baseType = null;
308:
309: // we search in this order (which corresponds to Java's name resolution order):
310: // * variables (only if without base expression)
311: // * parameters (only if without base expression)
312: // * fields/references/collections (at searched type if without base expression)
313: // types are currently not searched for as they are not allowed to appear directly
314: // except in casts
315: if (!nameExpr.hasBaseExpression()) {
316: // no base expressiom
317: LocalVariable var = _query.getVariable(nameExpr.getName());
318:
319: if (var == null) {
320: var = _query.getParameter(nameExpr.getName());
321: }
322: if (var != null) {
323: LocalVariableAccess varAccess = new LocalVariableAccess(
324: nameExpr.getName());
325:
326: varAccess.setAccessedVariable(var);
327: newExpr = varAccess;
328: } else {
329: // neither a variable nor parameter, so we must search in the class
330: // whose objects the query searches for
331: baseType = _query.getSearchedClass();
332: }
333: } else {
334: // we have a base expression which means that we follow a reference
335: baseType = nameExpr.getBaseExpression().getType();
336: }
337: if (newExpr == null) {
338: // so we determine the persistent type of the base expression
339: ClassDescriptor classDesc = findClassDescriptorFor(baseType);
340:
341: if (classDesc == null) {
342: throw new JDOUserException(
343: "Access to type "
344: + baseType.getName()
345: + " is not allowed because the type is not persistent");
346: }
347:
348: FieldAccess fieldAccess = new FieldAccess(nameExpr
349: .getBaseExpression(), nameExpr.getName());
350:
351: // it may be either a field, reference or collection descriptor -
352: // this depends on whether the name expression is a base expression
353: // to another name expression (not a method invocation or other expression)
354: ObjectReferenceDescriptor refDesc = classDesc
355: .getObjectReferenceDescriptorByName(nameExpr
356: .getName());
357:
358: if (refDesc != null) {
359: fieldAccess.setFieldDescriptor(refDesc);
360: } else if (nameExpr.hasParent()
361: && (nameExpr.getParent() instanceof NameExpression)) {
362: // if we are the base expression of another name expression, then it must be a reference
363: throw new JDOUserException(
364: "Cannot find reference "
365: + nameExpr.getName()
366: + " in type "
367: + baseType.getName()
368: + " because it is not defined, not persistent or not a reference");
369: } else {
370: // it can be a field or collection
371: CollectionDescriptor collDesc = classDesc
372: .getCollectionDescriptorByName(nameExpr
373: .getName());
374: if (collDesc != null) {
375: fieldAccess.setFieldDescriptor(collDesc);
376: } else {
377: FieldDescriptor fieldDesc = classDesc
378: .getFieldDescriptorByName(nameExpr
379: .getName());
380:
381: if (fieldDesc == null) {
382: throw new JDOUserException(
383: "Cannot find feature "
384: + nameExpr.getName()
385: + " in type "
386: + baseType.getName()
387: + " because it is not defined or not persistent");
388: }
389: fieldAccess.setFieldDescriptor(fieldDesc);
390: }
391: }
392: newExpr = fieldAccess;
393: }
394:
395: if (nameExpr.hasParent()) {
396: nameExpr.getParent().replaceChild(nameExpr, newExpr);
397: }
398: }
399:
400: /* (non-Javadoc)
401: * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.NullLiteral)
402: */
403: public void visit(NullLiteral nullLit) {
404: Expression parent = nullLit.getParent();
405:
406: if (parent == null) {
407: return;
408: }
409:
410: // the only interesting case is if the parent is a binary expression (e.g.
411: // a '==' expression) in which case the null literal shall have the same
412: // type as the other operand
413: if (parent instanceof BinaryExpression) {
414: Class type = null;
415:
416: if (((BinaryExpression) parent).getLeftSide() == nullLit) {
417: type = ((BinaryExpression) parent).getRightSide()
418: .getType();
419: } else {
420: type = ((BinaryExpression) parent).getLeftSide()
421: .getType();
422: }
423: if (type.isPrimitive()) {
424: throw new JDOUserException(
425: "Illegal binary expression with a 'null' and a primitive operand");
426: }
427: nullLit.setType(type);
428: }
429: }
430:
431: /* (non-Javadoc)
432: * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.ThisExpression)
433: */
434: public void visit(ThisExpression this Expr) {
435: super .visit(this Expr);
436: this Expr.setType(_query.getSearchedClass());
437: }
438:
439: /* (non-Javadoc)
440: * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.Type)
441: */
442: public void visit(Type type) throws JDOUserException {
443: ClassLoader loader = ClassHelper.getClassLoader();
444: Class result = null;
445: String name = type.getName();
446: int pos = name.indexOf('.');
447:
448: if (pos >= 0) {
449: // its either a qualified name or refers to an inner class
450: // we ignore inner classes here as they are handled by the import
451: // resolution below
452: // [tomdz] we might have to resolve inner/nested classes manually
453: // if the query should be executed in the scope of the
454: // searched class (which makes private/protected/friendly
455: // inner/nested classes visible)
456: // for now we assume that this is not the case
457: try {
458: result = Class.forName(name, true, loader);
459: } catch (ClassNotFoundException ex) {
460: // ignored
461: }
462: }
463: if (result == null) {
464: result = resolveUnqualifiedClassName(loader, name);
465: }
466: if (result == null) {
467: throw new JDOUserException("No such class " + name);
468: } else {
469: type.setType(result);
470: }
471: }
472:
473: /**
474: * Resolves the given unqualified class name against the imports of the query.
475: *
476: * @param loader The class loader to use
477: * @param unqualifiedName The unqualified class name
478: * @return The class if it has been found
479: * @exception JDOUserException If a direct import declaration is invalid
480: */
481: private Class resolveUnqualifiedClassName(ClassLoader loader,
482: String unqualifiedName) throws JDOUserException {
483: // A direct import has precedence over on-demand imports of packages that contain
484: // a class of the same short name (JLS 7.5.1)
485: // If multiple on-demand imports import packages that have a class with a given
486: // short name, then the last such import defines the class to be used
487: Import importDecl;
488: Class result = null;
489: int pos;
490:
491: // we first try to resolve it against java.lang
492: try {
493: result = Class.forName("java.lang." + unqualifiedName,
494: true, loader);
495: } catch (ClassNotFoundException ex) {
496: // ignored
497: }
498: for (Iterator it = _query.getImports().iterator(); it.hasNext();) {
499: importDecl = (Import) it.next();
500: if (importDecl.isOnDemand()) {
501: try {
502: result = Class.forName(importDecl.getSpec() + "."
503: + unqualifiedName, true, loader);
504: } catch (ClassNotFoundException ex) {
505: // ignored
506: }
507: } else {
508: pos = importDecl.getSpec().lastIndexOf('.');
509: if (unqualifiedName.equals(importDecl.getSpec()
510: .substring(pos + 1))) {
511: try {
512: // there can only be one direct import of a class with this
513: // unqualified name (imports have already been checked), and
514: // no on-demand import can shadow it, so we can simply return it
515: return Class.forName(importDecl.getSpec() + "."
516: + unqualifiedName, true, loader);
517: } catch (ClassNotFoundException ex) {
518: // we have a direct import for the class but the import is invalid
519: throw new JDOUserException("The import "
520: + importDecl.getSpec() + " is invalid");
521: }
522: }
523: }
524: }
525: return result;
526: }
527:
528: /* (non-Javadoc)
529: * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.UnaryExpression)
530: */
531: public void visit(UnaryExpression unaryExpr)
532: throws JDOUserException {
533: super .visit(unaryExpr);
534:
535: // one of the following must hold:
536: // * the operator is arithmetic and the inner type is numeric
537: // * the operator is bitwise and the inner type is Character or Long
538: // * the operator is logic and the inner type is Boolean
539: // * it is a cast and the cast type is assignment compatible to the inner type
540: Class innerType = unaryExpr.getInnerExpression().getType();
541: boolean typeWasSet = false;
542:
543: switch (unaryExpr.getOperator()) {
544: case UnaryExpression.OPERATOR_PLUS:
545: case UnaryExpression.OPERATOR_MINUS:
546: if (isNumeric(innerType)) {
547: unaryExpr.setType(innerType);
548: typeWasSet = true;
549: }
550: break;
551: case UnaryExpression.OPERATOR_BITWISE_COMPLEMENT:
552: if (isInteger(innerType)) {
553: unaryExpr.setType(innerType);
554: typeWasSet = true;
555: }
556: break;
557: case UnaryExpression.OPERATOR_NOT:
558: if (innerType == boolean.class) {
559: unaryExpr.setType(innerType);
560: typeWasSet = true;
561: }
562: break;
563: case UnaryExpression.OPERATOR_CAST:
564: Class castType = unaryExpr.getCastType().getType();
565:
566: if (isNumeric(castType) && isNumeric(innerType)) {
567: unaryExpr.setType(castType);
568: typeWasSet = true;
569: } else {
570: // check for narrowing or widening reference conversion
571: if (castType.isAssignableFrom(innerType)
572: || innerType.isAssignableFrom(castType)) {
573: unaryExpr.setType(castType);
574: typeWasSet = true;
575: }
576: }
577: break;
578:
579: }
580: if (!typeWasSet) {
581: if (unaryExpr.getOperator() == UnaryExpression.OPERATOR_CAST) {
582: throw new JDOUserException(
583: "Invalid cast expression because inner expression of type "
584: + innerType.getName()
585: + " cannot be cast to "
586: + unaryExpr.getCastType().getName());
587: } else {
588: throw new JDOUserException("Invalid unary expression");
589: }
590: }
591: }
592:
593: // Helper methods
594:
595: /**
596: * Retrieves OJB's class descriptor for the given type.
597: *
598: * @param The type to lookup
599: * @return The class descriptor or <code>null</code> if the class is not persistent
600: */
601: private ClassDescriptor findClassDescriptorFor(Class type) {
602: return MetadataManager.getInstance().getRepository()
603: .getDescriptorFor(type);
604: }
605:
606: /**
607: * Determines whether the given class denotes an integer primitive type.
608: *
609: * @param type The type
610: * @return <code>true</code> if the type is an integer type
611: */
612: private static boolean isInteger(Class type) {
613: if (type.isPrimitive()) {
614: return (type != boolean.class) && (type != float.class)
615: && (type != double.class);
616: } else {
617: return type == BigInteger.class;
618: }
619: }
620:
621: /**
622: * Determines whether the given class denotes a floating point primitive type.
623: *
624: * @param type The type
625: * @return <code>true</code> if the type is a floating point type
626: */
627: private static boolean isFloatingPoint(Class type) {
628: if (type.isPrimitive()) {
629: return (type == float.class) || (type == double.class);
630: } else {
631: return type == BigDecimal.class;
632: }
633: }
634:
635: /**
636: * Determines whether the given class denotes a numeric primitive type.
637: *
638: * @param type The type
639: * @return <code>true</code> if the type is a numeric type
640: */
641: private static boolean isNumeric(Class type) {
642: if (type.isPrimitive()) {
643: return type != boolean.class;
644: } else {
645: return (type == BigDecimal.class)
646: || (type == BigInteger.class);
647: }
648: }
649:
650: /**
651: * Determines the broader of the two given numeric types.
652: *
653: * @param typeA The first type
654: * @param typeB The seconf type
655: * @return The broader of the two types
656: */
657: private static Class getBroaderType(Class typeA, Class typeB) {
658: Integer numA = (Integer) _primitiveTypes.get(typeA);
659: Integer numB = (Integer) _primitiveTypes.get(typeB);
660:
661: return numA.intValue() < numB.intValue() ? typeB : typeA;
662: }
663: }
|