001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010: package org.mmbase.bridge.util.xml.query;
011:
012: import java.util.*;
013: import org.w3c.dom.*;
014: import org.w3c.dom.NodeList;
015:
016: import org.mmbase.bridge.*;
017: import org.mmbase.bridge.util.Queries;
018: import org.mmbase.storage.search.*;
019: import org.mmbase.storage.search.implementation.BasicCompositeConstraint;
020: import org.mmbase.util.*;
021:
022: /**
023: * This class contains static methods related to creating a Query object using a (fragment of an) XML.
024: *
025: * @author Pierre van Rooden
026: * @version $Id: QueryReader.java,v 1.16 2008/01/29 10:06:22 pierre Exp $
027: * @since MMBase-1.8
028: **/
029: public abstract class QueryReader {
030:
031: public static final String XSD_SEARCHQUERY_1_0 = "searchquery.xsd";
032: public static final String NAMESPACE_SEARCHQUERY_1_0 = "http://www.mmbase.org/xmlns/searchquery";
033:
034: /** most recent version */
035: public static final String NAMESPACE_SEARCHQUERY = NAMESPACE_SEARCHQUERY_1_0;
036:
037: /**
038: * Register the namespace and XSD used by QueryReader
039: * This method is called by XMLEntityResolver.
040: */
041: public static void registerSystemIDs() {
042: XMLEntityResolver.registerSystemID(NAMESPACE_SEARCHQUERY_1_0
043: + ".xsd", XSD_SEARCHQUERY_1_0, QueryReader.class);
044: }
045:
046: /**
047: * Returns whether an element has a certain attribute, either an unqualified attribute or an attribute that fits in the
048: * searchquery namespace
049: */
050: static public boolean hasAttribute(Element element, String localName) {
051: return element.hasAttributeNS(NAMESPACE_SEARCHQUERY, localName)
052: || element.hasAttribute(localName);
053: }
054:
055: /**
056: * Returns the value of a certain attribute, either an unqualified attribute or an attribute that fits in the
057: * searchquery namespace
058: */
059: static public String getAttribute(Element element, String localName) {
060: if (element.hasAttributeNS(NAMESPACE_SEARCHQUERY, localName)) {
061: return element.getAttributeNS(NAMESPACE_SEARCHQUERY,
062: localName);
063: } else {
064: return element.getAttribute(localName);
065: }
066: }
067:
068: /* Expands a fieldname in a multilevel query with the element nodemanager step if no step is given.
069: */
070: protected static String getFullFieldName(
071: QueryDefinition queryDefinition, String fieldName) {
072: if (queryDefinition.isMultiLevel
073: && fieldName.indexOf('.') == -1) {
074: fieldName = queryDefinition.elementManager.getName() + "."
075: + fieldName;
076: }
077: return fieldName;
078: }
079:
080: protected static void addField(Element fieldElement,
081: QueryDefinition queryDefinition, QueryConfigurer configurer) {
082: if (hasAttribute(fieldElement, "name")) {
083: FieldDefinition fieldDefinition = configurer
084: .getFieldDefinition();
085: fieldDefinition.fieldName = getFullFieldName(
086: queryDefinition, fieldElement.getAttribute("name"));
087:
088: String opt = fieldElement.getAttribute("optional");
089:
090: if (opt.equals("")) {
091: try {
092: fieldDefinition.stepField = queryDefinition.query
093: .createStepField(fieldDefinition.fieldName);
094: } catch (IllegalArgumentException iae) {
095: // the field did not exist in the database.
096: // this is possible if the field is, for instance, a bytefield that is stored on disc.
097: fieldDefinition.stepField = null;
098: }
099: } else {
100: fieldDefinition.optional = java.util.regex.Pattern
101: .compile(opt);
102: }
103: // custom configuration of field
104: fieldDefinition.configure(fieldElement);
105: queryDefinition.fields.add(fieldDefinition);
106: if (queryDefinition.isMultiLevel
107: && fieldDefinition.optional == null) {
108: // have to add field for multilevel queries
109: if (!queryDefinition.query
110: .getFields()
111: .contains(
112: queryDefinition.query
113: .createStepField(fieldDefinition.fieldName))) {
114: queryDefinition.query
115: .addField(fieldDefinition.fieldName);
116: }
117: }
118: } else {
119: throw new IllegalArgumentException(
120: "field tag has no 'name' attribute");
121: }
122: }
123:
124: protected static Constraint getConstraint(
125: Element constraintElement, QueryDefinition queryDefinition) {
126: if (!hasAttribute(constraintElement, "field")) {
127: throw new IllegalArgumentException(
128: "A constraint tag must have a 'field' attribute");
129: }
130: String fieldName = getFullFieldName(queryDefinition,
131: getAttribute(constraintElement, "field"));
132: Object value = null;
133: if (hasAttribute(constraintElement, "value")) {
134: if (hasAttribute(constraintElement, "field2")) {
135: throw new IllegalArgumentException(
136: "A constraint tag can only have one of 'value' or 'field2'");
137: }
138: value = getAttribute(constraintElement, "value");
139: } else if (hasAttribute(constraintElement, "field2")) {
140: value = queryDefinition.query
141: .createStepField(getFullFieldName(queryDefinition,
142: getAttribute(constraintElement, "field2")));
143: }
144: int operator = FieldCompareConstraint.EQUAL;
145: if (hasAttribute(constraintElement, "operator")) {
146: String sOperator = getAttribute(constraintElement,
147: "operator");
148: operator = Queries.getOperator(sOperator);
149: }
150: int part = -1;
151: if (hasAttribute(constraintElement, "part")) {
152: String sPart = getAttribute(constraintElement, "part");
153: part = Queries.getDateTimePart(sPart);
154: }
155: Object value2 = null;
156: if (hasAttribute(constraintElement, "value2")) {
157: if (operator != Queries.OPERATOR_BETWEEN) {
158: throw new IllegalArgumentException(
159: "A constraint tag can only use 'value2' attribute with operator BETWEEN");
160: }
161: value2 = getAttribute(constraintElement, "value2");
162: }
163: if (operator == Queries.OPERATOR_BETWEEN && value2 == null) {
164: throw new IllegalArgumentException(
165: "Operator BETWEEN in a constraint tag requires attribute 'value2'");
166: }
167: if (operator == Queries.OPERATOR_IN
168: && (value instanceof String)) {
169: value = Casting.toList(value);
170: }
171: boolean caseSensitive = false;
172: if (hasAttribute(constraintElement, "casesensitive")) {
173: caseSensitive = "true".equals(getAttribute(
174: constraintElement, "casesensitive"));
175: }
176: return Queries
177: .createConstraint(queryDefinition.query, fieldName,
178: operator, value, value2, caseSensitive, part);
179: }
180:
181: protected static int getDayMark(Cloud cloud, int age) {
182: // find day mark
183: NodeManager dayMarks = cloud.getNodeManager("daymarks");
184: NodeQuery query = dayMarks.createQuery();
185: StepField step = query.createStepField("daycount");
186: int currentDay = (int) (System.currentTimeMillis() / (1000 * 60 * 60 * 24));
187: int day = currentDay - age;
188: Constraint constraint = query
189: .createConstraint(step,
190: FieldCompareConstraint.LESS_EQUAL, Integer
191: .valueOf(day));
192: query.setConstraint(constraint);
193: query.addSortOrder(query.createStepField("daycount"),
194: SortOrder.ORDER_DESCENDING);
195: query.setMaxNumber(1);
196:
197: org.mmbase.bridge.NodeList result = dayMarks.getList(query);
198: int daymark = -1;
199: if (result.size() > 0) {
200: daymark = result.getNode(0).getIntValue("mark");
201: }
202: return daymark;
203: }
204:
205: protected static Constraint getAgeConstraint(
206: Element constraintElement, QueryDefinition queryDefinition) {
207: // find day mark
208: int minAge = -1;
209: if (hasAttribute(constraintElement, "minage")) {
210: minAge = Integer.parseInt(getAttribute(constraintElement,
211: "minage"));
212: }
213: int maxAge = -1;
214: if (hasAttribute(constraintElement, "maxage")) {
215: maxAge = Integer.parseInt(getAttribute(constraintElement,
216: "maxage"));
217: }
218: if (minAge < 0 && maxAge < 0) {
219: throw new IllegalArgumentException(
220: "Either 'minage' or 'maxage' (or both) attributes must be present");
221: }
222: StepField stepField = null;
223: String fieldName = "number";
224: if (hasAttribute(constraintElement, "element")) {
225: if (hasAttribute(constraintElement, "field")) {
226: throw new IllegalArgumentException(
227: "Can not specify both 'field' and 'element' attributes on ageconstraint");
228: }
229: fieldName = getAttribute(constraintElement, "element")
230: + ".number";
231: stepField = queryDefinition.query
232: .createStepField(fieldName);
233: } else if (hasAttribute(constraintElement, "field")) {
234: fieldName = getFullFieldName(queryDefinition, getAttribute(
235: constraintElement, "field"));
236: stepField = queryDefinition.query
237: .createStepField(fieldName);
238: } else {
239: if (queryDefinition.elementStep != null) {
240: stepField = queryDefinition.query.createStepField(
241: queryDefinition.elementStep, "number");
242: } else {
243: throw new IllegalArgumentException(
244: "Don't know on what path element the ageconstraint must be applied. Use the 'element' attribute");
245: }
246: }
247:
248: Constraint constraint = null;
249: // if minimal age given:
250: // you need the day marker of the day after that (hence -1 in code below inside the getDayMark), node have to have this number or lower
251: // if maximal age given:
252: // daymarker object of that age must be included, but last object of previous day not, hece the +1 outside the getDayMark
253:
254: Cloud cloud = queryDefinition.query.getCloud();
255: if (maxAge != -1 && minAge > 0) {
256: int maxMarker = getDayMark(cloud, maxAge);
257: if (maxMarker > 0) {
258: // BETWEEN constraint
259: constraint = queryDefinition.query.createConstraint(
260: stepField, Integer.valueOf(maxMarker + 1),
261: Integer.valueOf(getDayMark(cloud, minAge - 1)));
262: } else {
263: constraint = queryDefinition.query.createConstraint(
264: stepField, FieldCompareConstraint.LESS_EQUAL,
265: Integer.valueOf(getDayMark(cloud, minAge - 1)));
266: }
267: } else if (maxAge != -1) { // only on max
268: int maxMarker = getDayMark(cloud, maxAge);
269: if (maxMarker > 0) {
270: constraint = queryDefinition.query.createConstraint(
271: stepField,
272: FieldCompareConstraint.GREATER_EQUAL, Integer
273: .valueOf(maxMarker + 1));
274: }
275: } else if (minAge > 0) {
276: constraint = queryDefinition.query.createConstraint(
277: stepField, FieldCompareConstraint.LESS_EQUAL,
278: Integer.valueOf(getDayMark(cloud, minAge - 1)));
279: }
280: return constraint;
281: }
282:
283: protected static Integer getAlias(Cloud cloud, String name) {
284: org.mmbase.bridge.Node node = cloud.getNode(name);
285: return node.getNumber();
286: }
287:
288: protected static SortedSet<Integer> getAliases(Cloud cloud,
289: List<String> names) {
290: SortedSet<Integer> set = new TreeSet<Integer>();
291: for (String name : names) {
292: set.add(getAlias(cloud, name));
293: }
294: return set;
295: }
296:
297: protected static Constraint getAliasConstraint(
298: Element constraintElement, QueryDefinition queryDefinition) {
299: if (!hasAttribute(constraintElement, "name")) {
300: throw new IllegalArgumentException(
301: "An aliasconstraint tag must have a 'name' attribute");
302: }
303: String elementString = getAttribute(constraintElement,
304: "element");
305: Step step = queryDefinition.elementStep;
306: if (elementString != null || !elementString.equals("")) {
307: step = queryDefinition.query.getStep(elementString);
308: }
309: if (step == null) {
310: throw new IllegalArgumentException(
311: "Don't know on what path element the aliasconstraint must be applied. Use the 'element' attribute");
312: }
313: StepField stepField = queryDefinition.query.createStepField(
314: step, "number");
315:
316: String name = getAttribute(constraintElement, "name");
317: List<String> names = Casting.toList(name);
318: return queryDefinition.query.createConstraint(stepField,
319: getAliases(queryDefinition.query.getCloud(), names));
320: }
321:
322: protected static SortedSet<Integer> getOTypes(Cloud cloud,
323: List<String> names, boolean descendants) {
324: SortedSet<Integer> set = new TreeSet<Integer>();
325: Iterator<String> i = names.iterator();
326: while (i.hasNext()) {
327: NodeManager nm = cloud.getNodeManager(i.next());
328: set.add(nm.getNumber());
329: if (descendants) {
330: NodeManagerIterator j = nm.getDescendants()
331: .nodeManagerIterator();
332: while (j.hasNext()) {
333: set.add(j.nextNodeManager().getNumber());
334: }
335: }
336: }
337: return set;
338: }
339:
340: protected static Constraint getTypeConstraint(
341: Element constraintElement, QueryDefinition queryDefinition) {
342: if (!hasAttribute(constraintElement, "name")) {
343: throw new IllegalArgumentException(
344: "A typeconstraint tag must have a 'name' attribute");
345: }
346: String elementString = getAttribute(constraintElement,
347: "element");
348: Step step = queryDefinition.elementStep;
349: if (elementString != null || !elementString.equals("")) {
350: step = queryDefinition.query.getStep(elementString);
351: }
352: if (step == null) {
353: throw new IllegalArgumentException(
354: "Don't know on what path element the type constraint must be applied. Use the 'element' attribute");
355: }
356: StepField stepField = queryDefinition.query.createStepField(
357: step, "otype");
358: String name = getAttribute(constraintElement, "name");
359: List<String> names = Casting.toList(name);
360: boolean descendants = true;
361: if (hasAttribute(constraintElement, "descendants")) {
362: descendants = "true".equals(getAttribute(constraintElement,
363: "descendants"));
364: }
365: return queryDefinition.query.createConstraint(stepField,
366: getOTypes(queryDefinition.query.getCloud(), names,
367: descendants));
368: }
369:
370: protected static Constraint getCompositeConstraint(
371: Element constraintElement, QueryDefinition queryDefinition)
372: throws SearchQueryException {
373: int operator = CompositeConstraint.LOGICAL_AND;
374: if (hasAttribute(constraintElement, "operator")) {
375: String sOperator = getAttribute(constraintElement,
376: "operator");
377: if (sOperator != null
378: && sOperator.toUpperCase().equals("OR")) {
379: operator = CompositeConstraint.LOGICAL_OR;
380: }
381: }
382: CompositeConstraint constraint = new BasicCompositeConstraint(
383: operator);
384: if (hasAttribute(constraintElement, "inverse")) {
385: queryDefinition.query
386: .setInverse(constraint, "true".equals(getAttribute(
387: constraintElement, "inverse")));
388: }
389: NodeList childNodes = constraintElement.getChildNodes();
390: for (int k = 0; k < childNodes.getLength(); k++) {
391: if (childNodes.item(k) instanceof Element) {
392: Element childElement = (Element) childNodes.item(k);
393: addConstraint(childElement, queryDefinition, constraint);
394: }
395: }
396: return constraint;
397: }
398:
399: protected static void addConstraint(Element constraintElement,
400: QueryDefinition queryDefinition,
401: CompositeConstraint parentConstraint)
402: throws SearchQueryException {
403: Constraint constraint = null;
404: if ("constraint".equals(constraintElement.getLocalName())) {
405: constraint = getConstraint(constraintElement,
406: queryDefinition);
407: } else if ("ageconstraint".equals(constraintElement
408: .getLocalName())) {
409: constraint = getAgeConstraint(constraintElement,
410: queryDefinition);
411: } else if ("aliasconstraint".equals(constraintElement
412: .getLocalName())) {
413: constraint = getAliasConstraint(constraintElement,
414: queryDefinition);
415: } else if ("typeconstraint".equals(constraintElement
416: .getLocalName())) {
417: constraint = getTypeConstraint(constraintElement,
418: queryDefinition);
419: } else if ("compositeconstraint".equals(constraintElement
420: .getLocalName())) {
421: constraint = getCompositeConstraint(constraintElement,
422: queryDefinition);
423: }
424: if (constraint != null) {
425: if (hasAttribute(constraintElement, "inverse")) {
426: queryDefinition.query.setInverse(constraint, "true"
427: .equals(getAttribute(constraintElement,
428: "inverse")));
429: }
430: if (parentConstraint != null) {
431: ((BasicCompositeConstraint) parentConstraint)
432: .addChild(constraint);
433: } else {
434: Queries
435: .addConstraint(queryDefinition.query,
436: constraint);
437: }
438: }
439: }
440:
441: protected static void addDistinct(Element distinctElement,
442: QueryDefinition queryDefinition) {
443: boolean distinct = true;
444: if (hasAttribute(distinctElement, "value")) {
445: distinct = "true".equals(getAttribute(distinctElement,
446: "value"));
447: }
448: queryDefinition.query.setDistinct(distinct);
449: }
450:
451: protected static void addSortOrder(Element sortOrderElement,
452: QueryDefinition queryDefinition) {
453: if (!hasAttribute(sortOrderElement, "field")) {
454: throw new IllegalArgumentException(
455: "A sortorder tag must have a 'field' attribute");
456: }
457: StepField stepField = queryDefinition.query
458: .createStepField(getFullFieldName(queryDefinition,
459: getAttribute(sortOrderElement, "field")));
460: int order = SortOrder.ORDER_ASCENDING;
461: if (hasAttribute(sortOrderElement, "direction")) {
462: order = Queries.getSortOrder(getAttribute(sortOrderElement,
463: "direction"));
464: }
465: boolean casesensitive = false;
466: if (hasAttribute(sortOrderElement, "casesensitive")) {
467: casesensitive = "true".equals(getAttribute(
468: sortOrderElement, "casesensitive"));
469: }
470: queryDefinition.query.addSortOrder(stepField, order,
471: casesensitive);
472: }
473:
474: /**
475: * As {@link #parseQuery(Element, QueryConfigurer, Cloud, String)}, but with default QueryConfigurer
476: */
477: static public QueryDefinition parseQuery(Element queryElement,
478: Cloud cloud, String relateFrom) throws SearchQueryException {
479: return parseQuery(queryElement, null, cloud, relateFrom);
480: }
481:
482: /**
483: * Creates a Query object from an Element. The query is wrapped in a {@link QueryDefinition} and
484: * you can simply access the {@link QueryDefinition#query} member to have the actual Query.
485: *
486: * @param queryElement Any XML element which query sub-tags and attributes.
487: * @param configurer The configure which is responsible for instantiating the QueryDefinition
488: * @param cloud Cloud, needed to make Query objects.
489: * @param relateFrom (optional) name of a node manager which can be used to base the query on, as the first element of the path (can be <code>null</code>)
490: */
491:
492: static public QueryDefinition parseQuery(Element queryElement,
493: QueryConfigurer configurer, Cloud cloud, String relateFrom)
494: throws SearchQueryException {
495: if (configurer == null) {
496: configurer = QueryConfigurer.getDefaultConfigurer();
497: }
498: if (hasAttribute(queryElement, "type")
499: || hasAttribute(queryElement, "name")
500: || hasAttribute(queryElement, "path")) {
501:
502: String element = null;
503: String path = null;
504: String searchDirs = null;
505:
506: if (hasAttribute(queryElement, "type")) {
507: path = getAttribute(queryElement, "type");
508: element = path;
509: } else if (hasAttribute(queryElement, "name")) {
510: path = getAttribute(queryElement, "name");
511: element = path;
512: } else {
513: path = getAttribute(queryElement, "path");
514: searchDirs = getAttribute(queryElement, "searchdirs");
515: if (hasAttribute(queryElement, "element")) {
516: element = getAttribute(queryElement, "element");
517: } else {
518: List<String> builders = StringSplitter.split(path);
519: element = builders.get(builders.size() - 1);
520: }
521: }
522: if (relateFrom != null) {
523: path = relateFrom + "," + path;
524: }
525:
526: QueryDefinition queryDefinition = configurer
527: .getQueryDefinition();
528: queryDefinition.isMultiLevel = !path.equals(element);
529:
530: if (element != null) {
531: queryDefinition.elementManager = cloud
532: .getNodeManager(Queries.removeDigits(element));
533: }
534: if (queryDefinition.isMultiLevel) {
535: queryDefinition.query = cloud.createQuery();
536: Queries
537: .addPath(queryDefinition.query, path,
538: searchDirs);
539: } else {
540: queryDefinition.query = queryDefinition.elementManager
541: .createQuery();
542: }
543: if (element != null) {
544: queryDefinition.elementStep = queryDefinition.query
545: .getStep(element);
546: }
547: if (queryDefinition.fields == null)
548: queryDefinition.fields = new ArrayList<FieldDefinition>();
549:
550: if (hasAttribute(queryElement, "startnodes")) {
551: String startNodes = getAttribute(queryElement,
552: "startnodes");
553: Queries
554: .addStartNodes(queryDefinition.query,
555: startNodes);
556: }
557:
558: // custom configurations to the query
559: queryDefinition.configure(queryElement);
560:
561: NodeList childNodes = queryElement.getChildNodes();
562: for (int k = 0; k < childNodes.getLength(); k++) {
563: if (childNodes.item(k) instanceof Element) {
564: Element childElement = (Element) childNodes.item(k);
565: if ("field".equals(childElement.getLocalName())) {
566: addField(childElement, queryDefinition,
567: configurer);
568: } else if ("distinct".equals(childElement
569: .getLocalName())) {
570: addDistinct(childElement, queryDefinition);
571: } else if ("sortorder".equals(childElement
572: .getLocalName())) {
573: addSortOrder(childElement, queryDefinition);
574: } else {
575: addConstraint(childElement, queryDefinition,
576: null);
577: }
578: }
579: }
580: return queryDefinition;
581: } else {
582: throw new IllegalArgumentException(
583: "query has no 'path' or 'type' attribute");
584: }
585: }
586:
587: }
|