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.core.util;
011:
012: import java.util.*;
013:
014: import org.mmbase.bridge.Field;
015: import org.mmbase.bridge.NodeQuery;
016: import org.mmbase.cache.*;
017: import org.mmbase.core.CoreField;
018: import org.mmbase.module.core.*;
019: import org.mmbase.storage.*;
020: import org.mmbase.storage.util.Index;
021: import org.mmbase.storage.search.*;
022: import org.mmbase.storage.search.implementation.*;
023: import org.mmbase.util.QueryConvertor;
024: import org.mmbase.util.logging.Logger;
025: import org.mmbase.util.logging.Logging;
026:
027: /**
028: * A StorageConnector object is associated with a specific builder.
029: * It provides methods for loading nodes from the cloud (using the search query classes),
030: * either indivbidual nodes or nodelists.
031: *
032: * @since MMBase-1.8
033: * @author Pierre van Rooden
034: * @version $Id: StorageConnector.java,v 1.19 2007/08/21 14:53:10 michiel Exp $
035: */
036: public class StorageConnector {
037:
038: /**
039: * Max length of a query, informix = 32.0000 so we assume a bit less for other databases (???).
040: */
041: private static final int MAX_QUERY_SIZE = 20000;
042:
043: private static final Logger log = Logging
044: .getLoggerInstance(StorageConnector.class);
045:
046: /**
047: * Determines whether the cache need be refreshed.
048: * Seems useless, as this value is never changed (always true)
049: * @see #processSearchResults
050: */
051: /*
052: public static final boolean REPLACE_CACHE = true;
053: */
054:
055: /**
056: * Whenever a list should always return the correct types of nodes
057: * old behaviour is not...
058: * This is needed, when you want for example use the following code:
059: * <pre>
060: * MMObjectNode node = MMObjectBuilder.getNode(123);
061: * Enumeration relations = node.getRelations("posrel");
062: * while(enumeration.hasNext()) {
063: * MMObjectNode posrel = (MMObjectNode) enumeration.getElement();
064: * int pos = posrel.getIntValue("pos");
065: * }
066: * </pre>
067: * When the return of correct node types is the following code has to be used..
068: * <pre>
069: * MMObjectNode node = MMObjectBuilder.getNode(123);
070: * Enumeration relations = node.getRelations("posrel");
071: * while(enumeration.hasNext()) {
072: * MMObjectNode posrel = (MMObjectNode) enumeration.getElement();
073: * // next lines is needed when the return of correct nodes is not true
074: * posrel = posrel.parent.getNode(posrel.getNumber());
075: * // when the line above is skipped, the value of pos will always be -1
076: * int pos = posrel.getIntValue("pos");
077: * }
078: * </pre>
079: * Maybe this should be fixed in some otherway,.. but when we want to use the inheritance you
080: * _really_ need this thing turned into true.
081: */
082: /*
083: private static boolean CORRECT_NODE_TYPES = true;
084: */
085:
086: /**
087: * Maximum number of nodes to return on a query (-1 means no limit, and is also the default)
088: */
089: protected int maxNodesFromQuery = -1;
090:
091: /**
092: * @javadoc
093: */
094: protected final MMObjectBuilder builder;
095:
096: // indices for the storage layer
097: private Map<String, Index> indices = new HashMap<String, Index>();
098:
099: /**
100: * @javadoc
101: */
102: public StorageConnector(MMObjectBuilder builder) {
103: this .builder = builder;
104: }
105:
106: /**
107: * Determine the number of objects in this table.
108: * @return The number of entries in the table.
109: */
110: public int size() {
111: try {
112: return builder.getMMBase().getStorageManager()
113: .size(builder);
114: } catch (StorageException se) {
115: log.error(se.getMessage());
116: return -1;
117: }
118: }
119:
120: /**
121: * Check whether the table is accessible.
122: * In general, this means the table does not exist. Please note that this routine may
123: * also return false if the table is inaccessible due to insufficient rights.
124: * @return <code>true</code> if the table is accessible, <code>false</code> otherwise.
125: */
126: public boolean created() {
127: try {
128: return builder.getMMBase().getStorageManager().exists(
129: builder);
130: } catch (StorageException se) {
131: log.error(se.getMessage() + Logging.stackTrace(se));
132: return false;
133: }
134: }
135:
136: public Map<String, Index> getIndices() {
137: return indices;
138: }
139:
140: public void addIndex(Index index) {
141: if (index != null && index.getParent() == builder) {
142: indices.put(index.getName(), index);
143: }
144: }
145:
146: public void addIndices(List<Index> indexList) {
147: if (indexList != null) {
148: for (Index i : indexList) {
149: addIndex(i);
150: }
151: }
152: }
153:
154: public Index getIndex(String key) {
155: return indices.get(key);
156: }
157:
158: public synchronized Index createIndex(String key) {
159: Index index = getIndex(key);
160: if (index == null) {
161: index = new Index(builder, key);
162: indices.put(key, index);
163: }
164: return index;
165: }
166:
167: public void addToIndex(String key, Field field) {
168: createIndex(key).add(field);
169: }
170:
171: public void removeFromIndex(String key, Field field) {
172: Index index = createIndex(key);
173: if (index != null) {
174: index.remove(field);
175: }
176: }
177:
178: public boolean isInIndex(String key, Field field) {
179: Index index = getIndex(key);
180: return index != null && index.contains(field);
181: }
182:
183: // retrieve nodes
184: /**
185: * Retrieves a node based on it's number (a unique key).
186: * @todo when something goes wrong, the method currently catches the exception and returns null.
187: * It should actually throw a NotFoundException instead.
188: * @param number The number of the node to search for
189: * @param useCache If false, a fresh copy is returned.
190: * @return <code>null</code> if the node does not exist, the key is invalid,or a
191: * <code>MMObjectNode</code> containing the contents of the requested node.
192: */
193: public MMObjectNode getNode(final int number, final boolean useCache)
194: throws StorageException {
195: if (log.isDebugEnabled()) {
196: log.trace("Getting node with number " + number);
197: }
198: if (number < 0) {
199: throw new IllegalArgumentException(
200: "Tried to obtain node from builder '"
201: + builder.getTableName()
202: + "' with an illegal number = " + number);
203: }
204: MMObjectNode node = null;
205:
206: Integer numberValue = Integer.valueOf(number);
207: // try cache if indicated to do so
208: node = builder.getNodeFromCache(numberValue);
209: if (node != null) {
210: log.trace("Found in cache!");
211: if (useCache) {
212: return node;
213: } else {
214: return new MMObjectNode(node);
215: }
216: }
217:
218: MMBase mmb = builder.getMMBase();
219: // not in cache. We are going to put it in.
220: // retrieve node's objecttype
221: MMObjectBuilder nodeBuilder = getBuilderForNode(number);
222: // use storage factory if present
223: log.debug("Getting node from storage");
224: node = mmb.getStorageManager().getNode(nodeBuilder, number);
225: if (nodeBuilder == mmb.getInsRel()
226: && node.getOType() != nodeBuilder.getObjectType()) {
227: // the builder was unknown en we falled back to insrel.
228: // Perhaps it would have been better to fall back to object?
229: if (node.getNumber() <= 0) {
230: node = mmb.getStorageManager().getNode(
231: mmb.getRootBuilder(), number);
232: }
233: }
234: // store in cache if indicated to do so
235: if (useCache) {
236: if (log.isDebugEnabled()) {
237: log.debug("Caching node from storage" + node);
238: }
239: node = builder.safeCache(numberValue, node);
240: }
241: if (log.isDebugEnabled()) {
242: log.debug("Returning " + node);
243: }
244: if (useCache) {
245: return node;
246: } else {
247: return new MMObjectNode(node);
248: }
249: }
250:
251: private final Set<Integer> warnedBuilders = new HashSet<Integer>();
252:
253: public MMObjectBuilder getBuilderForNode(final int number) {
254: MMBase mmb = builder.getMMBase();
255: MMObjectBuilder nodeBuilder = builder;
256: int nodeType = getNodeType(number);
257: if (nodeType < 0) {
258: // the node does not exists, which according to javadoc should return null
259: throw new StorageNotFoundException(
260: "Cannot determine node type of node with number ="
261: + number);
262: }
263: // if the type is not for the current builder, determine the real builder
264: if (nodeType != builder.getNumber()) {
265: if (log.isDebugEnabled()) {
266: log.debug(" " + nodeType + "!=" + builder.getNumber());
267: }
268: String builderName = mmb.getTypeDef().getValue(nodeType);
269: if (builderName == null) {
270: log
271: .error("The nodetype name of node #"
272: + number
273: + " could not be found (nodetype # "
274: + nodeType
275: + "), taking '"
276: + builder.getTableName()
277: + "' (more errors of this kind are logged on debug)");
278: builderName = builder.getTableName();
279: }
280: nodeBuilder = mmb.getBuilder(builderName);
281: if (nodeBuilder == null) {
282: if (builderName.endsWith("rel")) {
283: nodeBuilder = mmb.getInsRel();
284: } else {
285: nodeBuilder = mmb.getRootBuilder();
286: }
287: if (!warnedBuilders.contains(nodeType)) {
288: log.warn("Builder " + builderName + "(" + nodeType
289: + ") is not loaded, taking "
290: + nodeBuilder.getTableName());
291: warnedBuilders.add(nodeType);
292: }
293: log.debug("Node #" + number + "'s builder "
294: + builderName + "(" + nodeType
295: + ") is not loaded. Taking "
296: + nodeBuilder.getTableName());
297:
298: }
299: }
300: return nodeBuilder;
301: }
302:
303: /**
304: * Retrieves an object's type. If necessary, the type is added to the cache.
305: * @todo when something goes wrong, the method currently catches the exception and returns -1.
306: * It should actually throw a NotFoundException instead.
307: * @param number The number of the node to search for
308: * @return an <code>int</code> value which is the object type (otype) of the node.
309: */
310: public int getNodeType(int number) throws StorageException {
311: if (number < 0) {
312: throw new IllegalArgumentException(
313: "node number was invalid (" + number + " < 0)");
314: } else {
315: return builder.getMMBase().getStorageManager().getNodeType(
316: number);
317: }
318: }
319:
320: // Search and query methods on a table
321:
322: /**
323: * Convert virtual nodes to real nodes based on their otype
324: *
325: * Normally a multirelations-search will return virtual nodes. These nodes
326: * will only contain values which where specified in the field-vector.
327: * This method will make real nodes of those virtual nodes.
328: *
329: * @param virtuals containing virtual nodes
330: * @return List containing real nodes, directly from this Builders
331: */
332: public List<MMObjectNode> getNodes(Collection<MMObjectNode> virtuals)
333: throws SearchQueryException {
334: List<MMObjectNode> result = new ArrayList<MMObjectNode>();
335:
336: int numbersSize = 0;
337: NodeSearchQuery query = new NodeSearchQuery(builder);
338: BasicStep step = (BasicStep) query.getSteps().get(0); // casting is ugly !!
339:
340: List<Integer> subResult = new ArrayList<Integer>();
341:
342: for (MMObjectNode node : virtuals) {
343: // check if this node is already in cache
344: Integer number = node.getNumber();
345: if (builder.isNodeCached(number)) {
346: result.add(builder.getNodeFromCache(number));
347: // else seek it with a search on builder in db
348: } else {
349: numbersSize += ("," + number).length();
350: subResult.add(number);
351: step.addNode(number.intValue());
352: }
353:
354: if (numbersSize > MAX_QUERY_SIZE) {
355: addSubResult(query, subResult, result);
356: query = new NodeSearchQuery(builder);
357: step = (BasicStep) query.getSteps().get(0);
358: numbersSize = 0;
359: subResult.clear();
360: }
361: }
362:
363: // now that we have a comma seperated string of numbers, we can
364: // the search with a where-clause containing this list
365: if (numbersSize > 0) {
366: addSubResult(query, subResult, result);
367: } // else everything from cache
368:
369: // check that we didnt loose any nodes
370: assert assertSizes(virtuals, result);
371:
372: return result;
373: }
374:
375: /**
376: * @param query Query with nodestep with added nodes.
377: * @param subResult List of Integer
378: * @param result List to which the real nodes must be added.
379: * @since MMBase-1.8.2
380: */
381: protected void addSubResult(final NodeSearchQuery query,
382: final List<Integer> subResult,
383: final List<MMObjectNode> result)
384: throws SearchQueryException {
385: List<MMObjectNode> rawNodes = getRawNodes(query, true);
386: // convert this list to a map, for easy reference when filling result.
387: // would the creation of this Map not somehow be avoidable?
388: Map<Integer, MMObjectNode> rawMap = new HashMap<Integer, MMObjectNode>();
389: for (MMObjectNode n : rawNodes) {
390: rawMap.put(n.getNumber(), n);
391: }
392: for (Integer n : subResult) {
393: result.add(rawMap.get(n));
394: }
395: }
396:
397: /**
398: * @since MMBase-1.8.2
399: */
400: protected boolean assertSizes(Collection<MMObjectNode> virtuals,
401: Collection<MMObjectNode> result) {
402: if (virtuals.size() != result.size()) {
403: log.error(" virtuals " + virtuals + " result " + result);
404: return false;
405: } else {
406: return true;
407: }
408: }
409:
410: /**
411: * Counts number of nodes matching a specified constraint.
412: * The constraint is specified by a query that selects nodes of
413: * a specified type, which must be the nodetype corresponding
414: * to this builder.
415: *
416: * @param query The query.
417: * @return The number of nodes.
418: * @throws IllegalArgumentException when an invalid argument is supplied.
419: * @throws SearchQueryException when failing to retrieve the data.
420: */
421: public int count(SearchQuery query) throws SearchQueryException {
422: // Test if nodetype corresponds to builder.
423: verifyBuilderQuery(query);
424:
425: // Wrap in modifiable query, replace fields by one count field.
426: ModifiableQuery modifiedQuery = new ModifiableQuery(query);
427: Step step = query.getSteps().get(0);
428: CoreField numberField = builder
429: .getField(MMObjectBuilder.FIELD_NUMBER);
430: AggregatedField field = new BasicAggregatedField(step,
431: numberField, AggregatedField.AGGREGATION_TYPE_COUNT);
432: List<StepField> newFields = new ArrayList<StepField>(1);
433: newFields.add(field);
434: modifiedQuery.setFields(newFields);
435:
436: AggregatedResultCache cache = AggregatedResultCache.getCache();
437:
438: List<MMObjectNode> results = cache.get(modifiedQuery);
439: if (results == null) {
440: // Execute query, return result.
441: results = builder.getMMBase().getSearchQueryHandler()
442: .getNodes(
443: modifiedQuery,
444: new ResultBuilder(builder.getMMBase(),
445: modifiedQuery));
446: cache.put(modifiedQuery, results);
447: }
448: ResultNode result = (ResultNode) results.get(0);
449: return result.getIntValue(MMObjectBuilder.FIELD_NUMBER);
450: }
451:
452: private void verifyBuilderQuery(SearchQuery query) {
453: String builderName = null;
454: if (query instanceof NodeQuery) {
455: builderName = ((NodeQuery) query).getNodeManager()
456: .getName();
457: } else if (query instanceof NodeSearchQuery) {
458: builderName = ((NodeSearchQuery) query).getBuilder()
459: .getTableName();
460: }
461: if (builderName != null
462: && !builderName.equals(builder.getTableName())) {
463: throw new IllegalArgumentException("Query passed runs on '"
464: + builderName + "' but was passed to '"
465: + builder.getTableName() + "'");
466: }
467: }
468:
469: /**
470: * Returns the Cache which should be used for the result of a certain query. The current
471: * implementation only makes the distinction between queries for the 'related nodes caches' and
472: * for the 'node list caches'. Multilevel queries are not done here, so are at the moment not
473: * anticipated.
474: *
475: * It returns a Map rather then a Cache. The idea behind this is that if in the future a
476: * query-result can be in more than one cache, a kind of 'chained map' can be returned, to
477: * reflect that.
478: * @todo Perhaps other usefull parameters like query-duration and query-result could be added
479: * (in that case searching a result should certainly returns such a chained map, because then of
480: * course you don't have those).
481: */
482: protected QueryResultCache getCache(SearchQuery query) {
483: List<Step> steps = query.getSteps();
484: if (steps.size() == 3) {
485: Step step0 = steps.get(0);
486: Collection<Integer> nodes = step0.getNodes();
487: if (nodes != null && nodes.size() == 1) {
488: return RelatedNodesCache.getCache();
489: }
490: }
491: return NodeListCache.getCache();
492:
493: }
494:
495: /**
496: * Returns nodes matching a specified constraint.
497: * The constraint is specified by a query that selects nodes of
498: * a specified type, which must be the nodetype corresponding
499: * to this builder.
500: *
501: * Cache is used, but not filled (because this function is used to calculate subresults)
502: *
503: * @param query The query.
504: * @param useCache if true, the querycache is used
505: * @return The nodes.
506: * @throws IllegalArgumentException When the nodetype specified
507: * by the query is not the nodetype corresponding to this builder.
508: */
509: private List<MMObjectNode> getRawNodes(SearchQuery query,
510: boolean useCache) throws SearchQueryException {
511: // Test if nodetype corresponds to builder.
512: verifyBuilderQuery(query);
513: List<MMObjectNode> results = useCache ? getCache(query).get(
514: query) : null;
515:
516: // if unavailable, obtain from storage
517: if (results == null) {
518: log.debug("result list is null, getting from storage");
519: results = builder.getMMBase().getSearchQueryHandler()
520: .getNodes(query, builder);
521: } else {
522: if (log.isDebugEnabled()) {
523: log.debug("Found from cache"
524: + getCache(query).getName() + " " + results);
525: }
526: }
527: return results;
528: }
529:
530: /**
531: * Returns nodes matching a specified constraint.
532: * The constraint is specified by a query that selects nodes of
533: * a specified type, which must be the nodetype corresponding
534: * to this builder.
535: *
536: * @param query The query.
537: * @return The nodes.
538: * @throws IllegalArgumentException When the nodetype specified
539: * by the query is not the nodetype corresponding to this builder.
540: */
541: public List<MMObjectNode> getNodes(SearchQuery query)
542: throws SearchQueryException {
543: return getNodes(query, true);
544: }
545:
546: /**
547: * Returns nodes matching a specified constraint.
548: * The constraint is specified by a query that selects nodes of
549: * a specified type, which must be the nodetype corresponding
550: * to this builder.
551: *
552: * @param query The query.
553: * @param useCache if true, the querycache is used
554: * @return The nodes.
555: * @throws IllegalArgumentException When the nodetype specified
556: * by the query is not the nodetype corresponding to this builder.
557: */
558: public List<MMObjectNode> getNodes(SearchQuery query,
559: boolean useCache) throws SearchQueryException {
560: List<MMObjectNode> results = getRawNodes(query, useCache);
561: // TODO (later): implement maximum set by maxNodesFromQuery?
562: // Perform necessary postprocessing.
563: processSearchResults(results);
564: if (useCache) {
565: getCache(query).put(query, results);
566: }
567: return results;
568: }
569:
570: /**
571: * Returns all the nodes from the associated builder.
572: * @return The nodes.
573: */
574: public List<MMObjectNode> getNodes() throws SearchQueryException {
575: return getNodes(new NodeSearchQuery(builder));
576: }
577:
578: /**
579: * Performs some necessary postprocessing on nodes retrieved from a
580: * search query.
581: * This consists of the following actions:
582: * <ul>
583: * <li>Stores retrieved nodes in the node cache, or
584: * <li>Replace partially retrieved nodes in the result by complete nodes.
585: * Nodes are partially retrieved when their type is a inheriting type
586: * of this builder's type, having additional fields. For these nodes
587: * additional queries are performed to retrieve the complete nodes.
588: * <li>Removes nodes with invalid node number from the result.
589: * </ul>
590: *
591: * @param results The nodes. After returning, partially retrieved nodes
592: * in the result are replaced <em>in place</em> by complete nodes.
593: */
594: private void processSearchResults(List<MMObjectNode> results) {
595: Map<Integer, Set<MMObjectNode>> convert = new HashMap<Integer, Set<MMObjectNode>>();
596: int convertCount = 0;
597: int convertedCount = 0;
598: int cacheGetCount = 0;
599: int cachePutCount = 0;
600:
601: ListIterator<MMObjectNode> resultsIterator = results
602: .listIterator();
603: while (resultsIterator.hasNext()) {
604: MMObjectNode node = resultsIterator.next();
605: Integer number = node.getNumber();
606: if (number.intValue() < 0) {
607: // never happened to me, and never should!
608: log
609: .error("invalid node found, node number was invalid:"
610: + node.getNumber()
611: + ", storage invalid?");
612: // dont know what to do with this node,...
613: // remove it from the results, continue to the next one!
614: resultsIterator.remove();
615: continue;
616: }
617:
618: boolean fromCache = false;
619: // only active when builder loaded (oType != -1)
620: // maybe we got the wrong node typeback, if so
621: // try to retrieve the correct node from the cache first
622: int oType = builder.getNumber();
623: if (oType != -1 && oType != node.getOType()) {
624: // try to retrieve the correct node from the
625: // nodecache
626: MMObjectNode cachedNode = builder
627: .getNodeFromCache(number);
628: if (cachedNode != null) {
629: node = cachedNode;
630: resultsIterator.set(node);
631: fromCache = true;
632: cacheGetCount++;
633: } else {
634: // add this node to the list of nodes that still need to
635: // be converted..
636: // we dont request the builder here, for this we need the
637: // typedef table, which could generate an additional query..
638: Integer nodeType = node.getOType();
639: Set<MMObjectNode> nodes = convert.get(nodeType);
640: // create an new entry for the type, if not yet there...
641: if (nodes == null) {
642: nodes = new HashSet<MMObjectNode>();
643: convert.put(nodeType, nodes);
644: }
645: nodes.add(node);
646: convertCount++;
647: }
648: /*
649: } else if (oType == node.getOType()) {
650: MMObjectNode oldNode = builder.getNodeFromCache(number);
651: // when we want to use cache also for new found nodes
652: // and cache may not be replaced, use the one from the
653: // cache..
654: if(!REPLACE_CACHE && oldNode != null) {
655: node = oldNode;
656: resultsIterator.set(node);
657: fromCache = true;
658: cacheGetCount++;
659: }
660: } else {
661: // skipping everything, our builder hasnt been started yet...
662: */
663: }
664:
665: // we can add the node to the cache _if_
666: // it was not from cache already, and it
667: // is of the correct type..
668: if (!fromCache && oType == node.getOType()) {
669: // can someone tell me what this has to do?
670: // clear the changed signal
671: node.clearChanged(); // huh?
672: node = builder.safeCache(number, node);
673: cachePutCount++;
674: }
675: }
676:
677: if (/* CORRECT_NODE_TYPES && */convert.size() > 0) {
678: // retieve the nodes from the builders....
679: // and put them into one big hashmap (integer/node)
680: // after that replace all the nodes in result, that
681: // were invalid.
682: Map<Integer, MMObjectNode> convertedNodes = new HashMap<Integer, MMObjectNode>();
683:
684: // process all the different types (builders)
685: for (Map.Entry<Integer, Set<MMObjectNode>> typeEntry : convert
686: .entrySet()) {
687: int nodeType = typeEntry.getKey();
688: Set<MMObjectNode> nodes = typeEntry.getValue();
689: MMObjectNode typedefNode;
690: try {
691: typedefNode = getNode(nodeType, true);
692: } catch (Exception e) {
693: log
694: .error("Exception during conversion of nodelist to right types. Nodes ("
695: + nodes
696: + ") of current type "
697: + nodeType
698: + " will be skipped. Probably the storage is inconsistent. Message: "
699: + e.getMessage());
700:
701: continue;
702: }
703: if (typedefNode == null) {
704: typedefNode = getNode(MMBase.getMMBase()
705: .getBuilder("object").getNumber(), true);
706: log.error("Could not find typedef node #"
707: + nodeType + " taking " + typedefNode
708: + " in stead");
709: }
710: String tableName = typedefNode.getBuilder()
711: .getTableName();
712: if (!tableName.equals("typedef")) {
713: typedefNode = getNode(MMBase.getMMBase()
714: .getBuilder("object").getNumber(), true);
715: log.error("The type of node '" + nodeType
716: + "' is not typedef (but '" + tableName
717: + "'). This is an error. Taking '"
718: + typedefNode + "' in stead.");
719: }
720:
721: MMObjectBuilder conversionBuilder = builder.getMMBase()
722: .getBuilder(typedefNode.getStringValue("name"));
723: if (conversionBuilder == null) {
724: // maybe it is not active?
725: log
726: .error("Could not find builder with name:"
727: + typedefNode
728: .getStringValue("name")
729: + " refered by node #"
730: + typedefNode.getNumber()
731: + ", is it active? Taking object builder in stead.");
732: conversionBuilder = MMBase.getMMBase().getBuilder(
733: "object");
734: }
735: try {
736: for (MMObjectNode current : conversionBuilder
737: .getStorageConnector().getNodes(nodes)) {
738: if (current == null) {
739: log.warn("Found a node which is NULL!");
740: continue;
741: }
742: convertedNodes
743: .put(current.getNumber(), current);
744: }
745: } catch (SearchQueryException sqe) {
746: log.error(sqe.getMessage(), sqe);
747: // no nodes
748: }
749: }
750:
751: // insert all the corrected nodes that were found into the list..
752: for (int i = 0; i < results.size(); i++) {
753: MMObjectNode current = results.get(i);
754: Integer number = current.getNumber();
755: if (convertedNodes.containsKey(number)) {
756: // converting the node...
757: results.set(i, convertedNodes.get(number));
758: convertedCount++;
759: }
760: current = results.get(i);
761: assert current.getNumber() >= 0;
762: }
763: } else if (convert.size() != 0) {
764: log.warn("we still need to convert " + convertCount
765: + " of the " + results.size() + " nodes"
766: + "(number of different types:" + convert.size()
767: + ")");
768: }
769: if (log.isDebugEnabled()) {
770: log.debug("retrieved " + results.size()
771: + " nodes, converted " + convertedCount
772: + " of the " + convertCount + " invalid nodes("
773: + convert.size() + " types, " + cacheGetCount
774: + " from cache, " + cachePutCount + " to cache)");
775: }
776: }
777:
778: /**
779: * Creates search query that retrieves nodes matching a specified
780: * constraint.
781: *
782: * @param where The constraint, can be a SQL where-clause, a MMNODE
783: * expression, an altavista-formatted expression, empty or
784: * <code>null</code>.
785: * @return The query.
786: * @since MMBase-1.7
787: */
788: public NodeSearchQuery getSearchQuery(String where) {
789: NodeSearchQuery query;
790:
791: if (where != null && where.startsWith("MMNODE ")) {
792: // MMNODE expression.
793: query = convertMMNodeSearch2Query(where);
794: } else {
795: query = new NodeSearchQuery(builder);
796: QueryConvertor.setConstraint(query, where);
797: }
798:
799: return query;
800: }
801:
802: /**
803: * Creates query based on an MMNODE expression.
804: *
805: * @deprecated MMNODE expressions are deprecated, scan only?
806: * @param expr The MMNODE expression.
807: * @return The query.
808: * @throws IllegalArgumentException when an invalid argument is supplied.
809: */
810: private NodeSearchQuery convertMMNodeSearch2Query(String expr) {
811: NodeSearchQuery query = new NodeSearchQuery(builder);
812: BasicCompositeConstraint constraints = new BasicCompositeConstraint(
813: CompositeConstraint.LOGICAL_AND);
814: String logicalOperator = null;
815:
816: // Strip leading string "MMNODE " from expression, parse
817: // fieldexpressions and logical operators.
818: // (legacy: eol characters '\n' and '\r' are interpreted as "AND NOT")
819: StringTokenizer tokenizer = new StringTokenizer(expr
820: .substring(7), "+-\n\r", true);
821: while (tokenizer.hasMoreTokens()) {
822: String fieldExpression = tokenizer.nextToken();
823:
824: // Remove prefix if present (example episodes.title==).
825: int pos = fieldExpression.indexOf('.');
826: if (pos != -1) {
827: fieldExpression = fieldExpression.substring(pos + 1);
828: }
829:
830: // Break up field expression in fieldname, comparison operator
831: // and value.
832: pos = fieldExpression.indexOf('=');
833: if (pos != -1 && fieldExpression.length() > pos + 2) {
834: String fieldName = fieldExpression.substring(0, pos);
835: char comparison = fieldExpression.charAt(pos + 1);
836: String value = fieldExpression.substring(pos + 2);
837:
838: // Add corresponding constraint to constraints.
839: CoreField field = builder.getField(fieldName);
840: if (field == null) {
841: throw new IllegalArgumentException(
842: "Invalid MMNODE expression: " + expr);
843: }
844: StepField stepField = query.getField(field);
845: BasicConstraint constraint = parseFieldPart(stepField,
846: comparison, value);
847: constraints.addChild(constraint);
848:
849: // Set to inverse if preceded by a logical operator that is
850: // not equal to "+".
851: if (logicalOperator != null
852: && !logicalOperator.equals("+")) {
853: constraint.setInverse(true);
854: }
855: } else {
856: // Invalid expression.
857: throw new IllegalArgumentException(
858: "Invalid MMNODE expression: " + expr);
859: }
860:
861: // Read next logical operator.
862: if (tokenizer.hasMoreTokens()) {
863: logicalOperator = tokenizer.nextToken();
864: }
865: }
866:
867: List<Constraint> childs = constraints.getChilds();
868: if (childs.size() == 1) {
869: query.setConstraint(childs.get(0));
870: } else if (childs.size() > 1) {
871: query.setConstraint(constraints);
872: }
873: return query;
874: }
875:
876: /**
877: * Creates a {@link org.mmbase.storage.search.FieldCompareConstraint
878: * FieldCompareConstraint}, based on parts of a field expression in a
879: * MMNODE expression.
880: *
881: * @deprecated MMNODE expressions are deprecated
882: * @param field The field
883: * @param comparison The second character of the comparison operator.
884: * @param strValue The value to compare with, represented as
885: * <code>String<code>.
886: * @return The constraint.
887: */
888: private BasicFieldValueConstraint parseFieldPart(StepField field,
889: char comparison, String strValue) {
890:
891: Object value = strValue;
892:
893: // For numberical fields, convert string representation to Double.
894: if (field.getType() != Field.TYPE_STRING
895: && field.getType() != Field.TYPE_XML
896: && field.getType() != Field.TYPE_UNKNOWN) {
897: // backwards comp fix. This is needed for the scan editors.
898: int length = strValue.length();
899: if (strValue.charAt(0) == '*'
900: && strValue.charAt(length - 1) == '*') {
901: strValue = strValue.substring(1, length - 1);
902: }
903: value = Double.valueOf(strValue);
904: }
905:
906: BasicFieldValueConstraint constraint = new BasicFieldValueConstraint(
907: field, value);
908:
909: switch (comparison) {
910: case '=':
911: case 'E':
912: // EQUAL (string field)
913: if (field.getType() == Field.TYPE_STRING
914: || field.getType() == Field.TYPE_XML) {
915: // Strip first and last character of value, when
916: // equal to '*'.
917: String str = (String) value;
918: int length = str.length();
919: if (str.charAt(0) == '*'
920: && str.charAt(length - 1) == '*') {
921: value = str.substring(1, length - 1);
922: }
923:
924: // Convert to LIKE comparison with wildchard characters
925: // before and after (legacy).
926: constraint.setValue('%' + (String) value + '%');
927: constraint.setCaseSensitive(false);
928: constraint.setOperator(FieldCompareConstraint.LIKE);
929:
930: // EQUAL (numerical field)
931: } else {
932: constraint.setOperator(FieldCompareConstraint.EQUAL);
933: }
934: break;
935:
936: case 'N':
937: constraint.setOperator(FieldCompareConstraint.NOT_EQUAL);
938: break;
939:
940: case 'G':
941: constraint.setOperator(FieldCompareConstraint.GREATER);
942: break;
943:
944: case 'g':
945: constraint
946: .setOperator(FieldCompareConstraint.GREATER_EQUAL);
947: break;
948:
949: case 'S':
950: constraint.setOperator(FieldCompareConstraint.LESS);
951: break;
952:
953: case 's':
954: constraint.setOperator(FieldCompareConstraint.LESS_EQUAL);
955: break;
956:
957: default:
958: throw new IllegalArgumentException(
959: "Invalid comparison character: '" + comparison
960: + "'");
961: }
962: return constraint;
963: }
964:
965: }
|