Source code: org/objectstyle/cayenne/access/util/QueryUtils.java
1 /* ====================================================================
2 *
3 * The ObjectStyle Group Software License, Version 1.0
4 *
5 * Copyright (c) 2002-2003 The ObjectStyle Group
6 * and individual authors of the software. All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 *
12 * 1. Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
14 *
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in
17 * the documentation and/or other materials provided with the
18 * distribution.
19 *
20 * 3. The end-user documentation included with the redistribution, if
21 * any, must include the following acknowlegement:
22 * "This product includes software developed by the
23 * ObjectStyle Group (http://objectstyle.org/)."
24 * Alternately, this acknowlegement may appear in the software itself,
25 * if and wherever such third-party acknowlegements normally appear.
26 *
27 * 4. The names "ObjectStyle Group" and "Cayenne"
28 * must not be used to endorse or promote products derived
29 * from this software without prior written permission. For written
30 * permission, please contact andrus@objectstyle.org.
31 *
32 * 5. Products derived from this software may not be called "ObjectStyle"
33 * nor may "ObjectStyle" appear in their names without prior written
34 * permission of the ObjectStyle Group.
35 *
36 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
37 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
38 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
39 * DISCLAIMED. IN NO EVENT SHALL THE OBJECTSTYLE GROUP OR
40 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
41 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
42 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
43 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
44 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
45 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
46 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
47 * SUCH DAMAGE.
48 * ====================================================================
49 *
50 * This software consists of voluntary contributions made by many
51 * individuals on behalf of the ObjectStyle Group. For more
52 * information on the ObjectStyle Group, please see
53 * <http://objectstyle.org/>.
54 *
55 */
56 package org.objectstyle.cayenne.access.util;
57
58 import java.util.HashMap;
59 import java.util.Iterator;
60 import java.util.List;
61 import java.util.ListIterator;
62 import java.util.Map;
63
64 import org.apache.log4j.Logger;
65 import org.objectstyle.cayenne.CayenneRuntimeException;
66 import org.objectstyle.cayenne.DataObject;
67 import org.objectstyle.cayenne.FlattenedObjectId;
68 import org.objectstyle.cayenne.ObjectId;
69 import org.objectstyle.cayenne.access.QueryEngine;
70 import org.objectstyle.cayenne.exp.Expression;
71 import org.objectstyle.cayenne.exp.ExpressionException;
72 import org.objectstyle.cayenne.exp.ExpressionFactory;
73 import org.objectstyle.cayenne.exp.ExpressionTraversal;
74 import org.objectstyle.cayenne.exp.TraversalHelper;
75 import org.objectstyle.cayenne.map.DbRelationship;
76 import org.objectstyle.cayenne.map.Entity;
77 import org.objectstyle.cayenne.map.ObjAttribute;
78 import org.objectstyle.cayenne.map.ObjEntity;
79 import org.objectstyle.cayenne.map.ObjRelationship;
80 import org.objectstyle.cayenne.map.Relationship;
81 import org.objectstyle.cayenne.query.DeleteQuery;
82 import org.objectstyle.cayenne.query.InsertQuery;
83 import org.objectstyle.cayenne.query.PrefetchSelectQuery;
84 import org.objectstyle.cayenne.query.SelectQuery;
85 import org.objectstyle.cayenne.query.UpdateQuery;
86
87 /**
88 * Implements helper methods that perform different query-related operations.
89 * <i>May be deprecated in the future, after its functionality is moved to the
90 * places where it is used now.</i>
91 *
92 * @author Andrei Adamchik
93 *
94 */
95 public class QueryUtils {
96 private static Logger logObj = Logger.getLogger(QueryUtils.class);
97
98 private static Map putModifiedAttribute(
99 Map aMap,
100 String name,
101 Object value) {
102 if (aMap == null) {
103 aMap = new HashMap();
104 }
105 aMap.put(name, value);
106 return aMap;
107 }
108
109 /**
110 * Returns a map of the properties of dataObject which have actually changed
111 * compared to the objects commited snapshot. Actual change is determined
112 * by using equals() (true implies no change).
113 * Will return null if there are no changes
114 *
115 * @param dataObject dataObject that may have changes
116 */
117 public static Map updatedProperties(DataObject dataObject) {
118 Map result = null;
119 //Lazily created to avoid creating too many unnecessary objects
120
121 Map committedSnapshot = dataObject.getCommittedSnapshot();
122 Map currentSnapshot = dataObject.getCurrentSnapshot();
123
124 Iterator it = currentSnapshot.keySet().iterator();
125 while (it.hasNext()) {
126 String attrName = (String) it.next();
127 Object newValue = currentSnapshot.get(attrName);
128
129 // if snapshot exists, compare old values and new values,
130 // only add attribute to the update clause if the value has changed
131 if (committedSnapshot != null) {
132 Object oldValue = committedSnapshot.get(attrName);
133 if (oldValue != null && !oldValue.equals(newValue)) {
134 result = putModifiedAttribute(result, attrName, newValue);
135 } else if (oldValue == null && newValue != null) {
136 result = putModifiedAttribute(result, attrName, newValue);
137 }
138 }
139 // if no snapshot exists, just add the fresh value to update clause
140 else {
141 result = putModifiedAttribute(result, attrName, newValue);
142 }
143 }
144
145 // original snapshot can have extra keys that are missing in the
146 // current snapshot; process those
147 if (committedSnapshot != null) {
148 Iterator origit = committedSnapshot.keySet().iterator();
149 while (origit.hasNext()) {
150 String attrName = (String) origit.next();
151 if (currentSnapshot.containsKey(attrName))
152 continue;
153
154 Object oldValue = committedSnapshot.get(attrName);
155 if (oldValue == null)
156 continue;
157 result = putModifiedAttribute(result, attrName, null);
158 }
159 }
160 return result; //Might be null if nothing was actually changed
161 }
162
163 /** Returns an update query for the DataObject that can be used to commit
164 * object state changes to the database. If no changes are found, null is returned.
165 *
166 * @param dataObject data object that potentially can have changes that need
167 * to be synchronized with the database.
168 */
169 public static UpdateQuery updateQuery(DataObject dataObject) {
170 UpdateQuery upd = new UpdateQuery();
171
172 ObjectId id = dataObject.getObjectId();
173 upd.setRoot(dataObject.getClass());
174 Map modifiedProperties = updatedProperties(dataObject);
175 if ((modifiedProperties != null) && (modifiedProperties.size() > 0)) {
176 Iterator keyIterator = modifiedProperties.keySet().iterator();
177 while (keyIterator.hasNext()) {
178 String key = (String) keyIterator.next();
179 upd.addUpdAttribute(key, modifiedProperties.get(key));
180 }
181 // set qualifier
182 upd.setQualifier(
183 ExpressionFactory.matchAllDbExp(
184 id.getIdSnapshot(),
185 Expression.EQUAL_TO));
186 return upd;
187 }
188
189 return null;
190 }
191
192 /** Generates a delete query for a specified data object */
193 public static DeleteQuery deleteQuery(DataObject dataObject) {
194 DeleteQuery del = new DeleteQuery();
195 ObjectId id = dataObject.getObjectId();
196 del.setRoot(dataObject.getClass());
197 del.setQualifier(
198 ExpressionFactory.matchAllDbExp(
199 id.getIdSnapshot(),
200 Expression.EQUAL_TO));
201 return del;
202 }
203
204 /** Generates an insert query for a specified data object.
205 *
206 * @param dataObject new data object that need to be inserted to the database.
207 * @param permId permanent object id that will be assigned to this data object after
208 * it is committed to the database.
209 */
210 public static InsertQuery insertQuery(
211 Map objectSnapshot,
212 ObjectId permId) {
213 InsertQuery ins = new InsertQuery();
214 ins.setRoot(permId.getObjClass());
215 ins.setObjectSnapshot(objectSnapshot);
216 ins.setObjectId(permId);
217 return ins;
218 }
219
220 public static SelectQuery selectObjectForFlattenedObjectId(
221 QueryEngine e,
222 FlattenedObjectId oid) {
223 //Create a selectquery using the relationship name
224 // and source id snapshot of the FlattenedObjectId
225 SelectQuery sel = new SelectQuery();
226 sel.setRoot(oid.getObjClass());
227
228 DataObject sourceObject = oid.getSourceObject();
229 Expression sourceExpression =
230 ExpressionFactory.matchAllDbExp(
231 sourceObject.getObjectId().getIdSnapshot(),
232 Expression.EQUAL_TO);
233 ObjEntity ent = e.getEntityResolver().lookupObjEntity(sourceObject);
234
235 sel.setQualifier(
236 transformQualifier(
237 ent,
238 sourceExpression,
239 oid.getRelationshipName()));
240
241 return sel;
242 }
243 /**
244 * Creates and returns a select query that can be used to
245 * fetch an object given an ObjectId.
246 */
247 public static SelectQuery selectObjectForId(ObjectId oid) {
248 if (oid instanceof FlattenedObjectId) {
249 throw new CayenneRuntimeException(
250 "Please use "
251 + "selectObjectForFlattenedObjectId(QueryEngine, FlattenedObjectId)"
252 + " to create a select query for a FlattenedObjectId");
253 }
254 SelectQuery sel = new SelectQuery();
255 sel.setRoot(oid.getObjClass());
256
257 sel.setQualifier(
258 ExpressionFactory.matchAllDbExp(
259 oid.getIdSnapshot(),
260 Expression.EQUAL_TO));
261 return sel;
262 }
263
264 /**
265 * Creates and returns a select query that can be used to
266 * fetch a list of objects given a list of ObjectIds.
267 * All ObjectIds must belong to the same entity.
268 */
269 public static SelectQuery selectQueryForIds(List oids) {
270 if (oids == null || oids.size() == 0) {
271 throw new IllegalArgumentException("List must contain at least one ObjectId");
272 }
273
274 SelectQuery sel = new SelectQuery();
275 sel.setRoot(((ObjectId) oids.get(0)).getObjClass());
276
277 Iterator it = oids.iterator();
278
279 ObjectId firstId = (ObjectId) it.next();
280 Expression exp =
281 ExpressionFactory.matchAllDbExp(
282 firstId.getIdSnapshot(),
283 Expression.EQUAL_TO);
284
285 while (it.hasNext()) {
286 ObjectId anId = (ObjectId) it.next();
287 exp =
288 exp.orExp(
289 ExpressionFactory.matchAllDbExp(
290 anId.getIdSnapshot(),
291 Expression.EQUAL_TO));
292 }
293
294 sel.setQualifier(exp);
295 return sel;
296 }
297
298 /**
299 * Creates and returns SelectQuery for a given SelectQuery and
300 * relationship prefetching path.
301 */
302 public static PrefetchSelectQuery selectPrefetchPath(
303 QueryEngine e,
304 SelectQuery q,
305 String prefetchPath) {
306 ObjEntity ent = e.getEntityResolver().lookupObjEntity(q);
307 PrefetchSelectQuery newQ = new PrefetchSelectQuery();
308
309 newQ.setPrefetchPath(prefetchPath);
310 newQ.setRootQuery(q);
311 Expression exp =
312 ExpressionFactory.unaryExp(Expression.OBJ_PATH, prefetchPath);
313 Iterator it = ent.resolvePathComponents(exp);
314 Relationship r = null;
315 int relCount = 0;
316 while (it.hasNext()) {
317 r = (Relationship) it.next();
318 relCount++;
319 }
320
321 if (r != null) {
322 ObjRelationship objR = (ObjRelationship) r;
323 newQ.setRoot(objR.getTargetEntity());
324 newQ.setQualifier(
325 transformQualifier(ent, q.getQualifier(), prefetchPath));
326 if ((relCount == 1) && objR.isToMany() && !objR.isFlattened()) {
327 //A one step toMany relationship needs the special handling
328 newQ.setSingleStepToManyRelationship(objR);
329 }
330 return newQ;
331 } else {
332 // TODO: what else could we do here?
333 return null;
334 }
335 }
336
337 /**
338 * Translates qualifier applicable for one ObjEntity into a
339 * qualifier for a related ObjEntity.
340 * @param ent the entity to which the original qualifier (<code>qual</code>)
341 * applies
342 * @param qual the qualifier on <code>ent</code>
343 * @param relPath a relationship path from <code>ent</code> to some target entity
344 * @return Expression which, when applied to the target entity of relPath,
345 * will give the union of the objects that would be obtained by following
346 * relPath from all of the objects in <code>ent</code> that match <code>qual</code>
347 */
348 public static Expression transformQualifier(
349 ObjEntity ent,
350 Expression qual,
351 String relPath) {
352 if (qual == null) {
353 return null;
354 }
355
356 ExpressionTranslator trans = new ExpressionTranslator(ent, relPath);
357 ExpressionTraversal parser = new ExpressionTraversal();
358 parser.setHandler(trans);
359 parser.traverseExpression(qual);
360
361 return trans.getPeer(qual);
362 }
363
364 /**
365 * Generates a SelectQuery that can be used to fetch
366 * relationship destination objects given a source object
367 * of a to-many relationship.
368 */
369 public static SelectQuery selectRelationshipObjects(
370 QueryEngine e,
371 DataObject source,
372 String relName) {
373
374 ObjEntity ent = e.getEntityResolver().lookupObjEntity(source);
375 ObjRelationship rel = (ObjRelationship) ent.getRelationship(relName);
376 ObjEntity destEnt = (ObjEntity) rel.getTargetEntity();
377
378 List dbRels = rel.getDbRelationships();
379
380 // sanity check
381 if (dbRels == null || dbRels.size() == 0) {
382 throw new CayenneRuntimeException(
383 "ObjRelationship '" + rel.getName() + "' is unmapped.");
384 }
385
386 // build a reverse DB path
387 // ...while reverse ObjRelationship may be absent,
388 // reverse DB must always be there...
389 StringBuffer buf = new StringBuffer();
390 ListIterator it = dbRels.listIterator(dbRels.size());
391 while (it.hasPrevious()) {
392 if (buf.length() > 0) {
393 buf.append(".");
394 }
395 DbRelationship dbRel = (DbRelationship) it.previous();
396 DbRelationship reverse = dbRel.getReverseRelationship();
397
398 // another sanity check
399 if (reverse == null) {
400 throw new CayenneRuntimeException(
401 "DbRelationship '"
402 + dbRel.getName()
403 + "' has no reverse relationship");
404 }
405
406 buf.append(reverse.getName());
407 }
408
409 SelectQuery sel = new SelectQuery(destEnt);
410 sel.setQualifier(
411 ExpressionFactory.binaryDbPathExp(
412 Expression.EQUAL_TO,
413 buf.toString(),
414 source));
415
416 return sel;
417 }
418
419 static final class ExpressionTranslator extends TraversalHelper {
420 protected Map expMap = new HashMap();
421 protected Map expFill = new HashMap();
422 protected ObjEntity ent;
423 protected String relationshipPath;
424 protected String relationshipDbPath;
425 protected String prependObjPath;
426 protected String prependDbPath;
427
428 public ExpressionTranslator(ObjEntity e, String relPath) {
429 this.ent = e;
430 this.relationshipPath = relPath;
431 this.relationshipDbPath = forwardDbPath(e, relPath);
432 this.prependObjPath = reversePath(e, relPath);
433 this.prependDbPath = reverseDbPath(e, relPath);
434 }
435
436 public Expression getPeer(Expression orig) {
437 return (Expression) expMap.get(orig);
438 }
439
440 private int getOperandIndex(Expression orig) {
441 Integer indObj = (Integer) expFill.get(orig);
442 int ind = (indObj != null) ? indObj.intValue() + 1 : 0;
443 expFill.put(orig, new Integer(ind));
444
445 return ind;
446 }
447
448 /**
449 * For a relationship path from source to target, builds a reverse path
450 * from target to source.
451 */
452 public String reversePath(ObjEntity e, String relPath) {
453 Expression exp =
454 ExpressionFactory.unaryExp(Expression.OBJ_PATH, relPath);
455 Iterator it = e.resolvePathComponents(exp);
456 StringBuffer buf = new StringBuffer();
457 boolean hasRels = false;
458
459 while (it.hasNext()) {
460 ObjRelationship rel = (ObjRelationship) it.next();
461 ObjRelationship reverse = rel.getReverseRelationship();
462 if (reverse == null) {
463 //Couldn't create reverse obj path because of a missing reverse
464 // relationship
465 return null;
466 }
467 if (hasRels) {
468 buf.insert(0, Entity.PATH_SEPARATOR);
469 }
470
471 buf.insert(0, reverse.getName());
472 hasRels = true;
473 }
474
475 return buf.toString();
476 }
477
478 public String forwardDbPath(ObjEntity e, String relPath) {
479 Expression exp =
480 ExpressionFactory.unaryExp(Expression.OBJ_PATH, relPath);
481 Iterator it = e.resolvePathComponents(exp);
482 StringBuffer buf = new StringBuffer();
483
484 while (it.hasNext()) {
485 ObjRelationship rel = (ObjRelationship) it.next();
486 Iterator dbRels = rel.getDbRelationships().iterator();
487 while (dbRels.hasNext()) {
488 DbRelationship r = (DbRelationship) dbRels.next();
489 if (buf.length() > 0) {
490 buf.append(Entity.PATH_SEPARATOR);
491 }
492
493 buf.append(r.getName());
494 }
495 }
496
497 return buf.toString();
498 }
499
500 /**
501 * For a relationship path from source to target, builds a reverse path
502 * from target to source.
503 */
504 public String reverseDbPath(ObjEntity e, String relPath) {
505 Expression exp =
506 ExpressionFactory.unaryExp(Expression.DB_PATH, relPath);
507 Iterator it = e.resolvePathComponents(exp);
508 StringBuffer buf = new StringBuffer();
509 boolean hasRels = false;
510
511 while (it.hasNext()) {
512 ObjRelationship rel = (ObjRelationship) it.next();
513
514 Iterator dbRels = rel.getDbRelationships().iterator();
515 while (dbRels.hasNext()) {
516 DbRelationship dbRel = (DbRelationship) dbRels.next();
517 DbRelationship reverse = dbRel.getReverseRelationship();
518
519 if (hasRels) {
520 buf.insert(0, Entity.PATH_SEPARATOR);
521 }
522
523 buf.insert(0, reverse.getName());
524 hasRels = true;
525 }
526 }
527
528 return buf.toString();
529 }
530
531 /**
532 * Creates expression of the same type and same operands
533 * as the original expression. Operands of the new expression
534 * are set to null.
535 */
536 public Expression createExpressionOfType(Expression e)
537 throws ExpressionException {
538 try {
539 Expression exp = (Expression) e.getClass().newInstance();
540 exp.setType(e.getType());
541 return exp;
542 } catch (Exception ex) {
543 throw new ExpressionException(
544 "Error instantiating expression.",
545 ex);
546 }
547 }
548
549 private void processOperand(Object operand, Expression parentNode) {
550 // attach operand to parent at index
551 if (parentNode != null) {
552 int ind = getOperandIndex(parentNode);
553 Expression parentPeer = getPeer(parentNode);
554 // operands of object expression need translation
555 if (parentPeer.getType() == Expression.OBJ_PATH) {
556 if (prependObjPath == null) {
557 //Change to a db path type expression,
558 // because there is no ObjPath
559 parentPeer.setType(Expression.DB_PATH);
560
561 Iterator it = ent.resolvePathComponents(parentNode);
562 Object lastComponent = null;
563 while (it.hasNext()) {
564 lastComponent = it.next();
565 }
566 if (lastComponent instanceof ObjAttribute) {
567 ObjAttribute objAttr = (ObjAttribute) lastComponent;
568 operand =
569 processPath(
570 objAttr.getDbAttribute().getName(),
571 relationshipDbPath,
572 prependDbPath);
573 } else {
574 //Not sure what to do... just hope that this
575 // will work I guess?
576 operand =
577 processPath(
578 (String) operand,
579 relationshipDbPath,
580 prependDbPath);
581
582 }
583
584 } else {
585 //There is a reverse obj path.. use it
586 operand =
587 processPath(
588 (String) operand,
589 relationshipPath,
590 prependObjPath);
591 }
592 } else if (parentPeer.getType() == Expression.DB_PATH) {
593 operand =
594 processPath(
595 (String) operand,
596 relationshipDbPath,
597 prependDbPath);
598 }
599 parentPeer.setOperand(ind, operand);
600 }
601 }
602
603 private String processPath(
604 String path,
605 String toPrefix,
606 String fromPrefix) {
607 if (path.equals(toPrefix)) {
608 // 1. Path ends with prefetch entity - match PK
609 throw new CayenneRuntimeException(
610 "Prefetching with path ending on "
611 + "prefetch entity is not supported yet.");
612 } else if (path.startsWith(toPrefix + Entity.PATH_SEPARATOR)) {
613 // 2. Path starts with prefetch entity - strip it.
614 return path.substring(
615 (toPrefix + Entity.PATH_SEPARATOR).length());
616 } else {
617 // 3. Path has nothing to do with prefetch entity - prepend rel from prefetch
618 return fromPrefix + Entity.PATH_SEPARATOR + path;
619 }
620 }
621
622 private void processNode(Expression node, Expression parentNode) {
623 Expression e = createExpressionOfType(node);
624 expMap.put(node, e);
625 processOperand(e, parentNode);
626 }
627
628 public void startUnaryNode(Expression node, Expression parentNode) {
629 processNode(node, parentNode);
630 }
631
632 public void startBinaryNode(Expression node, Expression parentNode) {
633 processNode(node, parentNode);
634 }
635
636 public void startTernaryNode(Expression node, Expression parentNode) {
637 processNode(node, parentNode);
638 }
639
640 public void objectNode(Object leaf, Expression parentNode) {
641 processOperand(leaf, parentNode);
642 }
643
644 public void startListNode(Expression node, Expression parentNode) {
645 processNode(node, parentNode);
646 }
647
648 }
649 }