0001: /*
0002:
0003: This software is OSI Certified Open Source Software.
0004: OSI Certified is a certification mark of the Open Source Initiative.
0005:
0006: The license (Mozilla version 1.0) can be read at the MMBase site.
0007: See http://www.MMBase.org/license
0008:
0009: */
0010: package org.mmbase.module.core;
0011:
0012: import java.util.*;
0013: import java.util.Map.Entry;
0014:
0015: import org.mmbase.module.corebuilders.*;
0016: import org.mmbase.core.CoreField;
0017: import org.mmbase.bridge.Field;
0018: import org.mmbase.core.util.Fields;
0019: import org.mmbase.util.functions.*;
0020: import org.mmbase.datatypes.*;
0021: import org.mmbase.storage.search.*;
0022: import org.mmbase.storage.search.implementation.*;
0023: import org.mmbase.storage.search.legacy.ConstraintParser;
0024: import org.mmbase.util.QueryConvertor;
0025: import org.mmbase.util.logging.*;
0026:
0027: /**
0028: * The builder for {@link ClusterNode clusternodes}.
0029: * <p>
0030: * Provides these methods to retrieve clusternodes:
0031: * <ul>
0032: * <li>{@link #getClusterNodes(SearchQuery)}
0033: * to retrieve clusternodes using a <code>SearchQuery</code> (recommended).
0034: * <li>{@link #getMultiLevelSearchQuery(List,List,String,List,String,List,List,int)}
0035: * as a convenience method to create a <code>SearchQuery</code>
0036: * <li>{@link #searchMultiLevelVector(List,List,String,List,String,List,List,int)}
0037: * to retrieve clusternodes using a constraint string.
0038: * </ul>
0039: * <p>
0040: * Individual nodes in a 'cluster' node can be retrieved by calling the node's
0041: * {@link MMObjectNode#getNodeValue(String) getNodeValue()} method, using
0042: * the builder name (or step alias) as argument.
0043: *
0044: * @todo XXXX. This 'builder' is actually singleton (only one instance is created). It does
0045: * therefore not support getFields, so this is more or less hacked in bridge. Perhaps in 'core' a
0046: * similar approach as now in birdge must be taken, so no ClusterBuilder, but only Virtual builders,
0047: * one for every query result.
0048: *
0049: *
0050: * @author Rico Jansen
0051: * @author Pierre van Rooden
0052: * @author Rob van Maris
0053: * @version $Id: ClusterBuilder.java,v 1.93 2008/02/03 17:33:57 nklasens Exp $
0054: * @see ClusterNode
0055: */
0056: public class ClusterBuilder extends VirtualBuilder {
0057:
0058: /**
0059: * Search for all valid relations.
0060: * When searching relations, return both relations from source to deastination and from destination to source,
0061: * provided there is an allowed relation in that directon.
0062: * @deprecated use {@link RelationStep#DIRECTIONS_BOTH}
0063: * In future versions of MMBase (1.8 and up) this will be the default value
0064: */
0065: public static final int SEARCH_BOTH = RelationStep.DIRECTIONS_BOTH;
0066:
0067: /**
0068: * Search for destinations,
0069: * When searching relations, return only relations from source to deastination.
0070: * @deprecated use {@link RelationStep#DIRECTIONS_DESTINATION}
0071: */
0072: public static final int SEARCH_DESTINATION = RelationStep.DIRECTIONS_DESTINATION;
0073:
0074: /**
0075: * Seach for sources.
0076: * When searching a multilevel, return only relations from destination to source, provided directionality allows
0077: * @deprecated use {@link RelationStep#DIRECTIONS_SOURCE}
0078: */
0079: public static final int SEARCH_SOURCE = RelationStep.DIRECTIONS_SOURCE;
0080:
0081: /**
0082: * Search for all relations. When searching a multilevel, return both relations from source to
0083: * deastination and from destination to source. Allowed relations are not checked - ALL
0084: * relations are used. This makes more inefficient queries, but it is not really wrong.
0085: * @deprecated use {@link RelationStep#DIRECTIONS_ALL}
0086: */
0087: public static final int SEARCH_ALL = RelationStep.DIRECTIONS_ALL;
0088:
0089: /**
0090: * Search for either destination or source.
0091: * When searching a multilevel, return either relations from source to destination OR from destination to source.
0092: * The returned set is decided through the typerel tabel. However, if both directions ARE somehow supported, the
0093: * system only returns source to destination relations.
0094: * This is the default value (for compatibility purposes).
0095: * @deprecated use {@link RelationStep#DIRECTIONS_EITHER}.
0096: * In future versions of MMBase (1.8 and up) the default value will be
0097: * {@link RelationStep#DIRECTIONS_BOTH}
0098: */
0099: public static final int SEARCH_EITHER = RelationStep.DIRECTIONS_EITHER;
0100:
0101: // logging variable
0102: private static final Logger log = Logging
0103: .getLoggerInstance(ClusterBuilder.class);
0104:
0105: /**
0106: * Creates <code>ClusterBuilder</code> instance.
0107: * Must be called from the MMBase class.
0108: * @param m the MMbase cloud creating the node
0109: * @scope package
0110: */
0111: public ClusterBuilder(MMBase m) {
0112: super (m, "clusternodes");
0113: }
0114:
0115: /**
0116: * Translates a string to a search direction constant.
0117: *
0118: * @since MMBase-1.6
0119: */
0120: public static int getSearchDir(String search) {
0121: if (search == null) {
0122: return RelationStep.DIRECTIONS_EITHER;
0123: }
0124: return org.mmbase.bridge.util.Queries
0125: .getRelationStepDirection(search);
0126: }
0127:
0128: /**
0129: * Translates a search direction constant to a string.
0130: *
0131: * @since MMBase-1.6
0132: */
0133: public static String getSearchDirString(int search) {
0134: if (search == RelationStep.DIRECTIONS_DESTINATION) {
0135: return "DESTINATION";
0136: } else if (search == RelationStep.DIRECTIONS_SOURCE) {
0137: return "SOURCE";
0138: } else if (search == RelationStep.DIRECTIONS_BOTH) {
0139: return "BOTH";
0140: } else if (search == RelationStep.DIRECTIONS_ALL) {
0141: return "ALL";
0142: } else {
0143: return "EITHER";
0144: }
0145: }
0146:
0147: /**
0148: * Get a new node, using this builder as its parent.
0149: * The new node is a cluster node.
0150: * Unlike most other nodes, a cluster node does not have a number,
0151: * owner, or otype fields.
0152: * @param owner The administrator creating the new node (ignored).
0153: * @return A newly initialized <code>VirtualNode</code>.
0154: */
0155: public MMObjectNode getNewNode(String owner) {
0156: throw new UnsupportedOperationException(
0157: "One cannot create new ClusterNodes");
0158: }
0159:
0160: /**
0161: * What should a GUI display for this node.
0162: * This version displays the contents of the 'name' field(s) that were retrieved.
0163: * XXX: should be changed to something better
0164: * @param node The node to display
0165: * @return the display of the node as a <code>String</code>
0166: */
0167: public String getGUIIndicator(MMObjectNode node) {
0168: // Return "name"-field when available.
0169: String s = node.getStringValue("name");
0170: if (s != null) {
0171: return s;
0172: }
0173:
0174: // Else "name"-fields of contained nodes.
0175: StringBuilder sb = new StringBuilder();
0176: for (Entry<String, Object> entry : node.getValues().entrySet()) {
0177: String key = entry.getKey();
0178: if (key.endsWith(".name")) {
0179: if (s.length() != 0) {
0180: sb.append(", ");
0181: }
0182: sb.append(entry.getValue());
0183: }
0184: }
0185: if (sb.length() > 15) {
0186: return sb.substring(0, 12) + "...";
0187: } else {
0188: return sb.toString();
0189: }
0190: }
0191:
0192: /**
0193: * What should a GUI display for this node/field combo.
0194: * For a multilevel node, the builder tries to determine
0195: * the original builder of a field, and invoke the method using
0196: * that builder.
0197: *
0198: * @param node The node to display
0199: * @param pars Parameters, see {@link MMObjectBuilder#GUI_PARAMETERS}
0200: * @return the display of the node's field as a <code>String</code>, null if not specified
0201: */
0202: public String getGUIIndicator(MMObjectNode node, Parameters pars) {
0203:
0204: if (node == null)
0205: throw new RuntimeException(
0206: "Tried to get GUIIndicator for " + pars
0207: + " with NULL node");
0208:
0209: ClusterNode clusterNode = (ClusterNode) node;
0210:
0211: String field = pars.getString(Parameter.FIELD);
0212: if (field == null) {
0213: return super .getGUIIndicator(node, pars);
0214: } else {
0215: int pos = field.indexOf('.');
0216: if (pos != -1) {
0217: String bulName = getTrueTableName(field.substring(0,
0218: pos));
0219: MMObjectNode n = clusterNode.getRealNode(bulName);
0220: if (n != null) {
0221: MMObjectBuilder bul = n.getBuilder();
0222: if (bul != null) {
0223: // what are we trying here?
0224: String fieldName = field.substring(pos + 1);
0225: Parameters newPars = new Parameters(pars
0226: .getDefinition(), pars);
0227: newPars.set(Parameter.FIELD, fieldName);
0228: newPars.set("stringvalue", null);
0229: org.mmbase.bridge.Node bnode = pars
0230: .get(Parameter.NODE);
0231: if (bnode != null) {
0232: newPars.set(Parameter.NODE, bnode
0233: .getNodeValue(bulName));
0234: }
0235: newPars.set(Parameter.CORENODE, n);
0236: return bul.guiFunction
0237: .getFunctionValue(newPars);
0238: }
0239: }
0240: }
0241: return super .getGUIIndicator(node, pars);
0242: }
0243: }
0244:
0245: /**
0246: * Determines the builder part of a specified field.
0247: * @param fieldName the name of the field
0248: * @return the name of the field's builder
0249: */
0250: public String getBuilderNameFromField(String fieldName) {
0251: int pos = fieldName.indexOf(".");
0252: if (pos != -1) {
0253: String bulName = fieldName.substring(0, pos);
0254: return getTrueTableName(bulName);
0255: }
0256: return "";
0257: }
0258:
0259: /**
0260: * Determines the fieldname part of a specified field (without the builder name).
0261: * @param fieldname the name of the field
0262: * @return the name of the field without its builder
0263: */
0264: public static String getFieldNameFromField(String fieldname) {
0265: int pos = fieldname.indexOf(".");
0266: if (pos != -1) {
0267: fieldname = fieldname.substring(pos + 1);
0268: }
0269: return fieldname;
0270: }
0271:
0272: /**
0273: * Return a field.
0274: * @param fieldName the requested field's name
0275: * @return the field
0276: */
0277: public FieldDefs getField(String fieldName) {
0278: String builderName = getBuilderNameFromField(fieldName);
0279: if (builderName.length() > 0) {
0280: MMObjectBuilder bul = mmb.getBuilder(builderName);
0281: if (bul == null) {
0282: throw new RuntimeException("No builder with name '"
0283: + builderName + "' found");
0284: }
0285: return bul.getField(getFieldNameFromField(fieldName));
0286: } else {
0287: //
0288: MMObjectBuilder bul = mmb
0289: .getBuilder(getTrueTableName(fieldName));
0290: if (bul != null) {
0291: return new FieldDefs(fieldName, Field.TYPE_NODE, -1,
0292: Field.STATE_VIRTUAL,
0293: org.mmbase.datatypes.DataTypes
0294: .getDataType("node"));
0295: }
0296: }
0297: return null;
0298: }
0299:
0300: public List<CoreField> getFields(int order) {
0301: throw new UnsupportedOperationException(
0302: "Cluster-nodes can have any field.");
0303: }
0304:
0305: public Collection<CoreField> getFields() {
0306: throw new UnsupportedOperationException(
0307: "Cluster-nodes can have any field.");
0308: }
0309:
0310: /**
0311: * @since MMBase-1.8
0312: */
0313: public Map<String, CoreField> getFields(MMObjectNode node) {
0314: Map<String, CoreField> ret = new HashMap<String, CoreField>();
0315: Iterator<String> i = node.getValues().keySet().iterator();
0316: DataType<? extends Object> nodeType = DataTypes
0317: .getDataType("node");
0318: while (i.hasNext()) {
0319: String name = i.next();
0320: int pos = name.indexOf(".");
0321: if (pos != -1) {
0322: String builderName = name.substring(0, pos);
0323: if (!ret.containsKey(builderName)) {
0324: CoreField fd = Fields.createField(builderName,
0325: Field.TYPE_NODE, Field.TYPE_UNKNOWN,
0326: Field.STATE_VIRTUAL, nodeType);
0327: ret.put(builderName, fd);
0328: }
0329: }
0330: ret.put(name, getField(name));
0331: }
0332: return ret;
0333: }
0334:
0335: /**
0336: * Same as {@link #searchMultiLevelVector(List,List,String,List,String,List,List,int)
0337: * searchMultiLevelVector(snodes, fields, pdistinct, tables, where, orderVec, direction, RelationStep.DIRECTIONS_EITHER)},
0338: * where <code>snodes</code> contains just the number specified by <code>snode</code>.
0339: *
0340: * @see #searchMultiLevelVector(List, List, String, List, String, List, List, List)
0341: */
0342: public Vector<MMObjectNode> searchMultiLevelVector(int snode,
0343: Vector<String> fields, String pdistinct,
0344: Vector<String> tables, String where,
0345: Vector<String> orderVec, Vector<String> direction) {
0346:
0347: List<String> v = new ArrayList<String>();
0348: v.add("" + snode);
0349: return searchMultiLevelVector(v, fields, pdistinct, tables,
0350: where, orderVec, direction,
0351: RelationStep.DIRECTIONS_EITHER);
0352: }
0353:
0354: /**
0355: * Same as {@link #searchMultiLevelVector(List,List,String,List,String,List,List,int)
0356: * searchMultiLevelVector(snodes, fields, pdistinct, tables, where, orderVec, direction, RelationStep.DIRECTIONS_EITHER)}.
0357: *
0358: * @see #searchMultiLevelVector(List,List,String,List,String,List,List,int)
0359: */
0360: public Vector<MMObjectNode> searchMultiLevelVector(
0361: Vector<String> snodes, Vector<String> fields,
0362: String pdistinct, Vector<String> tables, String where,
0363: Vector<String> orderVec, Vector<String> direction) {
0364: return searchMultiLevelVector(snodes, fields, pdistinct,
0365: tables, where, orderVec, direction,
0366: RelationStep.DIRECTIONS_EITHER);
0367: }
0368:
0369: /**
0370: * Return all the objects that match the searchkeys.
0371: * The constraint must be in one of the formats specified by {@link
0372: * org.mmbase.util.QueryConvertor#setConstraint(BasicSearchQuery,String)
0373: * QueryConvertor#setConstraint()}.
0374: *
0375: * @param snodes The numbers of the nodes to start the search with. These have to be present in the first table
0376: * listed in the tables parameter.
0377: * @param fields The fieldnames to return. This should include the name of the builder. Fieldnames without a builder prefix are ignored.
0378: * Fieldnames are accessible in the nodes returned in the same format (i.e. with manager indication) as they are specified in this parameter.
0379: * Examples: 'people.lastname'
0380: * @param pdistinct 'YES' indicates the records returned need to be distinct. Any other value indicates double values can be returned.
0381: * @param tables The builder chain. A list containing builder names.
0382: * The search is formed by following the relations between successive builders in the list. It is possible to explicitly supply
0383: * a relation builder by placing the name of the builder between two builders to search.
0384: * Example: company,people or typedef,authrel,people.
0385: * @param where The constraint, must be in one of the formats specified by {@link
0386: * org.mmbase.util.QueryConvertor#setConstraint(BasicSearchQuery,String)
0387: * QueryConvertor#setConstraint()}.
0388: * E.g. "WHERE news.title LIKE '%MMBase%' AND news.title > 100"
0389: * @param sortFields the fieldnames on which you want to sort.
0390: * @param directions A list of values containing, for each field in the order parameter, a value indicating whether the sort is
0391: * ascending (<code>UP</code>) or descending (<code>DOWN</code>). If less values are syupplied then there are fields in order,
0392: * the first value in the list is used for the remaining fields. Default value is <code>'UP'</code>.
0393: * @param searchDir Specifies in which direction relations are to be
0394: * followed, this must be one of the values defined by this class.
0395: * @return a <code>Vector</code> containing all matching nodes
0396: * @deprecated use {@link #searchMultiLevelVector(List snodes, List fields, String pdistinct, List tables, String where,
0397: * List orderVec, List directions, List searchDirs)}
0398: */
0399: public Vector<MMObjectNode> searchMultiLevelVector(
0400: List<String> snodes, List<String> fields, String pdistinct,
0401: List<String> tables, String where, List<String> sortFields,
0402: List<String> directions, int searchDir) {
0403: List<Integer> searchDirs = new ArrayList<Integer>();
0404: searchDirs.add(searchDir);
0405: return searchMultiLevelVector(snodes, fields, pdistinct,
0406: tables, where, sortFields, directions, searchDirs);
0407: }
0408:
0409: /**
0410: * Return all the objects that match the searchkeys.
0411: * The constraint must be in one of the formats specified by {@link
0412: * org.mmbase.util.QueryConvertor#setConstraint(BasicSearchQuery,String)
0413: * QueryConvertor#setConstraint()}.
0414: *
0415: * @param snodes The numbers of the nodes to start the search with. These have to be present in the first table
0416: * listed in the tables parameter.
0417: * @param fields The fieldnames to return. This should include the name of the builder. Fieldnames without a builder prefix are ignored.
0418: * Fieldnames are accessible in the nodes returned in the same format (i.e. with manager indication) as they are specified in this parameter.
0419: * Examples: 'people.lastname'
0420: * @param pdistinct 'YES' indicates the records returned need to be distinct. Any other value indicates double values can be returned.
0421: * @param tables The builder chain. A list containing builder names.
0422: * The search is formed by following the relations between successive builders in the list. It is possible to explicitly supply
0423: * a relation builder by placing the name of the builder between two builders to search.
0424: * Example: company,people or typedef,authrel,people.
0425: * @param where The constraint, must be in one of the formats specified by {@link
0426: * org.mmbase.util.QueryConvertor#setConstraint(BasicSearchQuery,String)
0427: * QueryConvertor#setConstraint()}.
0428: * E.g. "WHERE news.title LIKE '%MMBase%' AND news.title > 100"
0429: * @param sortFields the fieldnames on which you want to sort.
0430: * @param directions A list of values containing, for each field in the order parameter, a value indicating whether the sort is
0431: * ascending (<code>UP</code>) or descending (<code>DOWN</code>). If less values are syupplied then there are fields in order,
0432: * the first value in the list is used for the remaining fields. Default value is <code>'UP'</code>.
0433: * @param searchDirs Specifies in which direction relations are to be followed. You can specify a direction for each
0434: * relation in the path. If you specify less directions than there are relations, the last specified direction is used
0435: * for the remaining relations. If you specify an empty list the default direction is BOTH.
0436: * @return a <code>Vector</code> containing all matching nodes
0437: */
0438: public Vector<MMObjectNode> searchMultiLevelVector(
0439: List<String> snodes, List<String> fields, String pdistinct,
0440: List<String> tables, String where, List<String> sortFields,
0441: List<String> directions, List<Integer> searchDirs) {
0442: // Try to handle using the SearchQuery framework.
0443: try {
0444: SearchQuery query = getMultiLevelSearchQuery(snodes,
0445: fields, pdistinct, tables, where, sortFields,
0446: directions, searchDirs);
0447: List<MMObjectNode> clusterNodes = getClusterNodes(query);
0448: return new Vector<MMObjectNode>(clusterNodes);
0449: } catch (Exception e) {
0450: log.error(e.getMessage(), e);
0451: return null;
0452: }
0453: }
0454:
0455: /**
0456: * Executes query, returns results as {@link ClusterNode clusternodes} or MMObjectNodes if the
0457: * query is a Node-query.
0458: *
0459: * @param query The query.
0460: * @return The clusternodes.
0461: * @throws org.mmbase.storage.search.SearchQueryException
0462: * When an exception occurred while retrieving the results.
0463: * @since MMBase-1.7
0464: * @see org.mmbase.storage.search.SearchQueryHandler#getNodes
0465: */
0466: public List<MMObjectNode> getClusterNodes(SearchQuery query)
0467: throws SearchQueryException {
0468:
0469: // TODO (later): implement maximum set by maxNodesFromQuery?
0470: // Execute query, return results.
0471:
0472: return mmb.getSearchQueryHandler().getNodes(query, this );
0473:
0474: }
0475:
0476: /**
0477: * Returns the name part of a tablename.
0478: * The name part is the table name minus the numeric digit appended
0479: * to a name (if appliable).
0480: * @param table name of the original table
0481: * @return A <code>String</code> containing the table name
0482: */
0483: private String getTableName(String table) {
0484: int end = table.length();
0485: if (end == 0)
0486: throw new IllegalArgumentException("Table name too short '"
0487: + table + "'");
0488: while (Character.isDigit(table.charAt(end - 1)))
0489: --end;
0490: return table.substring(0, end);
0491: }
0492:
0493: /**
0494: * Returns the name part of a tablename, and convert it to a buidler name.
0495: * This will catch specifying a rolename in stead of a builder name when using relations.
0496: * @param table name of the original table
0497: * @return A <code>String</code> containing the table name
0498: */
0499: private String getTrueTableName(String table) {
0500: String tab = getTableName(table);
0501: int rnumber = mmb.getRelDef().getNumberByName(tab);
0502: if (rnumber != -1) {
0503: return mmb.getRelDef().getBuilderName(rnumber);
0504: } else {
0505: return tab;
0506: }
0507: }
0508:
0509: /**
0510: * Get text from a blob field.
0511: * The text is cut if it is to long.
0512: * @param fieldname name of the field
0513: * @param number number of the object in the table
0514: * @return a <code>String</code> containing the contents of a field as text
0515: */
0516: public String getShortedText(String fieldname, int number) {
0517: String buildername = getBuilderNameFromField(fieldname);
0518: if (buildername.length() > 0) {
0519: MMObjectBuilder bul = mmb.getMMObject(buildername);
0520: return bul.getShortedText(getFieldNameFromField(fieldname),
0521: bul.getNode(number));
0522: }
0523: return null;
0524: }
0525:
0526: /**
0527: * Get binary data of a database blob field.
0528: * The data is cut if it is to long.
0529: * @param fieldname name of the field
0530: * @param number number of the object in the table
0531: * @return an array of <code>byte</code> containing the contents of a field as text
0532: */
0533: public byte[] getShortedByte(String fieldname, int number) {
0534: String buildername = getBuilderNameFromField(fieldname);
0535: if (buildername.length() > 0) {
0536: MMObjectBuilder bul = mmb.getMMObject(buildername);
0537: return bul.getShortedByte(getFieldNameFromField(fieldname),
0538: bul.getNode(number));
0539: }
0540: return null;
0541: }
0542:
0543: /**
0544: * Creates search query that selects all the objects that match the
0545: * searchkeys.
0546: * The constraint must be in one of the formats specified by {@link
0547: * org.mmbase.util.QueryConvertor#setConstraint(BasicSearchQuery,String)
0548: * QueryConvertor#setConstraint()}.
0549: *
0550: * @param snodes <code>null</code> or a list of numbers
0551: * of nodes to start the search with.
0552: * These have to be present in the first table listed in the
0553: * tables parameter.
0554: * @param fields List of fieldnames to return.
0555: * These should be formatted as <em>stepalias.field</em>,
0556: * e.g. 'people.lastname'
0557: * @param pdistinct 'YES' if the records returned need to be
0558: * distinct (ignoring case).
0559: * Any other value indicates double values can be returned.
0560: * @param tables The builder chain, a list containing builder names.
0561: * The search is formed by following the relations between
0562: * successive builders in the list.
0563: * It is possible to explicitly supply a relation builder by
0564: * placing the name of the builder between two builders to search.
0565: * Example: company,people or typedef,authrel,people.
0566: * @param where The constraint, must be in one of the formats specified by {@link
0567: * org.mmbase.util.QueryConvertor#setConstraint(BasicSearchQuery,String)
0568: * QueryConvertor#setConstraint()}.
0569: * E.g. "WHERE news.title LIKE '%MMBase%' AND news.title > 100"
0570: * @param sortFields <code>null</code> or a list of fieldnames on which you want to sort.
0571: * @param directions <code>null</code> or a list of values containing, for each field in the
0572: * <code>sortFields</code> parameter, a value indicating whether the sort is
0573: * ascending (<code>UP</code>) or descending (<code>DOWN</code>).
0574: * If less values are supplied then there are fields in order,
0575: * the first value in the list is used for the remaining fields.
0576: * Default value is <code>'UP'</code>.
0577: * @param searchDir Specifies in which direction relations are to be
0578: * followed, this must be one of the values defined by this class.
0579: * @deprecated use {@link #getMultiLevelSearchQuery(List snodes, List fields, String pdistinct, List tables, String where,
0580: * List orderVec, List directions, int searchDir)}
0581: * @return the resulting search query.
0582: * @since MMBase-1.7
0583: */
0584: public BasicSearchQuery getMultiLevelSearchQuery(
0585: List<String> snodes, List<String> fields, String pdistinct,
0586: List<String> tables, String where, List<String> sortFields,
0587: List<String> directions, int searchDir) {
0588: List<Integer> searchDirs = new ArrayList<Integer>();
0589: searchDirs.add(searchDir);
0590: return getMultiLevelSearchQuery(snodes, fields, pdistinct,
0591: tables, where, sortFields, directions, searchDirs);
0592: }
0593:
0594: /**
0595: * Creates search query that selects all the objects that match the
0596: * searchkeys.
0597: * The constraint must be in one of the formats specified by {@link
0598: * org.mmbase.util.QueryConvertor#setConstraint(BasicSearchQuery,String)
0599: * QueryConvertor#setConstraint()}.
0600: *
0601: * @param snodes <code>null</code> or a list of numbers
0602: * of nodes to start the search with.
0603: * These have to be present in the first table listed in the
0604: * tables parameter.
0605: * @param fields List of fieldnames to return.
0606: * These should be formatted as <em>stepalias.field</em>,
0607: * e.g. 'people.lastname'
0608: * @param pdistinct 'YES' if the records returned need to be
0609: * distinct (ignoring case).
0610: * Any other value indicates double values can be returned.
0611: * @param tables The builder chain, a list containing builder names.
0612: * The search is formed by following the relations between
0613: * successive builders in the list.
0614: * It is possible to explicitly supply a relation builder by
0615: * placing the name of the builder between two builders to search.
0616: * Example: company,people or typedef,authrel,people.
0617: * @param where The constraint, must be in one of the formats specified by {@link
0618: * org.mmbase.util.QueryConvertor#setConstraint(BasicSearchQuery,String)
0619: * QueryConvertor#setConstraint()}.
0620: * E.g. "WHERE news.title LIKE '%MMBase%' AND news.title > 100"
0621: * @param sortFields <code>null</code> or a list of fieldnames on which you want to sort.
0622: * @param directions <code>null</code> or a list of values containing, for each field in the
0623: * <code>sortFields</code> parameter, a value indicating whether the sort is
0624: * ascending (<code>UP</code>) or descending (<code>DOWN</code>).
0625: * If less values are supplied then there are fields in order,
0626: * the first value in the list is used for the remaining fields.
0627: * Default value is <code>'UP'</code>.
0628: * @param searchDirs Specifies in which direction relations are to be
0629: * followed, this must be one of the values defined by this class.
0630: * @return the resulting search query.
0631: * @since MMBase-1.7
0632: */
0633: public BasicSearchQuery getMultiLevelSearchQuery(
0634: List<String> snodes, List<String> fields, String pdistinct,
0635: List<String> tables, String where, List<String> sortFields,
0636: List<String> directions, List<Integer> searchDirs) {
0637:
0638: // Create the query.
0639: BasicSearchQuery query = new BasicSearchQuery();
0640:
0641: // Set the distinct property.
0642: boolean distinct = pdistinct != null
0643: && pdistinct.equalsIgnoreCase("YES");
0644: query.setDistinct(distinct);
0645:
0646: // Get ALL tables (including missing reltables)
0647: Map<String, Integer> roles = new HashMap<String, Integer>();
0648: Map<String, BasicStepField> fieldsByAlias = new HashMap<String, BasicStepField>();
0649: Map<String, BasicStep> stepsByAlias = addSteps(query, tables,
0650: roles, !distinct, fieldsByAlias);
0651:
0652: // Add fields.
0653: Iterator<String> iFields = fields.iterator();
0654: while (iFields.hasNext()) {
0655: String field = iFields.next();
0656: addFields(query, field, stepsByAlias, fieldsByAlias);
0657: }
0658:
0659: // Add sortorders.
0660: addSortOrders(query, sortFields, directions, fieldsByAlias);
0661:
0662: // Supporting more then 1 source node or no source node at all
0663: // Note that node number -1 is seen as no source node
0664: if (snodes != null && snodes.size() > 0) {
0665: Integer nodeNumber = -1;
0666:
0667: // Copy list, so the original list is not affected.
0668: List<Integer> snodeNumbers = new ArrayList<Integer>();
0669:
0670: // Go trough the whole list of strings (each representing
0671: // either a nodenumber or an alias), convert all to Integer objects.
0672: // from last to first,,... since we want snode to be the one that
0673: // contains the first..
0674: for (int i = snodes.size() - 1; i >= 0; i--) {
0675: String str = snodes.get(i);
0676: try {
0677: nodeNumber = Integer.valueOf(str);
0678: } catch (NumberFormatException e) {
0679: // maybe it was not an integer, hmm lets look in OAlias
0680: // table then
0681: nodeNumber = mmb.getOAlias().getNumber(str);
0682: if (nodeNumber.intValue() < 0) {
0683: nodeNumber = 0;
0684: }
0685: }
0686: snodeNumbers.add(nodeNumber);
0687: }
0688:
0689: Step nodesStep = getNodesStep(query.getSteps(), nodeNumber
0690: .intValue());
0691:
0692: if (nodesStep == null) {
0693: // specified a node which is not of the type of one of the steps.
0694: // take as default the 'first' step (which will make the result empty, compatible with 1.6, bug #6440).
0695: nodesStep = query.getSteps().get(0);
0696: }
0697:
0698: Iterator<Integer> iNodeNumbers = snodeNumbers.iterator();
0699: while (iNodeNumbers.hasNext()) {
0700: Integer number = iNodeNumbers.next();
0701: nodesStep.addNode(number.intValue());
0702: }
0703: }
0704:
0705: addRelationDirections(query, searchDirs, roles);
0706:
0707: // Add constraints.
0708: // QueryConverter supports the old formats for backward compatibility.
0709: QueryConvertor.setConstraint(query, where);
0710:
0711: return query;
0712: }
0713:
0714: /**
0715: * Creates a full chain of steps, adds these to the specified query.
0716: * This includes adding necessary relation tables when not explicitly
0717: * specified, and generating unique table aliases where necessary.
0718: * Optionally adds "number"-fields for all tables in the original chain.
0719: *
0720: * @param query The searchquery.
0721: * @param tables The original chain of tables.
0722: * @param roles Map of tablenames mapped to <code>Integer</code> values,
0723: * representing the nodenumber of a corresponing RelDef node.
0724: * This method adds entries for table aliases that specify a role,
0725: * e.g. "related" or "related2".
0726: * @param includeAllReference Indicates if the "number"-fields must
0727: * included in the query for all tables in the original chain.
0728: * @param fieldsByAlias Map, mapping aliases (fieldname prefixed by table
0729: * alias) to the stepfields in the query. An entry is added for
0730: * each stepfield added to the query.
0731: * @return Map, maps original table names to steps.
0732: * @since MMBase-1.7
0733: */
0734: // package access!
0735: Map<String, BasicStep> addSteps(BasicSearchQuery query,
0736: List<String> tables, Map<String, Integer> roles,
0737: boolean includeAllReference,
0738: Map<String, BasicStepField> fieldsByAlias) {
0739:
0740: Map<String, BasicStep> stepsByAlias = new HashMap<String, BasicStep>(); // Maps original table names to steps.
0741: Set<String> tableAliases = new HashSet<String>(); // All table aliases that are in use.
0742:
0743: Iterator<String> iTables = tables.iterator();
0744: if (iTables.hasNext()) {
0745: // First table.
0746: String tableName = iTables.next();
0747: MMObjectBuilder bul = getBuilder(tableName, roles);
0748: String tableAlias = getUniqueTableAlias(tableName,
0749: tableAliases, tables);
0750: BasicStep step = query.addStep(bul);
0751: step.setAlias(tableAlias);
0752: stepsByAlias.put(tableName, step);
0753: if (includeAllReference) {
0754: // Add number field.
0755: addField(query, step, "number", fieldsByAlias);
0756: }
0757: }
0758: while (iTables.hasNext()) {
0759: String tableName2 = iTables.next();
0760: MMObjectBuilder bul2 = getBuilder(tableName2, roles);
0761: BasicRelationStep relation;
0762: BasicStep step2;
0763: String tableName;
0764: if (bul2 instanceof InsRel) {
0765: // Explicit relation step.
0766: tableName = tableName2;
0767: InsRel bul = (InsRel) bul2;
0768: tableName2 = iTables.next();
0769: bul2 = getBuilder(tableName2, roles);
0770: relation = query.addRelationStep(bul, bul2);
0771: step2 = (BasicStep) relation.getNext();
0772:
0773: // MM: setting aliases used to be _inside_ the includeAllReference-if.
0774: // but I don't see how that would make sense. Trying a while like this.
0775: relation.setAlias(tableName);
0776: step2.setAlias(tableName2);
0777: if (includeAllReference) {
0778: // Add number fields.
0779: addField(query, relation, "number", fieldsByAlias);
0780: addField(query, step2, "number", fieldsByAlias);
0781: }
0782: if (log.isDebugEnabled()) {
0783: log.debug("Created a relation step " + relation
0784: + " (explicit)" + roles);
0785: }
0786: } else {
0787: // Not a relation, relation step is implicit.
0788: tableName = "insrel";
0789: InsRel bul = mmb.getInsRel();
0790: relation = query.addRelationStep(bul, bul2);
0791: step2 = (BasicStep) relation.getNext();
0792: step2.setAlias(tableName2); //see above
0793: if (includeAllReference) {
0794: // Add number field.
0795: addField(query, step2, "number", fieldsByAlias);
0796: }
0797: if (log.isDebugEnabled()) {
0798: log.debug("Created a relation step " + relation
0799: + " (implicit)");
0800: }
0801: }
0802: String tableAlias = getUniqueTableAlias(tableName,
0803: tableAliases, tables);
0804: String tableAlias2 = getUniqueTableAlias(tableName2,
0805: tableAliases, tables);
0806: if (!tableName.equals(tableAlias)) {
0807: roles.put(tableAlias, roles.get(tableName));
0808: }
0809: relation.setAlias(tableAlias);
0810: step2.setAlias(tableAlias2);
0811: stepsByAlias.put(tableAlias, relation);
0812: stepsByAlias.put(tableAlias2, step2);
0813: }
0814: return stepsByAlias;
0815: }
0816:
0817: /**
0818: * Gets builder corresponding to the specified table alias.
0819: * This amounts to removing the optionally appended digit from the table
0820: * alias, and interpreting the result as either a tablename or a relation
0821: * role.
0822: *
0823: * @param tableAlias The table alias.
0824: * Must be tablename or relation role, optionally appended
0825: * with a digit, e.g. images, images3, related and related4.
0826: * @param roles Map of tablenames mapped to <code>Integer</code> values,
0827: * representing the nodenumber of a corresponing RelDef node.
0828: * This method adds entries for table aliases that specify a role,
0829: * e.g. "related" or "related2".
0830: * @return The builder.
0831: * @since MMBase-1.7
0832: */
0833: // package access!
0834: MMObjectBuilder getBuilder(String tableAlias,
0835: Map<String, Integer> roles) {
0836: String tableName = getTableName(tableAlias);
0837: // check builder - should throw exception if builder doesn't exist ?
0838: MMObjectBuilder bul = null;
0839: try {
0840: bul = mmb.getBuilder(tableName);
0841: } catch (BuilderConfigurationException e) {
0842: }
0843:
0844: if (bul == null) {
0845: // check if it is a role name. if so, use the builder of the
0846: // rolename and store a filter on rnumber.
0847: int rnumber = mmb.getRelDef().getNumberByName(tableName);
0848: if (rnumber == -1) {
0849: String msg = "Specified builder " + tableName
0850: + " does not exist.";
0851: log.error(msg);
0852: throw new IllegalArgumentException(msg);
0853: } else {
0854: bul = mmb.getRelDef().getBuilder(rnumber); // relation builder
0855: roles.put(tableAlias, rnumber);
0856: }
0857: } else if (bul instanceof InsRel) {
0858: int rnumber = mmb.getRelDef().getNumberByName(tableName);
0859: if (rnumber != -1) {
0860: roles.put(tableAlias, rnumber);
0861: }
0862: }
0863: if (log.isDebugEnabled()) {
0864: log.debug("Resolved table alias \"" + tableAlias
0865: + "\" to builder \"" + bul.getTableName() + "\"");
0866: }
0867: return bul;
0868: }
0869:
0870: /**
0871: * Returns unique table alias, must be tablename/rolename, optionally
0872: * appended with a digit.
0873: * Tests the provided table alias for uniqueness, generates alternative
0874: * table alias if the provided alias is already in use.
0875: *
0876: * @param tableAlias The table alias.
0877: * @param tableAliases The table aliases that are already in use. The
0878: * resulting table alias is added to this collection.
0879: * @param originalAliases The originally supplied aliases - generated
0880: * aliases should not match any of these.
0881: * @return The resulting table alias.
0882: * @since MMBase-1.7
0883: */
0884: // package access!
0885: String getUniqueTableAlias(String tableAlias,
0886: Set<String> tableAliases, Collection<String> originalAliases) {
0887:
0888: // If provided alias is not unique, try alternatives,
0889: // skipping alternatives that are already in originalAliases.
0890: if (tableAliases.contains(tableAlias)) {
0891: tableName = getTableName(tableAlias);
0892:
0893: tableAlias = tableName;
0894: char ch = '0';
0895: while (originalAliases.contains(tableAlias)
0896: || tableAliases.contains(tableAlias)) {
0897: // Can't create more than 11 aliases for same tablename.
0898: if (ch > '9') {
0899: throw new IndexOutOfBoundsException(
0900: "Failed to create unique table alias, because there "
0901: + "are already 11 aliases for this tablename: \""
0902: + tableName + "\"");
0903: }
0904: tableAlias = tableName + ch;
0905: ch++;
0906: }
0907: }
0908:
0909: // Unique table alias: add to collection, return as result.
0910: tableAliases.add(tableAlias);
0911: return tableAlias;
0912: }
0913:
0914: /**
0915: * Retrieves fieldnames from an expression, and adds these to a search
0916: * query.
0917: * The expression may be either a fieldname or a a functionname with a
0918: * (commaseparated) parameterlist between parenthesis
0919: * (parameters being expressions themselves).
0920: * <p>
0921: * Fieldnames must be formatted as <em>stepalias.field</em>.
0922: *
0923: * @param query The query.
0924: * @param expression The expression.
0925: * @param stepsByAlias Map, mapping step aliases to the steps in the query.
0926: * @param fieldsByAlias Map, mapping field aliases (fieldname prefixed by
0927: * table alias) to the stepfields in the query.
0928: * An entry is added for each stepfield added to the query.
0929: * @since MMBase-1.7
0930: */
0931: // package access!
0932: void addFields(BasicSearchQuery query, String expression,
0933: Map<String, BasicStep> stepsByAlias,
0934: Map<String, BasicStepField> fieldsByAlias) {
0935:
0936: // TODO RvM: stripping functions is this (still) necessary?.
0937: // Strip function(s).
0938: int pos1 = expression.indexOf('(');
0939: int pos2 = expression.indexOf(')');
0940: if (pos1 != -1 ^ pos2 != -1) {
0941: // Parenthesis do not match.
0942: throw new IllegalArgumentException(
0943: "Parenthesis do not match in expression: \""
0944: + expression + "\"");
0945: } else if (pos1 != -1) {
0946: // Function parameter list containing subexpression(s).
0947: String parameters = expression.substring(pos1 + 1, pos2);
0948: Iterator<String> iParameters = getFunctionParameters(
0949: parameters).iterator();
0950: while (iParameters.hasNext()) {
0951: String parameter = iParameters.next();
0952: addFields(query, parameter, stepsByAlias, fieldsByAlias);
0953: }
0954: } else if (!Character.isDigit(expression.charAt(0))) {
0955: int pos = expression.indexOf('.');
0956: if (pos < 1 || pos == (expression.length() - 1)) {
0957: throw new IllegalArgumentException(
0958: "Invalid fieldname: \"" + expression + "\"");
0959: }
0960: int bracketOffset = (expression.startsWith("[") && expression
0961: .endsWith("]")) ? 1 : 0;
0962: String stepAlias = expression.substring(0 + bracketOffset,
0963: pos);
0964: String fieldName = expression.substring(pos + 1
0965: - bracketOffset);
0966:
0967: BasicStep step = stepsByAlias.get(stepAlias);
0968: if (step == null) {
0969: throw new IllegalArgumentException(
0970: "Invalid step alias: \"" + stepAlias
0971: + "\" in fields list");
0972: }
0973: addField(query, step, fieldName, fieldsByAlias);
0974: }
0975: }
0976:
0977: /**
0978: * Adds field to a search query, unless it is already added.
0979: *
0980: * @param query The query.
0981: * @param step The non-null step corresponding to the field.
0982: * @param fieldName The fieldname.
0983: * @param fieldsByAlias Map, mapping field aliases (fieldname prefixed by
0984: * table alias) to the stepfields in the query.
0985: * An entry is added for each stepfield added to the query.
0986: * @since MMBase-1.7
0987: */
0988: private void addField(BasicSearchQuery query, BasicStep step,
0989: String fieldName, Map<String, BasicStepField> fieldsByAlias) {
0990:
0991: // Fieldalias = stepalias.fieldname.
0992: // This value is used to store the field in fieldsByAlias.
0993: // The actual alias of the field is not set.
0994: String fieldAlias = step.getAlias() + "." + fieldName;
0995: if (fieldsByAlias.containsKey(fieldAlias)) {
0996: // Added already.
0997: return;
0998: }
0999:
1000: MMObjectBuilder builder = mmb.getBuilder(step.getTableName());
1001: CoreField fieldDefs = builder.getField(fieldName);
1002: if (fieldDefs == null) {
1003: throw new IllegalArgumentException(
1004: "Not a known field of builder "
1005: + step.getTableName() + ": \"" + fieldName
1006: + "\"");
1007: }
1008:
1009: // Add the stepfield.
1010: BasicStepField stepField = query.addField(step, fieldDefs);
1011: fieldsByAlias.put(fieldAlias, stepField);
1012: }
1013:
1014: /**
1015: * Adds sorting orders to a search query.
1016: *
1017: * @param query The query.
1018: * @param fieldNames The fieldnames prefixed by the table aliases.
1019: * @param directions The corresponding sorting directions ("UP"/"DOWN").
1020: * @param fieldsByAlias Map, mapping field aliases (fieldname prefixed by
1021: * table alias) to the stepfields in the query.
1022: * @since MMBase-1.7
1023: */
1024: // package visibility!
1025: void addSortOrders(BasicSearchQuery query, List<String> fieldNames,
1026: List<String> directions,
1027: Map<String, BasicStepField> fieldsByAlias) {
1028:
1029: // Test if fieldnames are specified.
1030: if (fieldNames == null || fieldNames.size() == 0) {
1031: return;
1032: }
1033:
1034: int defaultSortOrder = SortOrder.ORDER_ASCENDING;
1035: if (directions != null && directions.size() != 0) {
1036: if (directions.get(0).trim().equalsIgnoreCase("DOWN")) {
1037: defaultSortOrder = SortOrder.ORDER_DESCENDING;
1038: }
1039: }
1040:
1041: Iterator<String> iFieldNames = fieldNames.iterator();
1042: Iterator<String> iDirections = directions.iterator();
1043: while (iFieldNames.hasNext()) {
1044: String fieldName = iFieldNames.next();
1045: StepField field = fieldsByAlias.get(fieldName);
1046: if (field == null) {
1047: // Field has not been added.
1048: field = ConstraintParser.getField(fieldName, query
1049: .getSteps());
1050: }
1051: if (field == null) {
1052: throw new IllegalArgumentException(
1053: "Invalid fieldname: \"" + fieldName + "\"");
1054: }
1055:
1056: // Add sort order.
1057: BasicSortOrder sortOrder = query.addSortOrder(field); // ascending
1058:
1059: // Change direction if needed.
1060: if (iDirections.hasNext()) {
1061: String direction = iDirections.next();
1062: if (direction.trim().equalsIgnoreCase("DOWN")) {
1063: sortOrder.setDirection(SortOrder.ORDER_DESCENDING);
1064: } else if (!direction.trim().equalsIgnoreCase("UP")) {
1065: throw new IllegalArgumentException(
1066: "Parameter directions contains an invalid value ("
1067: + direction
1068: + "), should be UP or DOWN.");
1069: }
1070:
1071: } else {
1072: sortOrder.setDirection(defaultSortOrder);
1073: }
1074: }
1075: }
1076:
1077: /**
1078: * Gets first step from list, that corresponds to the builder
1079: * of a specified node - or one of its parentbuilders.
1080: *
1081: * @param steps The steps.
1082: * @param nodeNumber The number identifying the node.
1083: * @return The step, or <code>null</code> when not found.
1084: * @since MMBase-1.7
1085: */
1086: // package visibility!
1087: Step getNodesStep(List<Step> steps, int nodeNumber) {
1088: if (nodeNumber < 0) {
1089: return null;
1090: }
1091:
1092: MMObjectNode node = getNode(nodeNumber);
1093: if (node == null) {
1094: return null;
1095: }
1096:
1097: MMObjectBuilder builder = node.parent;
1098: Step result = null;
1099: do {
1100: // Find step corresponding to builder.
1101: Iterator<Step> iSteps = steps.iterator();
1102: while (iSteps.hasNext() && result == null) {
1103: Step step = iSteps.next();
1104: if (step.getTableName().equals(builder.tableName)) { // should inheritance not be considered?
1105: // Found.
1106: result = step;
1107: }
1108: }
1109: // Not found, then try again with parentbuilder.
1110: builder = builder.getParentBuilder();
1111: } while (builder != null && result == null);
1112:
1113: return result;
1114: }
1115:
1116: /**
1117: * Adds relation directions.
1118: *
1119: * @param query The search query.
1120: * @param searchDirs Specifies in which direction relations are to be followed. You can specify a direction for each
1121: * relation in the path. If you specify less directions than there are relations, the last specified direction is used
1122: * for the remaining relations. If you specify an empty list the default direction is BOTH.
1123: * @param roles Map of tablenames mapped to <code>Integer</code> values,
1124: * representing the nodenumber of the corresponing RelDef node.
1125: * @since MMBase-1.7
1126: */
1127: // package visibility!
1128: void addRelationDirections(BasicSearchQuery query,
1129: List<Integer> searchDirs, Map<String, Integer> roles) {
1130:
1131: Iterator<Step> iSteps = query.getSteps().iterator();
1132: Iterator<Integer> iSearchDirs = searchDirs.iterator();
1133: int searchDir = RelationStep.DIRECTIONS_BOTH;
1134:
1135: if (!iSteps.hasNext())
1136: return; // nothing to be done.
1137: Step sourceStep = iSteps.next();
1138: Step destinationStep = null;
1139:
1140: while (iSteps.hasNext()) {
1141: if (destinationStep != null) {
1142: sourceStep = destinationStep;
1143: }
1144: BasicRelationStep relationStep = (BasicRelationStep) iSteps
1145: .next();
1146: destinationStep = iSteps.next();
1147: if (iSearchDirs.hasNext())
1148: searchDir = iSearchDirs.next().intValue();
1149:
1150: // FIXME this cast to BasicStep is ugly and should not be here in a clean implementation
1151:
1152: // Determine typedef number of the source-type.
1153: int sourceType = ((BasicStep) sourceStep).getBuilder()
1154: .getObjectType();
1155: // Determine the typedef number of the destination-type.
1156: int destinationType = ((BasicStep) destinationStep)
1157: .getBuilder().getObjectType();
1158:
1159: // Determine reldef number of the role.
1160: Integer role = roles.get(relationStep.getAlias());
1161:
1162: int roleInt;
1163: if (role != null) {
1164: roleInt = role.intValue();
1165: relationStep.setRole(role);
1166: } else {
1167: roleInt = -1;
1168: }
1169:
1170: if (!mmb.getTypeRel().optimizeRelationStep(relationStep,
1171: sourceType, destinationType, roleInt, searchDir)) {
1172: if (searchDir != RelationStep.DIRECTIONS_SOURCE
1173: && searchDir != RelationStep.DIRECTIONS_DESTINATION) {
1174: log
1175: .warn("No relation defined between "
1176: + sourceStep.getTableName()
1177: + " and "
1178: + destinationStep.getTableName()
1179: + " using "
1180: + relationStep
1181: + " with direction(s) "
1182: + getSearchDirString(searchDir)
1183: + ". Searching in 'destination' direction now, but perhaps the query should be fixed, because this should always result nothing.");
1184: } else {
1185: log
1186: .warn("No relation defined between "
1187: + sourceStep.getTableName()
1188: + " and "
1189: + destinationStep.getTableName()
1190: + " using "
1191: + relationStep
1192: + " with direction(s) "
1193: + getSearchDirString(searchDir)
1194: + ". Trying anyway, but perhaps the query should be fixed, because this should always result nothing.");
1195: }
1196: log.warn(Logging.applicationStacktrace());
1197: }
1198:
1199: }
1200: }
1201:
1202: }
|