package com.avaje.ebeaninternal.server.querydefn; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import com.avaje.ebean.ExpressionFactory; import com.avaje.ebean.FetchConfig; import com.avaje.ebean.OrderBy; import com.avaje.ebean.Query; import com.avaje.ebean.event.BeanQueryRequest; import com.avaje.ebeaninternal.api.HashQueryPlanBuilder; import com.avaje.ebeaninternal.api.SpiExpression; import com.avaje.ebeaninternal.api.SpiExpressionFactory; import com.avaje.ebeaninternal.api.SpiExpressionList; import com.avaje.ebeaninternal.api.SpiQuery; import com.avaje.ebeaninternal.server.expression.FilterExprPath; import com.avaje.ebeaninternal.server.lib.util.StringHelper; import com.avaje.ebeaninternal.server.query.SplitName; import com.avaje.ebeaninternal.util.FilterExpressionList; /** * Represents the Properties of an Object Relational query. */ public class OrmQueryProperties implements Serializable { private static final long serialVersionUID = -8785582703966455658L; private String parentPath; private String path; private String properties; private String trimmedProperties; /** * NB: -1 means no +query, 0 means use the default batch size. */ private int queryFetchBatch = -1; private boolean queryFetchAll; /** * NB: -1 means no +lazy, 0 means use the default batch size. */ private int lazyFetchBatch = -1; private FetchConfig fetchConfig; private boolean cache; private boolean readOnly; /** * Note this SHOULD be a LinkedHashSet to preserve order of the properties. This is to make using * SqlSelect easier with predictable property/column ordering. */ private Set included; /** * Included bean joins. */ private Set includedBeanJoin; /** * Add these properties to the select so that the foreign key columns are included in the query. */ private Set secondaryQueryJoins; private List secondaryChildren; /** * OrderBy properties that where on the main query but moved here as they relate to this (query * join). */ @SuppressWarnings("rawtypes") private OrderBy orderBy; /** * A filter that can be applied to the fetch of this path in the object graph. */ @SuppressWarnings("rawtypes") private SpiExpressionList filterMany; /** * Construct with a given path (null == root path). */ public OrmQueryProperties(String path) { this.path = path; this.parentPath = SplitName.parent(path); } public OrmQueryProperties() { this(null); } /** * Used by query language parser. */ public OrmQueryProperties(String path, String properties) { this(path); setProperties(properties); } /** * Move a OrderBy.Property from the main query to this query join. */ @SuppressWarnings("rawtypes") public void addSecJoinOrderProperty(OrderBy.Property orderProp) { if (orderBy == null) { orderBy = new OrderBy(); } orderBy.add(orderProp); } /** * Set the Fetch configuration options for this path. */ public void setFetchConfig(FetchConfig fetchConfig) { if (fetchConfig != null) { this.fetchConfig = fetchConfig; lazyFetchBatch = fetchConfig.getLazyBatchSize(); queryFetchBatch = fetchConfig.getQueryBatchSize(); queryFetchAll = fetchConfig.isQueryAll(); } } public FetchConfig getFetchConfig() { return fetchConfig; } /** * Set the comma delimited properties to fetch for this path. *

* This can include the +query and +lazy type hints. *

*/ public void setProperties(String properties) { this.properties = properties; this.trimmedProperties = properties; parseProperties(); if (!isAllProperties()) { Set parsed = parseIncluded(trimmedProperties); if (parsed.contains("*")) { this.included = null; } else { this.included = parsed; } } else { this.included = null; } } private boolean isAllProperties() { return (trimmedProperties == null) || (trimmedProperties.length() == 0) || "*".equals(trimmedProperties); } /** * Return the expressions used to filter on this path. This should be a many path to use this * method. */ @SuppressWarnings({ "rawtypes", "unchecked" }) public SpiExpressionList filterMany(Query rootQuery) { if (filterMany == null) { FilterExprPath exprPath = new FilterExprPath(path); SpiExpressionFactory queryEf = (SpiExpressionFactory) rootQuery.getExpressionFactory(); ExpressionFactory filterEf = queryEf.createExpressionFactory();// exprPath); filterMany = new FilterExpressionList(exprPath, filterEf, rootQuery); // by default we need to make this a 'query join' now queryFetchAll = true; queryFetchBatch = 100; lazyFetchBatch = 100; } return filterMany; } /** * Return the filterMany expression list (can be null). */ public SpiExpressionList getFilterManyTrimPath(int trimPath) { if (filterMany == null) { return null; } return filterMany.trimPath(trimPath); } /** * Return the filterMany expression list (can be null). */ public SpiExpressionList getFilterMany() { return filterMany; } /** * Set the filterMany expression list. */ public void setFilterMany(SpiExpressionList filterMany) { this.filterMany = filterMany; } /** * Set the properties from deployment default FetchTypes. */ public void setDefaultProperties(String properties, Set included) { this.properties = properties; this.trimmedProperties = properties; this.included = included; } /** * Set the properties from a matching autofetch tuned properties. */ public void setTunedProperties(OrmQueryProperties tunedProperties) { this.properties = tunedProperties.properties; this.trimmedProperties = tunedProperties.trimmedProperties; this.included = tunedProperties.included; } /** * Define the select and joins for this query. */ @SuppressWarnings("unchecked") public void configureBeanQuery(SpiQuery query) { if (trimmedProperties != null && trimmedProperties.length() > 0) { query.select(trimmedProperties); } if (filterMany != null) { SpiExpressionList trimPath = filterMany.trimPath(path.length() + 1); List underlyingList = trimPath.getUnderlyingList(); for (SpiExpression spiExpression : underlyingList) { query.where().add(spiExpression); } } if (secondaryChildren != null) { int trimPath = path.length() + 1; for (int i = 0; i < secondaryChildren.size(); i++) { OrmQueryProperties p = secondaryChildren.get(i); String path = p.getPath(); path = path.substring(trimPath); query.fetch(path, p.getProperties(), p.getFetchConfig()); query.setFilterMany(path, p.getFilterManyTrimPath(trimPath)); } } if (orderBy != null) { query.setOrder(orderBy.copyWithTrim(path)); } } /** * Creates a copy of the OrmQueryProperties. */ public OrmQueryProperties copy() { OrmQueryProperties copy = new OrmQueryProperties(); copy.parentPath = parentPath; copy.path = path; copy.properties = properties; copy.cache = cache; copy.readOnly = readOnly; copy.queryFetchAll = queryFetchAll; copy.queryFetchBatch = queryFetchBatch; copy.lazyFetchBatch = lazyFetchBatch; copy.filterMany = filterMany; if (included != null) { copy.included = new HashSet(included); } if (includedBeanJoin != null) { copy.includedBeanJoin = new HashSet(includedBeanJoin); } return copy; } public boolean hasSelectClause() { if ("*".equals(trimmedProperties)) { // explicitly selected all properties return true; } // explicitly selected some properties return included != null; } public String toString() { String s = ""; if (path != null) { s += path + " "; } if (properties != null) { s += "(" + properties + ") "; } else if (included != null) { s += "(" + included + ") "; } return s; } public boolean isChild(OrmQueryProperties possibleChild) { return possibleChild.getPath().startsWith(path + "."); } /** * For secondary queries add a child element. */ public void add(OrmQueryProperties child) { if (secondaryChildren == null) { secondaryChildren = new ArrayList(); } secondaryChildren.add(child); } public int autofetchPlanHash() { int hc = (path != null ? path.hashCode() : 1); if (properties != null) { hc = hc * 31 + properties.hashCode(); } else { hc = hc * 31 + (included != null ? included.hashCode() : 1); } return hc; } /** * Calculate the query plan hash. */ @SuppressWarnings("unchecked") public void queryPlanHash(BeanQueryRequest request, HashQueryPlanBuilder builder) { builder.add(path); if (properties != null) { builder.add(properties); } else { builder.add(included); } builder.add(filterMany != null); if (filterMany != null) { filterMany.queryPlanHash(request, builder); } } public String getProperties() { return properties; } /** * Return true if this has properties. */ public boolean hasProperties() { return properties != null || included != null; } /** * Return true if this property is included as a bean join. *

* If a property is included as a bean join then it should not be included as a reference/proxy to * avoid duplication. *

*/ public boolean isIncludedBeanJoin(String propertyName) { if (includedBeanJoin == null) { return false; } else { return includedBeanJoin.contains(propertyName); } } /** * Add a bean join property. */ public void includeBeanJoin(String propertyName) { if (includedBeanJoin == null) { includedBeanJoin = new HashSet(); } includedBeanJoin.add(propertyName); } public boolean allProperties() { return included == null; } /** * This excludes the bean joined properties. *

* This is because bean joins will have there own node in the SqlTree. *

*/ public Set getSelectProperties() { if (secondaryQueryJoins == null) { return included; } LinkedHashSet temp = new LinkedHashSet(secondaryQueryJoins.size() + included.size()); temp.addAll(included); temp.addAll(secondaryQueryJoins); return temp; } public void addSecondaryQueryJoin(String property) { if (secondaryQueryJoins == null) { secondaryQueryJoins = new HashSet(4); } secondaryQueryJoins.add(property); } /** * Return all the properties including the bean joins. This is the set that will be used by * EntityBeanIntercept to determine if a property needs to be lazy loaded. */ public Set getAllIncludedProperties() { if (included == null) { return null; } if (includedBeanJoin == null && secondaryQueryJoins == null) { return new LinkedHashSet(included); } LinkedHashSet s = new LinkedHashSet(2 * (included.size() + 5)); if (included != null) { s.addAll(included); } if (includedBeanJoin != null) { s.addAll(includedBeanJoin); } if (secondaryQueryJoins != null) { s.addAll(secondaryQueryJoins); } return s; } public boolean isIncluded(String propName) { if (includedBeanJoin != null && includedBeanJoin.contains(propName)) { return false; } if (included == null) { // all properties included return true; } return included.contains(propName); } public OrmQueryProperties setQueryFetch(int batch, boolean queryFetchAll) { this.queryFetchBatch = batch; this.queryFetchAll = queryFetchAll; return this; } public boolean isFetchJoin() { return !isQueryFetch() && !isLazyFetch(); } public boolean isQueryFetch() { return queryFetchBatch > -1; } public int getQueryFetchBatch() { return queryFetchBatch; } public boolean isQueryFetchAll() { return queryFetchAll; } public boolean isLazyFetch() { return lazyFetchBatch > -1; } public int getLazyFetchBatch() { return lazyFetchBatch; } public boolean isReadOnly() { return readOnly; } public boolean isCache() { return cache; } public String getParentPath() { return parentPath; } public String getPath() { return path; } private void parseProperties() { if (trimmedProperties == null) { return; } int pos = trimmedProperties.indexOf("+readonly"); if (pos > -1) { trimmedProperties = StringHelper.replaceString(trimmedProperties, "+readonly", ""); this.readOnly = true; } pos = trimmedProperties.indexOf("+cache"); if (pos > -1) { trimmedProperties = StringHelper.replaceString(trimmedProperties, "+cache", ""); this.cache = true; } pos = trimmedProperties.indexOf("+query"); if (pos > -1) { queryFetchBatch = parseBatchHint(pos, "+query"); } pos = trimmedProperties.indexOf("+lazy"); if (pos > -1) { lazyFetchBatch = parseBatchHint(pos, "+lazy"); } trimmedProperties = trimmedProperties.trim(); while (trimmedProperties.startsWith(",")) { trimmedProperties = trimmedProperties.substring(1).trim(); } } private int parseBatchHint(int pos, String option) { int startPos = pos + option.length(); int endPos = findEndPos(startPos, trimmedProperties); if (endPos == -1) { trimmedProperties = StringHelper.replaceString(trimmedProperties, option, ""); return 0; } else { String batchParam = trimmedProperties.substring(startPos + 1, endPos); if (endPos + 1 >= trimmedProperties.length()) { trimmedProperties = trimmedProperties.substring(0, pos); } else { trimmedProperties = trimmedProperties.substring(0, pos) + trimmedProperties.substring(endPos + 1); } return Integer.parseInt(batchParam); } } private int findEndPos(int pos, String props) { if (pos < props.length()) { if (props.charAt(pos) == '(') { int endPara = props.indexOf(')', pos + 1); if (endPara == -1) { String m = "Error could not find ')' in " + props + " after position " + pos; throw new RuntimeException(m); } return endPara; } } return -1; } /** * Parse the include separating by comma or semicolon. */ private static Set parseIncluded(String rawList) { String[] res = rawList.split(","); LinkedHashSet set = new LinkedHashSet(res.length + 3); String temp; for (int i = 0; i < res.length; i++) { temp = res[i].trim(); if (temp.length() > 0) { set.add(temp); } } return Collections.unmodifiableSet(set); } }