1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.openjpa.kernel.exps;
20
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.Iterator;
25 import java.util.Map;
26 import java.util.Set;
27
28 import org.apache.openjpa.kernel.Filters;
29 import org.apache.openjpa.lib.util.Localizer;
30 import org.apache.openjpa.lib.util.Localizer.Message;
31 import org.apache.openjpa.meta.ClassMetaData;
32 import org.apache.openjpa.meta.FieldMetaData;
33 import org.apache.openjpa.meta.JavaTypes;
34 import org.apache.openjpa.meta.XMLMetaData;
35 import org.apache.openjpa.util.InternalException;
36 import org.apache.openjpa.util.OpenJPAException;
37 import org.apache.openjpa.util.UnsupportedException;
38 import org.apache.openjpa.util.UserException;
39 import serp.util.Strings;
40
41 /**
42 * Abstract base class to help build expressions. Provides
43 * generic language-independent support for variable resolution,
44 * path traversal, and error messages.
45 *
46 * @author Marc Prud'hommeaux
47 * @nojavadoc
48 */
49 public abstract class AbstractExpressionBuilder {
50
51 // used for error messages
52 protected static final int EX_USER = 0;
53 protected static final int EX_FATAL = 1;
54 protected static final int EX_UNSUPPORTED = 2;
55
56 // common implicit type settings
57 protected static final Class TYPE_OBJECT = Object.class;
58 protected static final Class TYPE_STRING = String.class;
59 protected static final Class TYPE_CHAR_OBJ = Character.class;
60 protected static final Class TYPE_NUMBER = Number.class;
61 protected static final Class TYPE_COLLECTION = Collection.class;
62 protected static final Class TYPE_MAP = Map.class;
63
64 // contains types for setImplicitTypes
65 protected static final int CONTAINS_TYPE_ELEMENT = 1;
66 protected static final int CONTAINS_TYPE_KEY = 2;
67 protected static final int CONTAINS_TYPE_VALUE = 3;
68
69 private static final Localizer _loc = Localizer.forPackage
70 (AbstractExpressionBuilder.class);
71
72 protected final Resolver resolver;
73 protected ExpressionFactory factory;
74
75 private final Set _accessPath = new HashSet();
76 private Map _seenVars = null;
77 private Set _boundVars = null;
78
79 /**
80 * Constructor.
81 *
82 * @param factory the expression factory to use
83 * @param resolver used to resolve variables, parameters, and class
84 * names used in the query
85 */
86 public AbstractExpressionBuilder(ExpressionFactory factory,
87 Resolver resolver) {
88 this.factory = factory;
89 this.resolver = resolver;
90 }
91
92 /**
93 * Returns the class loader that should be used for resolving
94 * class names (in addition to the resolver in the query).
95 */
96 protected abstract ClassLoader getClassLoader();
97
98 /**
99 * Create a proper parse exception for the given reason.
100 */
101 protected OpenJPAException parseException(int e, String token,
102 Object[] args,
103 Exception nest) {
104 String argStr;
105 if (args == null)
106 argStr = getLocalizer().get(token).getMessage();
107 else
108 argStr = getLocalizer().get(token, args).getMessage();
109
110 Message msg = _loc.get("parse-error", argStr, currentQuery());
111
112 switch (e) {
113 case EX_FATAL:
114 throw new InternalException(msg, nest);
115 case EX_UNSUPPORTED:
116 throw new UnsupportedException(msg, nest);
117 default:
118 throw new UserException(msg, nest);
119 }
120 }
121
122 /**
123 * Register the specified metadata as being in the query's access path.
124 */
125 protected ClassMetaData addAccessPath(ClassMetaData meta) {
126 _accessPath.add(meta);
127 return meta;
128 }
129
130 /**
131 * Return the recorded query access path.
132 */
133 protected ClassMetaData[] getAccessPath() {
134 return (ClassMetaData[]) _accessPath.toArray
135 (new ClassMetaData[_accessPath.size()]);
136 }
137
138 /**
139 * Return true if the given variable has been bound.
140 */
141 protected boolean isBound(Value var) {
142 return _boundVars != null && _boundVars.contains(var);
143 }
144
145 /**
146 * Record that the given variable is bound.
147 */
148 protected void bind(Value var) {
149 if (_boundVars == null)
150 _boundVars = new HashSet();
151 _boundVars.add(var);
152 }
153
154 /**
155 * Returns a value for the given id.
156 */
157 protected Value getVariable(String id, boolean bind) {
158 // check for already constructed var
159 if (isSeenVariable(id))
160 return (Value) _seenVars.get(id);
161
162 // create and cache var
163 Class type = getDeclaredVariableType(id);
164
165 // add this type to the set of classes in the filter's access path
166 ClassMetaData meta = null;
167 if (type == null)
168 type = TYPE_OBJECT;
169 else
170 meta = getMetaData(type, false);
171 if (meta != null)
172 _accessPath.add(meta);
173
174 Value var;
175 if (bind)
176 var = factory.newBoundVariable(id, type);
177 else
178 var = factory.newUnboundVariable(id, type);
179 var.setMetaData(meta);
180
181 if (_seenVars == null)
182 _seenVars = new HashMap();
183 _seenVars.put(id, var);
184 return var;
185 }
186
187 /**
188 * Validate that all unbound variables are of a PC type. If not, assume
189 * that the user actually made a typo that we took for an implicit
190 * unbound variable.
191 */
192 protected void assertUnboundVariablesValid() {
193 if (_seenVars == null)
194 return;
195
196 Map.Entry entry;
197 Value var;
198 for (Iterator itr = _seenVars.entrySet().iterator(); itr.hasNext();) {
199 entry = (Map.Entry) itr.next();
200 var = (Value) entry.getValue();
201 if (var.getMetaData() == null && !isBound(var)
202 && !isDeclaredVariable((String) entry.getKey())) {
203 throw parseException(EX_USER, "not-unbound-var",
204 new Object[]{ entry.getKey() }, null);
205 }
206 }
207 }
208
209 /**
210 * Returns whether the specified variable name has been explicitly
211 * declared. Note all query languages necessarily support declaring
212 * variables.
213 *
214 * @param id the variable to check
215 * @return true if the variabe has been explicitely declared
216 */
217 protected abstract boolean isDeclaredVariable(String id);
218
219 /**
220 * Return whether the given id has been used as a variable.
221 */
222 protected boolean isSeenVariable(String id) {
223 return _seenVars != null && _seenVars.containsKey(id);
224 }
225
226 /**
227 * Convenience method to get metadata for the given type.
228 */
229 protected ClassMetaData getMetaData(Class c, boolean required) {
230 return getMetaData(c, required, getClassLoader());
231 }
232
233 /**
234 * Convenience method to get metadata for the given type.
235 */
236 protected ClassMetaData getMetaData(Class c, boolean required,
237 ClassLoader loader) {
238 return resolver.getConfiguration().getMetaDataRepositoryInstance().
239 getMetaData(c, loader, required);
240 }
241
242 /**
243 * Traverse the given field in the given path.
244 */
245 protected Value traversePath(Path path, String field) {
246 return traversePath(path, field, false, false);
247 }
248
249 protected Value traverseXPath(Path path, String field) {
250 XMLMetaData meta = path.getXmlMapping();
251 if (meta.getFieldMapping(field) == null) {
252 throw parseException(EX_USER, "no-field",
253 new Object[]{ meta.getType(), field }, null);
254 }
255 else {
256 // collection-valued xpath is not allowed
257 int type = meta.getFieldMapping(field).getTypeCode();
258 switch (type) {
259 case JavaTypes.ARRAY:
260 case JavaTypes.COLLECTION:
261 case JavaTypes.MAP:
262 throw new UserException(_loc.get("collection-valued-path",
263 field));
264 }
265 }
266 path.get(meta, field);
267 return path;
268 }
269
270 /**
271 * Traverse the given field in the given path.
272 */
273 protected Value traversePath(Path path, String field, boolean pcOnly,
274 boolean allowNull) {
275 ClassMetaData meta = path.getMetaData();
276 if (meta == null)
277 throw parseException(EX_USER, "path-no-meta",
278 new Object[]{ field, path.getType() }, null);
279
280 FieldMetaData fmd = meta.getField(field);
281 if (fmd == null) {
282 Object val = traverseStaticField(meta.getDescribedType(), field);
283 if (val == null)
284 throw parseException(EX_USER, "no-field",
285 new Object[]{ meta.getDescribedType(), field }, null);
286
287 return factory.newLiteral(val, Literal.TYPE_UNKNOWN);
288 }
289
290 if (fmd.isEmbedded())
291 meta = fmd.getEmbeddedMetaData();
292 else
293 meta = fmd.getDeclaredTypeMetaData();
294 if (meta != null) {
295 addAccessPath(meta);
296 path.setMetaData(meta);
297 }
298 else {
299 // xmlsupport xpath
300 XMLMetaData xmlmeta = fmd.getRepository().getXMLMetaData(fmd);
301 if (xmlmeta != null) {
302 path.get(fmd, xmlmeta);
303 return path;
304 }
305 }
306
307 if (meta != null || !pcOnly)
308 path.get(fmd, allowNull);
309
310 return path;
311 }
312
313 /**
314 * Return a constant containing the value of the given static field.
315 */
316 protected Object traverseStaticField(Class cls, String field) {
317 try {
318 return cls.getField(field).get(null);
319 } catch (Exception e) {
320 // count not locate the field: return null
321 return null;
322 }
323 }
324
325 /**
326 * Returns the type of the named variabe if it has been declared.
327 */
328 protected abstract Class getDeclaredVariableType(String name);
329
330 /**
331 * Set the implicit types of the given values based on the fact that
332 * they're used together, and based on the operator type.
333 */
334 protected void setImplicitTypes(Value val1, Value val2,
335 Class expected) {
336 Class c1 = val1.getType();
337 Class c2 = val2.getType();
338 boolean o1 = c1 == TYPE_OBJECT;
339 boolean o2 = c2 == TYPE_OBJECT;
340
341 if (o1 && !o2) {
342 val1.setImplicitType(c2);
343 if (val1.getMetaData() == null && !val1.isXPath())
344 val1.setMetaData(val2.getMetaData());
345 } else if (!o1 && o2) {
346 val2.setImplicitType(c1);
347 if (val2.getMetaData() == null && !val1.isXPath())
348 val2.setMetaData(val1.getMetaData());
349 } else if (o1 && o2 && expected != null) {
350 // we never expect a pc type, so don't bother with metadata
351 val1.setImplicitType(expected);
352 val2.setImplicitType(expected);
353 } else if (isNumeric(val1.getType()) != isNumeric(val2.getType())) {
354 if (resolver.getConfiguration().getCompatibilityInstance().
355 getQuotedNumbersInQueries())
356 convertTypesQuotedNumbers(val1, val2);
357 else
358 convertTypes(val1, val2);
359 }
360 }
361
362 /**
363 * Perform conversions to make values compatible.
364 */
365 private void convertTypes(Value val1, Value val2) {
366 Class t1 = val1.getType();
367 Class t2 = val2.getType();
368
369 // allow string-to-char conversions
370 if (t1 == TYPE_STRING && (Filters.wrap(t2) == TYPE_CHAR_OBJ
371 && !(val2 instanceof Path))) {
372 val2.setImplicitType(String.class);
373 return;
374 }
375 if (t2 == TYPE_STRING && (Filters.wrap(t1) == TYPE_CHAR_OBJ)
376 && !(val1 instanceof Path)) {
377 val1.setImplicitType(String.class);
378 return;
379 }
380
381 // if the non-numeric side is a string of length 1, cast it
382 // to a character
383 if (t1 == TYPE_STRING && val1 instanceof Literal
384 && ((String) ((Literal) val1).getValue()).length() == 1) {
385 val1.setImplicitType(Character.class);
386 return;
387 }
388 if (t2 == TYPE_STRING && val2 instanceof Literal
389 && ((String) ((Literal) val2).getValue()).length() == 1) {
390 val2.setImplicitType(Character.class);
391 return;
392 }
393
394 // error
395 String left;
396 String right;
397 if (val1 instanceof Path && ((Path) val1).last() != null)
398 left = _loc.get("non-numeric-path", ((Path) val1).last().
399 getName(), t1.getName()).getMessage();
400 else
401 left = _loc.get("non-numeric-value", t1.getName()).getMessage();
402 if (val2 instanceof Path && ((Path) val2).last() != null)
403 right = _loc.get("non-numeric-path", ((Path) val2).last().
404 getName(), t2.getName()).getMessage();
405 else
406 right = _loc.get("non-numeric-value", t2.getName()).getMessage();
407 throw new UserException(_loc.get("non-numeric-comparison",
408 left, right));
409 }
410
411 /**
412 * Perform conversions to make values compatible.
413 */
414 private void convertTypesQuotedNumbers(Value val1, Value val2) {
415 Class t1 = val1.getType();
416 Class t2 = val2.getType();
417
418 // if we're comparing to a single-quoted string, convert
419 // the value according to the 3.1 rules.
420 if (t1 == TYPE_STRING && val1 instanceof Literal
421 && ((Literal) val1).getParseType() == Literal.TYPE_SQ_STRING) {
422 String s = (String) ((Literal) val1).getValue();
423 if (s.length() > 1) {
424 ((Literal) val1).setValue(s.substring(0, 1));
425 val1.setImplicitType(Character.TYPE);
426 }
427 }
428 if (t2 == TYPE_STRING && val2 instanceof Literal
429 && ((Literal) val2).getParseType() == Literal.TYPE_SQ_STRING) {
430 String s = (String) ((Literal) val2).getValue();
431 if (s.length() > 1) {
432 ((Literal) val2).setValue(s.substring(0, 1));
433 val2.setImplicitType(Character.TYPE);
434 }
435 }
436
437 // if we're comparing to a double-quoted string, convert the
438 // value directly to a number
439 if (t1 == TYPE_STRING && val1 instanceof Literal
440 && ((Literal) val1).getParseType() == Literal.TYPE_STRING) {
441 String s = (String) ((Literal) val1).getValue();
442 ((Literal) val1).setValue(Strings.parse(s, Filters.wrap(t2)));
443 val1.setImplicitType(t2);
444 }
445 if (t2 == TYPE_STRING && val2 instanceof Literal
446 && ((Literal) val2).getParseType() == Literal.TYPE_STRING) {
447 String s = (String) ((Literal) val2).getValue();
448 ((Literal) val2).setValue(Strings.parse(s, Filters.wrap(t1)));
449 val2.setImplicitType(t1);
450 }
451 }
452
453 /**
454 * Return true if given class can be used as a number.
455 */
456 private static boolean isNumeric(Class type) {
457 type = Filters.wrap(type);
458 return Number.class.isAssignableFrom(type)
459 || type == Character.TYPE || type == TYPE_CHAR_OBJ;
460 }
461
462 /**
463 * Set the implicit types of the given values based on the fact that
464 * the first is supposed to contain the second.
465 */
466 protected void setImplicitContainsTypes(Value val1, Value val2, int op) {
467 if (val1.getType() == TYPE_OBJECT) {
468 if (op == CONTAINS_TYPE_ELEMENT)
469 val1.setImplicitType(Collection.class);
470 else
471 val1.setImplicitType(Map.class);
472 }
473
474 if (val2.getType() == TYPE_OBJECT && val1 instanceof Path) {
475 FieldMetaData fmd = ((Path) val1).last();
476 ClassMetaData meta;
477 if (fmd != null) {
478 if (op == CONTAINS_TYPE_ELEMENT || op == CONTAINS_TYPE_VALUE) {
479 val2.setImplicitType(fmd.getElement().getDeclaredType());
480 meta = fmd.getElement().getDeclaredTypeMetaData();
481 if (meta != null) {
482 val2.setMetaData(meta);
483 addAccessPath(meta);
484 }
485 } else {
486 val2.setImplicitType(fmd.getKey().getDeclaredType());
487 meta = fmd.getKey().getDeclaredTypeMetaData();
488 if (meta != null) {
489 val2.setMetaData(meta);
490 addAccessPath(meta);
491 }
492 }
493 }
494 }
495 }
496
497 /**
498 * Set the implicit type of the given value to the given class.
499 */
500 protected static void setImplicitType(Value val, Class expected) {
501 // we never expect a pc type, so no need to worry about metadata
502 if (val.getType() == TYPE_OBJECT)
503 val.setImplicitType(expected);
504 }
505
506 /**
507 * Used for obtaining the {@link Localizer} to use for translating
508 * error messages.
509 */
510 protected abstract Localizer getLocalizer();
511
512 /**
513 * Returns the current string being parsed; used for error messages.
514 */
515 protected abstract String currentQuery ();
516 }
517