/* * (C) Copyright 2014-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.storage.dbs; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACL_NAME; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ACP; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_ID; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_MIXIN_TYPES; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_NAME; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PARENT_ID; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_PRIMARY_TYPE; import static org.nuxeo.ecm.core.storage.dbs.DBSDocument.KEY_READ_ACL; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.query.QueryParseException; import org.nuxeo.ecm.core.query.sql.NXQL; import org.nuxeo.ecm.core.query.sql.model.Expression; import org.nuxeo.ecm.core.query.sql.model.MultiExpression; import org.nuxeo.ecm.core.query.sql.model.Operand; import org.nuxeo.ecm.core.query.sql.model.OrderByClause; import org.nuxeo.ecm.core.query.sql.model.OrderByExpr; import org.nuxeo.ecm.core.query.sql.model.Reference; import org.nuxeo.ecm.core.query.sql.model.SelectClause; import org.nuxeo.ecm.core.query.sql.model.SelectList; import org.nuxeo.ecm.core.schema.DocumentType; import org.nuxeo.ecm.core.schema.SchemaManager; import org.nuxeo.ecm.core.schema.types.ComplexType; import org.nuxeo.ecm.core.schema.types.Field; import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.Schema; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; import org.nuxeo.ecm.core.schema.types.primitives.DateType; import org.nuxeo.ecm.core.storage.ExpressionEvaluator; import org.nuxeo.ecm.core.storage.State; import org.nuxeo.runtime.api.Framework; /** * Expression evaluator for a {@link DBSDocument} state. * * @since 5.9.4 */ public class DBSExpressionEvaluator extends ExpressionEvaluator { private static final Log log = LogFactory.getLog(DBSExpressionEvaluator.class); private static final Long ZERO = Long.valueOf(0); private static final Long ONE = Long.valueOf(1); protected final SelectClause selectClause; protected final Expression expression; protected final OrderByClause orderByClause; protected SchemaManager schemaManager; protected List<String> documentTypes; protected State state; protected boolean parsing; /** Info about a value and how to compute it from the toplevel state or an iterator's state. */ protected static final class ValueInfo { /** * Traversed steps to compute this value from a state. Traversal steps can be: * <ul> * <li>String: a map key. * <li>Integer: a list element. * </ul> */ // also used to temporarily hold the full parsed reference public List<Serializable> steps; // original NXQL name in query public final String nxqlProp; public final String canonRef; public Type type; public boolean isTrueOrNullBoolean; public boolean isDateCast; /** The value computed for this reference. */ public Object value; public ValueInfo(List<Serializable> steps, String nxqlProp, String canonRef) { this.steps = steps; this.nxqlProp = nxqlProp; this.canonRef = canonRef; } public Object getValueForEvaluation() { if (type instanceof BooleanType) { // boolean evaluation is like 0 / 1 if (isTrueOrNullBoolean) { return TRUE.equals(value) ? ONE : ZERO; } else { return value == null ? null : (((Boolean) value).booleanValue() ? ONE : ZERO); } } else if (isDateCast) { if (value == null) { return null; } else if (value instanceof Calendar) { return castToDate((Calendar) value); } else { // array Object[] array = (Object[]) value; List<Calendar> dates = new ArrayList<>(array.length); for (Object v : array) { v = v instanceof Calendar ? castToDate((Calendar) v) : null; dates.add((Calendar) v); } return dates.toArray(); } } else if (value == null && type instanceof ListType && ((ListType) type).isArray()) { // don't use null, as list-based matches don't use ternary logic return new Object[0]; } else { return value; } } protected Calendar castToDate(Calendar date) { date.set(Calendar.HOUR_OF_DAY, 0); date.set(Calendar.MINUTE, 0); date.set(Calendar.SECOND, 0); date.set(Calendar.MILLISECOND, 0); return date; } @Override public String toString() { return "ValueInfo(" + canonRef + " " + steps + " = " + value + ")"; } } /** * Info about an iterator and how to compute it from a state. * <p> * The iterator iterates over a list of states or scalars and can be reset to a new list. * <p> * Also contains information about dependent values and iterators. */ protected static final class IterInfo implements Iterator<Object> { /** * Traversed steps to compute this iterator list from a state. Traversal steps can be: * <ul> * <li>String: a map key. * <li>Integer: a list element. * </ul> */ public final List<Serializable> steps; public final List<ValueInfo> dependentValueInfos = new ArrayList<>(2); public final List<IterInfo> dependentIterInfos = new ArrayList<>(2); protected List<Object> list; protected Iterator<Object> it; public IterInfo(List<Serializable> steps) { this.steps = steps; } public void setList(Object list) { if (list == null) { this.list = Collections.emptyList(); } else if (list instanceof List) { @SuppressWarnings("unchecked") List<Object> stateList = (List<Object>) list; this.list = stateList; } else { this.list = Arrays.asList((Object[]) list); } reset(); } public void reset() { it = list.iterator(); } @Override public boolean hasNext() { return it.hasNext(); } @Override public Object next() { return it.next(); } @Override public String toString() { return "IterInfo(" + System.identityHashCode(this) + "," + steps + ")"; } } protected static class DBSPathResolver implements PathResolver { protected final DBSSession session; public DBSPathResolver(DBSSession session) { this.session = session; } @Override public String getIdForPath(String path) { return session.getDocumentIdByPath(path); } } /** For each encountered reference in traversal order, the corresponding value info. */ protected List<ValueInfo> referenceValueInfos; /** Map of canonical reference to value info. */ protected Map<String, ValueInfo> canonicalReferenceValueInfos; /** Map of canonical reference prefix to iterator. */ protected Map<String, IterInfo> canonicalPrefixIterInfos; /** List of all iterators, in reversed order. */ protected List<IterInfo> allIterInfos; /** The toplevel iterators. */ protected List<IterInfo> toplevelIterInfos; /** The toplevel values, computed without wildcards. */ protected List<ValueInfo> toplevelValueInfos; // correlation to use for each uncorrelated wildcard (negative to avoid collisions with correlated ones) protected int uncorrelatedCounter; // did we find a wildcard in the SELECT projection or WHERE expression protected boolean hasWildcard; // which reference index is being visited, reset / updated during each pass protected int refCount; public DBSExpressionEvaluator(DBSSession session, SelectClause selectClause, Expression expression, OrderByClause orderByClause, String[] principals, boolean fulltextSearchDisabled) { super(new DBSPathResolver(session), principals, fulltextSearchDisabled); this.selectClause = selectClause; this.expression = expression; this.orderByClause = orderByClause; } public SelectClause getSelectClause() { return selectClause; } public Expression getExpression() { return expression; } public OrderByClause getOrderByClause() { return orderByClause; } protected List<String> getDocumentTypes() { // TODO precompute in SchemaManager if (documentTypes == null) { documentTypes = new ArrayList<>(); for (DocumentType docType : schemaManager.getDocumentTypes()) { documentTypes.add(docType.getName()); } } return documentTypes; } protected Set<String> getMixinDocumentTypes(String mixin) { Set<String> types = schemaManager.getDocumentTypeNamesForFacet(mixin); return types == null ? Collections.emptySet() : types; } protected boolean isNeverPerInstanceMixin(String mixin) { return schemaManager.getNoPerDocumentQueryFacets().contains(mixin); } /** * Initializes parsing datastructures. */ public void parse() { schemaManager = Framework.getService(SchemaManager.class); referenceValueInfos = new ArrayList<>(); canonicalReferenceValueInfos = new HashMap<>(); allIterInfos = new ArrayList<>(); toplevelIterInfos = new ArrayList<>(); toplevelValueInfos = new ArrayList<>(); canonicalPrefixIterInfos = new HashMap<>(); uncorrelatedCounter = -1; hasWildcard = false; // we do parsing using the ExpressionEvaluator to be sure that references // are visited in the same order as when we'll do actual expression evaluation parsing = true; walkAll(); parsing = false; // we use all iterators in reversed ordered to increment them lexicographically from the end Collections.reverse(allIterInfos); } /** * Returns the projection matches for a given state. */ public List<Map<String, Serializable>> matches(State state) { if (!checkSecurity(state)) { return Collections.emptyList(); } this.state = state; // needed for mixin types evaluation // initializes values and wildcards initializeValuesAndIterators(state); List<Map<String, Serializable>> matches = new ArrayList<>(); for (;;) { Map<String, Serializable> projection = walkAll(); if (projection != null) { matches.add(projection); } if (!hasWildcard) { // all projections will be the same, get at most one break; } boolean finished = incrementIterators(); if (finished) { break; } } return matches; } protected boolean checkSecurity(State state) { if (principals == null) { return true; } String[] racl = (String[]) state.get(KEY_READ_ACL); if (racl == null) { log.error("NULL racl for " + state.get(KEY_ID)); return false; } for (String user : racl) { if (principals.contains(user)) { return true; } } return false; } /** * Does one walk of the expression, using the wildcardIndexes currently defined. */ protected Map<String, Serializable> walkAll() { refCount = 0; Map<String, Serializable> projection = walkSelectClauseAndOrderBy(selectClause, orderByClause); Object res = walkExpression(expression); if (TRUE.equals(res)) { // returns one match return projection; } else { return null; } } /** * Walks the select clause and order by clause, and returns the projection. */ public Map<String, Serializable> walkSelectClauseAndOrderBy(SelectClause selectClause, OrderByClause orderByClause) { Map<String, Serializable> projection = new HashMap<>(); boolean projectionOnFulltextScore = false; boolean sortOnFulltextScore = false; SelectList elements = selectClause.getSelectList(); for (Operand op : elements.values()) { if (op instanceof Reference) { Reference ref = (Reference) op; if (ref.name.equals(NXQL.ECM_FULLTEXT_SCORE)) { projectionOnFulltextScore = true; } addProjection(ref, projection); } } if (orderByClause != null) { for (OrderByExpr obe : orderByClause.elements) { Reference ref = obe.reference; if (ref.name.equals(NXQL.ECM_FULLTEXT_SCORE)) { sortOnFulltextScore = true; } addProjection(ref, projection); } } if (projectionOnFulltextScore || sortOnFulltextScore) { if (!parsing) { if (!hasFulltext) { throw new QueryParseException( NXQL.ECM_FULLTEXT_SCORE + " cannot be used without " + NXQL.ECM_FULLTEXT); } projection.put(NXQL.ECM_FULLTEXT_SCORE, Double.valueOf(1)); } } return projection; } protected void addProjection(Reference ref, Map<String, Serializable> projection) { String name = ref.name; if (name.equals(NXQL.ECM_PATH)) { // ecm:path is special, computed and not stored in database if (!parsing) { // to compute PATH we need NAME, ID and PARENT_ID for all states projection.put(NXQL.ECM_NAME, state.get(KEY_NAME)); projection.put(NXQL.ECM_UUID, state.get(KEY_ID)); projection.put(NXQL.ECM_PARENTID, state.get(KEY_PARENT_ID)); } return; } ValueInfo valueInfo = walkReferenceGetValueInfo(ref); if (!parsing) { projection.put(valueInfo.nxqlProp, (Serializable) valueInfo.value); } } public boolean hasWildcardProjection() { return selectClause.getSelectList().values().stream().anyMatch( operand -> operand instanceof Reference && ((Reference) operand).name.contains("*")); } @Override public Object walkReference(Reference ref) { return walkReferenceGetValueInfo(ref).getValueForEvaluation(); } protected ValueInfo walkReferenceGetValueInfo(Reference ref) { if (parsing) { ValueInfo valueInfo = parseReference(ref); referenceValueInfos.add(valueInfo); return valueInfo; } else { return referenceValueInfos.get(refCount++); } } /** * Parses and computes value and iterator information for a reference. */ protected ValueInfo parseReference(Reference ref) { ValueInfo parsed = parseReference(ref.name); if (DATE_CAST.equals(ref.cast)) { Type type = parsed.type; if (!(type instanceof DateType || (type instanceof ListType && ((ListType) type).getFieldType() instanceof DateType))) { throw new QueryParseException("Cannot cast to " + ref.cast + ": " + ref.name); } parsed.isDateCast = true; } return canonicalReferenceValueInfos.computeIfAbsent(parsed.canonRef, k -> { List<IterInfo> iterInfos = toplevelIterInfos; List<ValueInfo> valueInfos = toplevelValueInfos; List<String> prefix = new ArrayList<>(3); // canonical prefix List<Serializable> steps = new ArrayList<>(1); for (Serializable step : parsed.steps) { if (step instanceof String) { // complex sub-property prefix.add((String) step); steps.add(step); continue; } if (step instanceof Integer) { // explicit list index prefix.add(step.toString()); steps.add(step); continue; } // wildcard hasWildcard = true; prefix.add("*" + step); String canonPrefix = StringUtils.join(prefix, '/'); IterInfo iter = canonicalPrefixIterInfos.get(canonPrefix); if (iter == null) { // first time we see this wildcard prefix, use a new iterator iter = new IterInfo(steps); canonicalPrefixIterInfos.put(canonPrefix, iter); allIterInfos.add(iter); iterInfos.add(iter); } iterInfos = iter.dependentIterInfos; valueInfos = iter.dependentValueInfos; // reset traversal for next cycle steps = new ArrayList<>(); } // truncate traversal to steps since last wildcard, may be empty if referencing wildcard list directly parsed.steps = steps; valueInfos.add(parsed); return parsed; }); } /** * Gets the canonical reference and parsed reference for this reference name. * <p> * The parsed reference is a list of components to traverse to get the value: * <ul> * <li>String = map key * <li>Integer = list element * <li>Long = wildcard correlation number (pos/neg) * </ul> * * @return the canonical reference (with resolved uncorrelated wildcards) */ protected ValueInfo parseReference(String name) { String[] parts = name.split("/"); // convert first part to internal representation, and canonicalize prefixed schema String prop = parts[0]; Type type; boolean isTrueOrNullBoolean; if (prop.startsWith(NXQL.ECM_PREFIX)) { prop = DBSSession.convToInternal(prop); if (prop.equals(KEY_ACP)) { return parseACP(parts, name); } type = DBSSession.getType(prop); isTrueOrNullBoolean = true; } else { Field field = schemaManager.getField(prop); if (field == null) { if (prop.indexOf(':') > -1) { throw new QueryParseException("No such property: " + name); } // check without prefix // TODO precompute this in SchemaManagerImpl for (Schema schema : schemaManager.getSchemas()) { if (!StringUtils.isBlank(schema.getNamespace().prefix)) { // schema with prefix, do not consider as candidate continue; } if (schema != null) { field = schema.getField(prop); if (field != null) { break; } } } if (field == null) { throw new QueryParseException("No such property: " + name); } } type = field.getType(); isTrueOrNullBoolean = false; prop = field.getName().getPrefixedName(); } parts[0] = prop; // canonical prefix used to find shared values (foo/*1 referenced twice always uses the same value) List<String> canonParts = new ArrayList<>(parts.length); List<Serializable> steps = new ArrayList<>(parts.length); boolean firstPart = true; for (String part : parts) { int c = part.indexOf('['); if (c >= 0) { // compat xpath foo[123] -> 123 part = part.substring(c + 1, part.length() - 1); } Serializable step; if (NumberUtils.isDigits(part)) { // explicit list index step = Integer.valueOf(part); type = ((ListType) type).getFieldType(); } else if (!part.startsWith("*")) { // complex sub-property step = part; if (!firstPart) { // we already computed the type of the first part Field field = ((ComplexType) type).getField(part); if (field == null) { throw new QueryParseException("No such property: " + name); } type = field.getType(); } } else { // wildcard int corr; if (part.length() == 1) { // uncorrelated wildcard corr = uncorrelatedCounter--; // negative part = "*" + corr; // unique correlation } else { // correlated wildcard, use correlation number String digits = part.substring(1); if (!NumberUtils.isDigits(digits)) { throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name); } corr = Integer.parseInt(digits); if (corr < 0) { throw new QueryParseException("Invalid wildcard (" + part + ") in property: " + name); } } step = Long.valueOf(corr); type = ((ListType) type).getFieldType(); } canonParts.add(part); steps.add(step); firstPart = false; } String canonRef = StringUtils.join(canonParts, '/'); ValueInfo valueInfo = new ValueInfo(steps, name, canonRef); valueInfo.type = type; valueInfo.isTrueOrNullBoolean = isTrueOrNullBoolean; return valueInfo; } protected ValueInfo parseACP(String[] parts, String name) { if (parts.length != 3) { throw new QueryParseException("No such property: " + name); } String wildcard = parts[1]; if (NumberUtils.isDigits(wildcard)) { throw new QueryParseException("Cannot use explicit index in ACLs: " + name); } int corr; if (wildcard.length() == 1) { // uncorrelated wildcard corr = uncorrelatedCounter--; // negative wildcard = "*" + corr; // unique correlation } else { // correlated wildcard, use correlation number String digits = wildcard.substring(1); if (!NumberUtils.isDigits(digits)) { throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name); } corr = Integer.parseInt(digits); if (corr < 0) { throw new QueryParseException("Invalid wildcard (" + wildcard + ") in property: " + name); } } String subPart = DBSSession.convToInternalAce(parts[2]); if (subPart == null) { throw new QueryParseException("No such property: " + name); } List<Serializable> steps; String canonRef; if (subPart.equals(KEY_ACL_NAME)) { steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL_NAME)); canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL_NAME; } else { // for the second iterator we want a correlation number tied to the first one int corr2 = corr * 1000000; String wildcard2 = "*" + corr2; steps = new ArrayList<>(Arrays.asList(KEY_ACP, Long.valueOf(corr), KEY_ACL, Long.valueOf(corr2), subPart)); canonRef = KEY_ACP + '/' + wildcard + '/' + KEY_ACL + '/' + wildcard2 + '/' + subPart; } ValueInfo valueInfo = new ValueInfo(steps, name, canonRef); valueInfo.type = DBSSession.getType(subPart); valueInfo.isTrueOrNullBoolean = false; // TODO check ok return valueInfo; } /** * Initializes toplevel values and iterators for a given state. */ protected void initializeValuesAndIterators(State state) { init(state, toplevelValueInfos, toplevelIterInfos); } /** * Initializes values and iterators for a given state. */ protected void init(Object state, List<ValueInfo> valueInfos, List<IterInfo> iterInfos) { for (ValueInfo valueInfo : valueInfos) { valueInfo.value = traverse(state, valueInfo.steps); } for (IterInfo iterInfo : iterInfos) { Object value = traverse(state, iterInfo.steps); iterInfo.setList(value); Object iterState = iterInfo.hasNext() ? iterInfo.next() : null; init(iterState, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos); } } /** * Traverses an object in a series of steps. */ protected Object traverse(Object value, List<Serializable> steps) { for (Serializable step : steps) { value = traverse(value, step); } return value; } /** * Traverses a single step. */ protected Object traverse(Object value, Serializable step) { if (step instanceof String) { // complex sub-property if (value != null && !(value instanceof State)) { throw new QueryParseException("Invalid property " + step + " (no State but " + value.getClass() + ")"); } return value == null ? null : ((State) value).get(step); } else if (step instanceof Integer) { // explicit list index int index = ((Integer) step).intValue(); if (value == null) { return null; } else if (value instanceof List) { @SuppressWarnings("unchecked") List<Serializable> list = (List<Serializable>) value; if (index >= list.size()) { return null; } else { return list.get(index); } } else if (value instanceof Object[]) { Object[] array = (Object[]) value; if (index >= array.length) { return null; } else { return array[index]; } } else { throw new QueryParseException( "Invalid property " + step + " (no List/array but " + value.getClass() + ")"); } } else { throw new QueryParseException("Invalid step " + step + " (unknown class " + step.getClass() + ")"); } } /** * Increments iterators lexicographically. * <p> * Returns {@code true} when all iterations are finished. */ protected boolean incrementIterators() { // we iterate on a pre-reversed allIterInfos list as this ensure that // dependent iterators are incremented before those that control them boolean more = false; for (IterInfo iterInfo : allIterInfos) { more = iterInfo.hasNext(); if (!more) { // end of this iterator, reset and !more will carry to next one iterInfo.reset(); } // get the current value, if any Object state = iterInfo.hasNext() ? iterInfo.next() : null; // recompute dependent stuff init(state, iterInfo.dependentValueInfos, iterInfo.dependentIterInfos); if (more) { break; } } return !more; } /** * {@inheritDoc} * <p> * ecm:mixinTypes IN ('Foo', 'Bar') * <p> * primarytype IN (... types with Foo or Bar ...) OR mixintypes LIKE '%Foo%' OR mixintypes LIKE '%Bar%' * <p> * ecm:mixinTypes NOT IN ('Foo', 'Bar') * <p> * primarytype IN (... types without Foo nor Bar ...) AND (mixintypes NOT LIKE '%Foo%' AND mixintypes NOT LIKE * '%Bar%' OR mixintypes IS NULL) */ @Override public Boolean walkMixinTypes(List<String> mixins, boolean include) { if (parsing) { return null; } /* * Primary types that match. */ Set<String> matchPrimaryTypes; if (include) { matchPrimaryTypes = new HashSet<>(); for (String mixin : mixins) { matchPrimaryTypes.addAll(getMixinDocumentTypes(mixin)); } } else { matchPrimaryTypes = new HashSet<>(getDocumentTypes()); for (String mixin : mixins) { matchPrimaryTypes.removeAll(getMixinDocumentTypes(mixin)); } } /* * Instance mixins that match. */ Set<String> matchMixinTypes = new HashSet<>(); for (String mixin : mixins) { if (!isNeverPerInstanceMixin(mixin)) { matchMixinTypes.add(mixin); } } /* * Evaluation. */ String primaryType = (String) state.get(KEY_PRIMARY_TYPE); Object[] mixinTypesArray = (Object[]) state.get(KEY_MIXIN_TYPES); List<Object> mixinTypes = mixinTypesArray == null ? Collections.emptyList() : Arrays.asList(mixinTypesArray); if (include) { // primary types if (matchPrimaryTypes.contains(primaryType)) { return TRUE; } // mixin types matchMixinTypes.retainAll(mixinTypes); // intersection return Boolean.valueOf(!matchMixinTypes.isEmpty()); } else { // primary types if (!matchPrimaryTypes.contains(primaryType)) { return FALSE; } // mixin types matchMixinTypes.retainAll(mixinTypes); // intersection return Boolean.valueOf(matchMixinTypes.isEmpty()); } } @Override public String toString() { StringBuilder sb = new StringBuilder("SELECT "); sb.append(selectClause); sb.append(" WHERE "); if (expression instanceof MultiExpression) { for (Iterator<Operand> it = ((MultiExpression) expression).values.iterator(); it.hasNext();) { Operand operand = it.next(); sb.append(operand.toString()); if (it.hasNext()) { sb.append(" AND "); } } } else { sb.append(expression); } if (orderByClause != null) { sb.append(" ORDER BY "); sb.append(orderByClause); } return sb.toString(); } }