#919 - io.ebean package initial

This commit is contained in:
Rob Bygrave
2016-12-11 23:23:22 +13:00
parent 5821dc96b9
commit 971e2dc91b
2134 changed files with 10721 additions and 8652 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,200 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.EbeanServer;
import io.ebeaninternal.api.BindParams;
import io.ebeaninternal.api.SpiUpdate;
import java.io.Serializable;
/**
* Default implementation of OrmUpdate.
*/
public final class DefaultOrmUpdate<T> implements SpiUpdate<T>, Serializable {
private static final long serialVersionUID = -8791423602246515438L;
private transient final EbeanServer server;
private final Class<?> beanType;
/**
* The name of the update.
*/
private final String name;
/**
* The parameters used to bind to the sql.
*/
private final BindParams bindParams = new BindParams();
/**
* The sql update or delete statement.
*/
private final String updateStatement;
/**
* Automatically detect the table being modified by this sql. This will
* register this information so that eBean invalidates cached objects if
* required.
*/
private boolean notifyCache = true;
private int timeout;
private String generatedSql;
private final String baseTable;
private final OrmUpdateType type;
/**
* Create with a specific server. This means you can use the
* UpdateSql.execute() method.
*/
public DefaultOrmUpdate(Class<?> beanType, EbeanServer server, String baseTable, String updateStatement) {
this.beanType = beanType;
this.server = server;
this.baseTable = baseTable;
this.name = "";
this.updateStatement = updateStatement;
this.type = deriveType(updateStatement);
}
public DefaultOrmUpdate<T> setTimeout(int secs) {
this.timeout = secs;
return this;
}
public Class<?> getBeanType() {
return beanType;
}
/**
* Return the timeout in seconds.
*/
public int getTimeout() {
return timeout;
}
private SpiUpdate.OrmUpdateType deriveType(String updateStatement) {
updateStatement = updateStatement.trim();
int spacepos = updateStatement.indexOf(' ');
if (spacepos == -1) {
return SpiUpdate.OrmUpdateType.UNKNOWN;
} else {
String firstWord = updateStatement.substring(0, spacepos);
if (firstWord.equalsIgnoreCase("update")) {
return SpiUpdate.OrmUpdateType.UPDATE;
} else if (firstWord.equalsIgnoreCase("insert")) {
return SpiUpdate.OrmUpdateType.INSERT;
} else if (firstWord.equalsIgnoreCase("delete")) {
return SpiUpdate.OrmUpdateType.DELETE;
} else {
return SpiUpdate.OrmUpdateType.UNKNOWN;
}
}
}
public int execute() {
return server.execute(this);
}
/**
* Set this to false if you don't want eBean to automatically deduce the
* table modification information and process it.
* <p>
* Set this to false if you don't want any cache invalidation or text index
* management to occur. You may do this when say you update only one column
* and you know that it is not important for cached objects or text indexes.
* </p>
*/
public DefaultOrmUpdate<T> setNotifyCache(boolean notifyCache) {
this.notifyCache = notifyCache;
return this;
}
/**
* Return true if the cache should be notified so that invalidates
* appropriate objects.
*/
public boolean isNotifyCache() {
return notifyCache;
}
public String getName() {
return name;
}
public String getUpdateStatement() {
return updateStatement;
}
public DefaultOrmUpdate<T> set(int position, Object value) {
bindParams.setParameter(position, value);
return this;
}
public DefaultOrmUpdate<T> setParameter(int position, Object value) {
bindParams.setParameter(position, value);
return this;
}
public DefaultOrmUpdate<T> setNull(int position, int jdbcType) {
bindParams.setNullParameter(position, jdbcType);
return this;
}
public DefaultOrmUpdate<T> setNullParameter(int position, int jdbcType) {
bindParams.setNullParameter(position, jdbcType);
return this;
}
public DefaultOrmUpdate<T> set(String name, Object value) {
bindParams.setParameter(name, value);
return this;
}
public DefaultOrmUpdate<T> setParameter(String name, Object param) {
bindParams.setParameter(name, param);
return this;
}
public DefaultOrmUpdate<T> setNull(String name, int jdbcType) {
bindParams.setNullParameter(name, jdbcType);
return this;
}
public DefaultOrmUpdate<T> setNullParameter(String name, int jdbcType) {
bindParams.setNullParameter(name, jdbcType);
return this;
}
/**
* Return the bind parameters.
*/
public BindParams getBindParams() {
return bindParams;
}
public String getGeneratedSql() {
return generatedSql;
}
public void setGeneratedSql(String generatedSql) {
this.generatedSql = generatedSql;
}
public String getBaseTable() {
return baseTable;
}
public OrmUpdateType getOrmUpdateType() {
return type;
}
}
@@ -0,0 +1,125 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.EbeanServer;
import io.ebean.SqlRow;
import io.ebeaninternal.api.BindParams;
import io.ebeaninternal.api.SpiSqlQuery;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* Default implementation of SQuery - SQL Query.
*/
public class DefaultRelationalQuery implements SpiSqlQuery {
private static final long serialVersionUID = -1098305779779591068L;
private final transient EbeanServer server;
private String query;
private int firstRow;
private int maxRows;
private int timeout;
private int bufferFetchSizeHint;
/**
* Bind parameters when using the query language.
*/
private final BindParams bindParams = new BindParams();
/**
* Additional supply a query detail object.
*/
public DefaultRelationalQuery(EbeanServer server, String query) {
this.server = server;
this.query = query;
}
public DefaultRelationalQuery setQuery(String query) {
this.query = query;
return this;
}
@Override
public void findEach(Consumer<SqlRow> consumer) {
server.findEach(this, consumer, null);
}
@Override
public void findEachWhile(Predicate<SqlRow> consumer) {
server.findEachWhile(this, consumer, null);
}
public List<SqlRow> findList() {
return server.findList(this, null);
}
public SqlRow findUnique() {
return server.findUnique(this, null);
}
public DefaultRelationalQuery setParameter(int position, Object value) {
bindParams.setParameter(position, value);
return this;
}
public DefaultRelationalQuery setParameter(String paramName, Object value) {
bindParams.setParameter(paramName, value);
return this;
}
public String toString() {
return "SqlQuery [" + query + "]";
}
public int getFirstRow() {
return firstRow;
}
public DefaultRelationalQuery setFirstRow(int firstRow) {
this.firstRow = firstRow;
return this;
}
public int getMaxRows() {
return maxRows;
}
public DefaultRelationalQuery setMaxRows(int maxRows) {
this.maxRows = maxRows;
return this;
}
public int getTimeout() {
return timeout;
}
public DefaultRelationalQuery setTimeout(int secs) {
this.timeout = secs;
return this;
}
public BindParams getBindParams() {
return bindParams;
}
public DefaultRelationalQuery setBufferFetchSizeHint(int bufferFetchSizeHint) {
this.bufferFetchSizeHint = bufferFetchSizeHint;
return this;
}
public int getBufferFetchSizeHint() {
return bufferFetchSizeHint;
}
public String getQuery() {
return query;
}
}
@@ -0,0 +1,48 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.ExpressionList;
import io.ebean.UpdateQuery;
/**
* Default implementation of UpdateQuery.
*/
public class DefaultUpdateQuery<T> implements UpdateQuery<T> {
private final OrmUpdateProperties values = new OrmUpdateProperties();
private final DefaultOrmQuery<T> query;
public DefaultUpdateQuery(DefaultOrmQuery<T> query) {
this.query = query;
query.setUpdateProperties(values);
}
@Override
public UpdateQuery<T> set(String property, Object value) {
values.set(property, value);
return this;
}
@Override
public UpdateQuery<T> setNull(String property) {
values.set(property, null);
return this;
}
@Override
public UpdateQuery<T> setRaw(String propertyExpression) {
values.setRaw(propertyExpression);
return this;
}
@Override
public UpdateQuery<T> setRaw(String propertyExpression, Object... vals) {
values.setRaw(propertyExpression, vals);
return this;
}
@Override
public ExpressionList<T> where() {
return query.where();
}
}
@@ -0,0 +1,22 @@
package io.ebeaninternal.server.querydefn;
public class NaturalKeyBindParam {
private final String name;
private final Object value;
public NaturalKeyBindParam(String name, Object value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
}
@@ -0,0 +1,45 @@
package io.ebeaninternal.server.querydefn;
import io.ebeaninternal.api.SpiNamedParam;
import javax.persistence.PersistenceException;
/**
* Named parameter used as placeholder in expressions created by EQL language parsing.
*/
class ONamedParam implements SpiNamedParam {
private final String name;
private Object value;
/**
* Create with the given name.
*/
ONamedParam(String name) {
this.name = name;
}
/**
* Set the bind value for this named parameter.
*/
public void setValue(Object value) {
this.value = value;
}
/**
* Return the bind value for this named parameter.
*/
public Object getValue() {
return value;
}
/**
* Check the bind value has been set (so does not support null value).
*/
void checkValueSet() {
if (value == null) {
throw new PersistenceException("Named parameter [" + name + "] has not had it's value set.");
}
}
}
@@ -0,0 +1,510 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.FetchConfig;
import io.ebeaninternal.api.HashQueryPlanBuilder;
import io.ebeaninternal.server.deploy.BeanDescriptor;
import io.ebeaninternal.server.deploy.BeanPropertyAssoc;
import io.ebeaninternal.server.el.ElPropertyDeploy;
import io.ebeaninternal.server.el.ElPropertyValue;
import io.ebeaninternal.server.query.SplitName;
import javax.persistence.PersistenceException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Represents the internal structure of an Object Relational query.
* <p>
* Holds the select() and join() details of a ORM query.
* </p>
* <p>
* It is worth noting that for AutoTune a "tuned fetch info" builds an instance of OrmQueryDetail.
* Tuning a query is a matter of replacing an instance of this class with one that has been tuned
* with select() and join() set.
* </p>
*/
public class OrmQueryDetail implements Serializable {
private static final long serialVersionUID = -2510486880141461807L;
/**
* Root level properties.
*/
private OrmQueryProperties baseProps = new OrmQueryProperties();
/**
* Contains the fetch/lazy/query joins and their properties.
*/
private LinkedHashMap<String, OrmQueryProperties> fetchPaths = new LinkedHashMap<>();
/**
* Return a deep copy of the OrmQueryDetail.
*/
public OrmQueryDetail copy() {
OrmQueryDetail copy = new OrmQueryDetail();
copy.baseProps = baseProps.copy();
for (Map.Entry<String, OrmQueryProperties> entry : fetchPaths.entrySet()) {
copy.fetchPaths.put(entry.getKey(), entry.getValue().copy());
}
return copy;
}
public int queryPlanHash() {
HashQueryPlanBuilder builder = new HashQueryPlanBuilder();
queryPlanHash(builder);
return builder.getPlanHash();
}
/**
* Calculate the hash for the query plan.
*/
public void queryPlanHash(HashQueryPlanBuilder builder) {
baseProps.queryPlanHash(builder);
if (fetchPaths != null) {
for (OrmQueryProperties p : fetchPaths.values()) {
p.queryPlanHash(builder);
}
}
}
/**
* Return true if the details are the same for query plan purposes.
*/
public boolean isSameByPlan(OrmQueryDetail otherDetail) {
if (!isSameByPlan(baseProps, otherDetail.baseProps)) {
return false;
}
if (fetchPaths == null) {
return otherDetail.fetchPaths == null;
}
if (fetchPaths.size() != otherDetail.fetchPaths.size()) {
return false;
}
// check with ordering being important
Iterator<Map.Entry<String, OrmQueryProperties>> thisIt = fetchPaths.entrySet().iterator();
Iterator<Map.Entry<String, OrmQueryProperties>> thatIt = otherDetail.fetchPaths.entrySet().iterator();
while (thisIt.hasNext() && thatIt.hasNext()) {
Map.Entry<String, OrmQueryProperties> thisEntry = thisIt.next();
Map.Entry<String, OrmQueryProperties> thatEntry = thatIt.next();
if (!thisEntry.getKey().equals(thatEntry.getKey())) {
return false;
}
if (!thisEntry.getValue().isSameByPlan(thatEntry.getValue())) {
return false;
}
}
return true;
}
/**
* Return true if equal in terms of autoTune (select and fetch without property ordering).
*/
public boolean isAutoTuneEqual(OrmQueryDetail otherDetail) {
if (!isSameByAutoTune(baseProps, otherDetail.baseProps)) {
return false;
}
if (fetchPaths == null) {
return otherDetail.fetchPaths == null;
}
if (fetchPaths.size() != otherDetail.fetchPaths.size()) {
return false;
}
// check without regard to ordering
for (Map.Entry<String, OrmQueryProperties> entry : fetchPaths.entrySet()) {
OrmQueryProperties chunk = otherDetail.getChunk(entry.getKey(), false);
if (!isSameByAutoTune(entry.getValue(), chunk)) {
return false;
}
}
return true;
}
private boolean isSameByAutoTune(OrmQueryProperties p1, OrmQueryProperties p2) {
return p1 == null ? p2 == null : p1.isSameByAutoTune(p2);
}
private boolean isSameByPlan(OrmQueryProperties p1, OrmQueryProperties p2) {
return p1 == null ? p2 == null : p1.isSameByPlan(p2);
}
public String toString() {
return asString();
}
/**
* Return the detail in string form.
*/
public String asString() {
StringBuilder sb = new StringBuilder();
if (!baseProps.isEmpty()) {
baseProps.append("select ", sb);
}
if (fetchPaths != null) {
for (OrmQueryProperties join : fetchPaths.values()) {
join.append(" fetch ", sb);
}
}
return sb.toString();
}
public int hashCode() {
throw new RuntimeException("should not use");
}
/**
* set the properties to include on the base / root entity.
*/
public void select(String columns) {
baseProps = new OrmQueryProperties(null, columns, null);
}
boolean containsProperty(String property) {
return baseProps.isIncluded(property);
}
/**
* Set the base / root query properties.
*/
public void setBase(OrmQueryProperties baseProps) {
this.baseProps = baseProps;
}
List<OrmQueryProperties> removeSecondaryQueries() {
return removeSecondaryQueries(false);
}
List<OrmQueryProperties> removeSecondaryLazyQueries() {
return removeSecondaryQueries(true);
}
private List<OrmQueryProperties> removeSecondaryQueries(boolean lazyQuery) {
ArrayList<String> matchingPaths = new ArrayList<>(2);
for (OrmQueryProperties chunk : fetchPaths.values()) {
boolean match = lazyQuery ? chunk.isLazyFetch() : chunk.isQueryFetch();
if (match) {
matchingPaths.add(chunk.getPath());
}
}
if (matchingPaths.isEmpty()) {
return null;
}
// sort into depth order to remove
Collections.sort(matchingPaths);
// the list of secondary queries
ArrayList<OrmQueryProperties> props = new ArrayList<>();
for (String path : matchingPaths) {
OrmQueryProperties secQuery = fetchPaths.remove(path);
if (secQuery != null) {
props.add(secQuery);
// remove any child properties for this path
Iterator<OrmQueryProperties> pass2It = fetchPaths.values().iterator();
while (pass2It.hasNext()) {
OrmQueryProperties pass2Prop = pass2It.next();
if (secQuery.isChild(pass2Prop)) {
// remove join to secondary query from the main query
// and add to this secondary query
pass2It.remove();
secQuery.add(pass2Prop);
}
}
}
}
// Add the secondary queries as select properties
// to the parent chunk to ensure the foreign keys
// are included in the query
for (OrmQueryProperties prop : props) {
String path = prop.getPath();
// split into parent and property
String[] split = SplitName.split(path);
// add property to parent chunk
OrmQueryProperties chunk = getChunk(split[0], true);
chunk.addSecondaryQueryJoin(split[1]);
}
return props;
}
boolean tuneFetchProperties(OrmQueryDetail tunedDetail) {
boolean tuned = false;
OrmQueryProperties tunedRoot = tunedDetail.getChunk(null, false);
if (tunedRoot != null) {
tuned = true;
baseProps = tunedRoot;
for (OrmQueryProperties tunedChunk : tunedDetail.fetchPaths.values()) {
fetch(tunedChunk.copy());
}
}
return tuned;
}
/**
* Add or replace the fetch detail.
*/
protected void fetch(OrmQueryProperties chunk) {
String path = chunk.getPath();
if (path == null) {
baseProps = chunk;
} else {
fetchPaths.put(path, chunk);
}
}
/**
* Remove all joins and properties.
* <p>
* Typically for the row count query.
* </p>
*/
public void clear() {
fetchPaths.clear();
}
/**
* Set the fetch properties and configuration for a given path.
*
* @param path the property to join
* @param partialProps the properties on the join property to include
*/
public void fetch(String path, String partialProps, FetchConfig fetchConfig) {
fetch(new OrmQueryProperties(path, partialProps, fetchConfig));
}
/**
* Add for raw sql etc when the properties are already parsed into a set.
*/
public void fetch(String path, LinkedHashSet<String> properties) {
fetch(new OrmQueryProperties(path, properties));
}
/**
* Sort the fetch paths into depth order adding any missing parent paths if necessary.
*/
public void sortFetchPaths(BeanDescriptor<?> d) {
if (!fetchPaths.isEmpty()) {
LinkedHashMap<String, OrmQueryProperties> sorted = new LinkedHashMap<>();
for (OrmQueryProperties p : fetchPaths.values()) {
sortFetchPaths(d, p, sorted);
}
fetchPaths = sorted;
}
}
private void sortFetchPaths(BeanDescriptor<?> d, OrmQueryProperties p, LinkedHashMap<String, OrmQueryProperties> sorted) {
String path = p.getPath();
if (!sorted.containsKey(path)) {
String parentPath = p.getParentPath();
if (parentPath == null || sorted.containsKey(parentPath)) {
// off root path or parent already ahead in fetch order
sorted.put(path, p);
} else {
OrmQueryProperties parentProp = fetchPaths.get(parentPath);
if (parentProp == null) {
ElPropertyValue el = d.getElGetValue(parentPath);
if (el == null) {
throw new PersistenceException("Path [" + parentPath + "] not valid from " + d.getFullName());
}
// add a missing parent path just fetching the Id property
BeanPropertyAssoc<?> assocOne = (BeanPropertyAssoc<?>) el.getBeanProperty();
parentProp = new OrmQueryProperties(parentPath, assocOne.getTargetIdProperty());
}
sortFetchPaths(d, parentProp, sorted);
sorted.put(path, p);
}
}
}
/**
* Mark 'fetch joins' to 'many' properties over to 'query joins' where needed.
*/
void markQueryJoins(BeanDescriptor<?> beanDescriptor, String lazyLoadManyPath, boolean allowOne) {
// the name of the many fetch property if there is one
String manyFetchProperty = null;
// flag that is set once the many fetch property is chosen
boolean fetchJoinFirstMany = allowOne;
sortFetchPaths(beanDescriptor);
for (String fetchPath : fetchPaths.keySet()) {
ElPropertyDeploy elProp = beanDescriptor.getElPropertyDeploy(fetchPath);
if (elProp == null) {
throw new PersistenceException("Invalid fetch path " + fetchPath + " from " + beanDescriptor.getFullName());
}
if (elProp.containsManySince(manyFetchProperty)) {
// this is a join to a *ToMany
OrmQueryProperties chunk = fetchPaths.get(fetchPath);
if (isQueryJoinCandidate(lazyLoadManyPath, chunk)) {
// this is a 'fetch join' (included in main query)
if (fetchJoinFirstMany) {
// letting the first one remain a 'fetch join'
fetchJoinFirstMany = false;
manyFetchProperty = fetchPath;
} else {
// convert this one over to a 'query join'
chunk.markForQueryJoin();
}
}
}
}
}
/**
* Return true if this path is a candidate for converting to a query join.
*/
private boolean isQueryJoinCandidate(String lazyLoadManyPath, OrmQueryProperties chunk) {
return chunk.isFetchJoin()
&& !isLazyLoadManyRoot(lazyLoadManyPath, chunk)
&& !hasParentSecJoin(lazyLoadManyPath, chunk);
}
/**
* Return true if this is actually the root level of a +query/+lazy loading query.
*/
private boolean isLazyLoadManyRoot(String lazyLoadManyPath, OrmQueryProperties chunk) {
return lazyLoadManyPath != null && lazyLoadManyPath.equals(chunk.getPath());
}
/**
* If the chunk has a parent that is a query or lazy join. In this case it does not need to be
* converted.
*/
private boolean hasParentSecJoin(String lazyLoadManyPath, OrmQueryProperties chunk) {
OrmQueryProperties parent = getParent(chunk);
if (parent == null) {
return false;
} else {
if (lazyLoadManyPath != null && lazyLoadManyPath.equals(parent.getPath())) {
return false;
} else {
return !parent.isFetchJoin() || hasParentSecJoin(lazyLoadManyPath, parent);
}
}
}
/**
* Return the parent chunk.
*/
private OrmQueryProperties getParent(OrmQueryProperties chunk) {
String parentPath = chunk.getParentPath();
return parentPath == null ? null : fetchPaths.get(parentPath);
}
/**
* Set any default select clauses for the main bean and any joins that have not explicitly defined
* a select clause.
* <p>
* That is this will use FetchType.LAZY to exclude some properties by default.
* </p>
*/
public void setDefaultSelectClause(BeanDescriptor<?> desc) {
if (desc.hasDefaultSelectClause() && !hasSelectClause()) {
baseProps = new OrmQueryProperties(null, desc.getDefaultSelectClause());
}
for (OrmQueryProperties joinProps : fetchPaths.values()) {
if (!joinProps.hasSelectClause()) {
BeanDescriptor<?> assocDesc = desc.getBeanDescriptor(joinProps.getPath());
if (assocDesc.hasDefaultSelectClause()) {
fetch(joinProps.getPath(), assocDesc.getDefaultSelectClause(), joinProps.getFetchConfig());
}
}
}
}
private boolean hasSelectClause() {
return baseProps.hasSelectClause();
}
/**
* Return true if the query detail has neither select properties specified or any joins defined.
*/
public boolean isEmpty() {
return fetchPaths.isEmpty() && baseProps.allProperties();
}
/**
* Return true if there are no joins.
*/
public boolean isJoinsEmpty() {
return fetchPaths.isEmpty();
}
/**
* Add the explicit bean join.
* <p>
* This is also used to Exclude the matching property from the parent select (aka remove the
* foreign key) because it is now included in it's on node in the SqlTree.
* </p>
*/
public void includeBeanJoin(String parentPath, String propertyName) {
OrmQueryProperties parentChunk = getChunk(parentPath, true);
parentChunk.includeBeanJoin(propertyName);
}
public OrmQueryProperties getChunk(String path, boolean create) {
if (path == null) {
return baseProps;
}
OrmQueryProperties props = fetchPaths.get(path);
if (create && props == null) {
props = new OrmQueryProperties(path);
fetch(props);
return props;
} else {
return props;
}
}
/**
* Return true if the fetch path is included.
*/
public boolean includesPath(String path) {
OrmQueryProperties chunk = fetchPaths.get(path);
// may not have fetch properties if just +cache etc
return chunk != null && !chunk.isCache();
}
/**
* Return the fetch paths for this detail.
*/
public Set<String> getFetchPaths() {
return fetchPaths.keySet();
}
/**
* Return the underlying fetch path entries.
*/
public Set<Map.Entry<String, OrmQueryProperties>> entries() {
return fetchPaths.entrySet();
}
}
@@ -0,0 +1,107 @@
package io.ebeaninternal.server.querydefn;
import javax.persistence.PersistenceException;
/**
* Parses a Object relational query statement into a OrmQueryDetail and OrmQueryAttributes.
* <p>
* The reason they are split into detail and attributes is that the AutoTune feature is used to
* replace the OrmQueryDetail leaving the attributes unchanged.
* </p>
*/
public class OrmQueryDetailParser {
private final OrmQueryDetail detail = new OrmQueryDetail();
private final SimpleTextParser parser;
public OrmQueryDetailParser(String oql) {
this.parser = new SimpleTextParser(oql);
}
public OrmQueryDetail parse() throws PersistenceException {
if (parser.isEmpty()) return detail;
parser.nextWord();
processInitial();
return detail;
}
private void processInitial() {
if (parser.isMatch("select")) {
readSelect();
} else if (parser.isMatch("find")) {
OrmQueryProperties props = readFindFetch();
detail.setBase(props);
} else {
process();
}
while (!parser.isFinished()) {
process();
}
}
private boolean isFetch() {
return parser.isMatch("fetch") || parser.isMatch("join");
}
private void process() {
if (isFetch()) {
detail.fetch(readFindFetch());
} else {
throw new PersistenceException("Query expected 'fetch', 'where','order by' or 'limit' keyword but got ["
+ parser.getWord() + "] \r " + parser.getOql());
}
}
private void readSelect() {
String props = parser.nextWord();
if (props.startsWith("(")) {
props = props.substring(1, props.length() - 1);
OrmQueryProperties base = new OrmQueryProperties(null, props);
detail.setBase(base);
parser.nextWord();
} else {
process();
}
}
private OrmQueryProperties readFindFetch() {
boolean readAlias = false;
String props = null;
String path = parser.nextWord();
String token;
while ((token = parser.nextWord()) != null) {
if (!readAlias && parser.isMatch("as")) {
// next token is alias
parser.nextWord();
readAlias = true;
} else if ('(' == token.charAt(0)) {
props = token;
parser.nextWord();
break;
} else if (isFindFetchEnd()) {
break;
} else if (!readAlias) {
readAlias = true;
} else {
throw new PersistenceException("Expected (props) or new 'fetch' 'where' but got " + token);
}
}
if (props != null) {
props = props.substring(1, props.length() - 1);
}
return new OrmQueryProperties(path, props);
}
private boolean isFindFetchEnd() {
return isFetch() || parser.isMatch("where") || parser.isMatch("order", "by");
}
}
@@ -0,0 +1,51 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.config.dbplatform.DatabasePlatform;
import io.ebean.config.dbplatform.SqlLimitRequest;
import io.ebeaninternal.api.SpiQuery;
public class OrmQueryLimitRequest implements SqlLimitRequest {
private final SpiQuery<?> ormQuery;
private final DatabasePlatform dbPlatform;
private final String sql;
private final String sqlOrderBy;
public OrmQueryLimitRequest(String sql, String sqlOrderBy, SpiQuery<?> ormQuery, DatabasePlatform dbPlatform) {
this.sql = sql;
this.sqlOrderBy = sqlOrderBy;
this.ormQuery = ormQuery;
this.dbPlatform = dbPlatform;
}
public String getDbOrderBy() {
return sqlOrderBy;
}
public String getDbSql() {
return sql;
}
public int getFirstRow() {
return ormQuery.getFirstRow();
}
public int getMaxRows() {
return ormQuery.getMaxRows();
}
public boolean isDistinct() {
return ormQuery.isDistinctQuery();
}
public SpiQuery<?> getOrmQuery() {
return ormQuery;
}
public DatabasePlatform getDbPlatform() {
return dbPlatform;
}
}
@@ -0,0 +1,133 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.OrderBy;
import io.ebean.RawSql;
import io.ebeaninternal.api.BindParams;
import io.ebeaninternal.api.CQueryPlanKey;
import io.ebeaninternal.api.HashQueryPlanBuilder;
import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.deploy.TableJoin;
/**
* Query plan key for ORM queries.
*/
class OrmQueryPlanKey implements CQueryPlanKey {
private final String m2mIncludeTable;
private final String orderByAsSting;
private final SpiExpression where;
private final SpiExpression having;
private final RawSql.Key rawSqlKey;
private final boolean hasIdValue;
private final SpiQuery.Type type;
private final int maxRows;
private final int firstRow;
private final boolean disableLazyLoading;
private final boolean distinct;
private final boolean sqlDistinct;
private final String mapKey;
private final SpiQuery.TemporalMode temporalMode;
private final boolean forUpdate;
private final String rootTableAlias;
private final OrmUpdateProperties updateProperties;
private final int planHash;
private final int bindCount;
OrmQueryPlanKey(TableJoin m2mIncludeTable, SpiQuery.Type type, OrmQueryDetail detail, int maxRows, int firstRow, boolean disableLazyLoading, OrderBy<?> orderBy, boolean distinct, boolean sqlDistinct, String mapKey, Object id, BindParams bindParams, SpiExpression whereExpressions, SpiExpression havingExpressions, SpiQuery.TemporalMode temporalMode, boolean forUpdate, String rootTableAlias, RawSql rawSql, OrmUpdateProperties updateProperties) {
this.m2mIncludeTable = m2mIncludeTable == null ? null : m2mIncludeTable.getTable();
this.type = type;
this.maxRows = maxRows;
this.firstRow = firstRow;
this.disableLazyLoading = disableLazyLoading;
this.orderByAsSting = (orderBy == null) ? null : orderBy.toStringFormat();
this.distinct = distinct;
this.sqlDistinct = sqlDistinct;
this.mapKey = mapKey;
this.hasIdValue = (id != null);
this.where = (whereExpressions == null) ? null : whereExpressions.copyForPlanKey();
this.having = (havingExpressions == null) ? null : havingExpressions.copyForPlanKey();
this.temporalMode = temporalMode;
this.forUpdate = forUpdate;
this.rootTableAlias = rootTableAlias;
this.updateProperties = updateProperties;
this.rawSqlKey = (rawSql == null) ? null : rawSql.getKey();
// exclude bind values and things unrelated to the sql being generated
HashQueryPlanBuilder builder = new HashQueryPlanBuilder();
builder.add((type == null ? 0 : type.ordinal() + 1));
builder.add(distinct).add(sqlDistinct);
builder.add(firstRow).add(maxRows);
builder.add(orderBy).add(forUpdate);
builder.add(mapKey);
builder.add(disableLazyLoading);
builder.add(hasIdValue);
builder.add(temporalMode);
builder.add(rawSqlKey == null ? 0 : rawSqlKey.hashCode());
builder.add(this.m2mIncludeTable);
builder.add(rootTableAlias);
if (detail != null) {
detail.queryPlanHash(builder);
}
if (bindParams != null) {
bindParams.buildQueryPlanHash(builder);
}
if (where != null) {
where.queryPlanHash(builder);
}
if (having != null) {
having.queryPlanHash(builder);
}
if (updateProperties != null) {
updateProperties.buildQueryPlanHash(builder);
}
this.planHash = builder.getPlanHash();
this.bindCount = builder.getBindCount();
}
@Override
public String getPartialKey() {
return planHash + "_" + bindCount;
}
@Override
public int hashCode() {
return planHash;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrmQueryPlanKey that = (OrmQueryPlanKey) o;
if (planHash != that.planHash) return false;
if (bindCount != that.bindCount) return false;
if (maxRows != that.maxRows) return false;
if (firstRow != that.firstRow) return false;
if (disableLazyLoading != that.disableLazyLoading) return false;
if (distinct != that.distinct) return false;
if (sqlDistinct != that.sqlDistinct) return false;
if (forUpdate != that.forUpdate) return false;
if (hasIdValue != that.hasIdValue) return false;
if (type != that.type) return false;
if (temporalMode != that.temporalMode) return false;
if (m2mIncludeTable != null ? !m2mIncludeTable.equals(that.m2mIncludeTable) : that.m2mIncludeTable != null)
return false;
if (orderByAsSting != null ? !orderByAsSting.equals(that.orderByAsSting) : that.orderByAsSting != null)
return false;
if (where != null ? !where.isSameByPlan(that.where) : that.where != null) return false;
if (having != null ? !having.isSameByPlan(that.having) : that.having != null) return false;
if (updateProperties != null ? !updateProperties.isSameByPlan(that.updateProperties) : that.updateProperties != null)
return false;
if (rawSqlKey != null ? !rawSqlKey.equals(that.rawSqlKey) : that.rawSqlKey != null) return false;
if (mapKey != null ? !mapKey.equals(that.mapKey) : that.mapKey != null) return false;
return rootTableAlias != null ? rootTableAlias.equals(that.rootTableAlias) : that.rootTableAlias == null;
}
}
@@ -0,0 +1,507 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.ExpressionFactory;
import io.ebean.FetchConfig;
import io.ebean.OrderBy;
import io.ebean.Query;
import io.ebeaninternal.api.HashQueryPlanBuilder;
import io.ebeaninternal.api.SpiExpression;
import io.ebeaninternal.api.SpiExpressionFactory;
import io.ebeaninternal.api.SpiExpressionList;
import io.ebeaninternal.api.SpiQuery;
import io.ebeaninternal.server.expression.FilterExprPath;
import io.ebeaninternal.server.expression.FilterExpressionList;
import io.ebeaninternal.server.expression.Same;
import io.ebeaninternal.server.query.SplitName;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Represents the Properties of an Object Relational query.
*/
public class OrmQueryProperties implements Serializable {
private static final long serialVersionUID = -8785582703966455658L;
static final FetchConfig DEFAULT_FETCH = new FetchConfig();
private final String parentPath;
private final String path;
private final String rawProperties;
private final String trimmedProperties;
private final LinkedHashSet<String> included;
private final FetchConfig fetchConfig;
/**
* Flag set when this fetch path needs to be a query join.
*/
private boolean markForQueryJoin;
private boolean cache;
private boolean readOnly;
/**
* Included bean joins.
*/
private Set<String> includedBeanJoin;
/**
* Add these properties to the select so that the foreign key columns are included in the query.
*/
private Set<String> secondaryQueryJoins;
private List<OrmQueryProperties> 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 for root so path (and parentPath) are null.
*/
public OrmQueryProperties() {
this((String) null);
}
/**
* Construct with a given path.
*/
public OrmQueryProperties(String path) {
this.path = path;
this.parentPath = SplitName.parent(path);
this.rawProperties = null;
this.trimmedProperties = null;
this.included = null;
this.fetchConfig = DEFAULT_FETCH;
}
public OrmQueryProperties(String path, String rawProperties) {
this(path, rawProperties, null);
}
public OrmQueryProperties(String path, String rawProperties, FetchConfig fetchConfig) {
OrmQueryPropertiesParser.Response response = OrmQueryPropertiesParser.parse(rawProperties);
this.path = path;
this.parentPath = SplitName.parent(path);
this.rawProperties = rawProperties;
this.trimmedProperties = response.properties;
this.included = response.included;
this.cache = response.cache;
this.readOnly = response.readOnly;
if (fetchConfig != null) {
this.fetchConfig = fetchConfig;
} else {
this.fetchConfig = response.fetchConfig;
}
}
public OrmQueryProperties(String path, LinkedHashSet<String> parsedProperties) {
if (parsedProperties == null) {
throw new IllegalArgumentException("parsedProperties is null");
}
this.path = path;
this.parentPath = SplitName.parent(path);
// for rawSql parsedProperties can be empty (when only fetching Id property)
this.included = parsedProperties;
this.rawProperties = join(parsedProperties);
this.trimmedProperties = rawProperties;
this.cache = false;
this.readOnly = false;
this.fetchConfig = DEFAULT_FETCH;
}
/**
* Join the set of properties into a comma delimited string.
*/
private String join(LinkedHashSet<String> parsedProperties) {
StringBuilder sb = new StringBuilder(50);
boolean first = true;
for (String property : parsedProperties) {
if (first) {
first = false;
} else {
sb.append(",");
}
sb.append(property);
}
return sb.toString();
}
/**
* Copy constructor.
*/
private OrmQueryProperties(OrmQueryProperties source) {
this.parentPath = source.parentPath;
this.path = source.path;
this.rawProperties = source.rawProperties;
this.trimmedProperties = source.trimmedProperties;
this.cache = source.cache;
this.readOnly = source.readOnly;
this.fetchConfig = source.fetchConfig;
this.filterMany = source.filterMany;
this.included = (source.included == null) ? null : new LinkedHashSet<>(source.included);
if (includedBeanJoin != null) {
this.includedBeanJoin = new HashSet<>(source.includedBeanJoin);
}
}
/**
* Creates a copy of the OrmQueryProperties.
*/
public OrmQueryProperties copy() {
return new OrmQueryProperties(this);
}
/**
* Move a OrderBy.Property from the main query to this query join.
*/
@SuppressWarnings("rawtypes")
void addSecJoinOrderProperty(OrderBy.Property orderProp) {
if (orderBy == null) {
orderBy = new OrderBy();
}
orderBy.add(orderProp);
}
public FetchConfig getFetchConfig() {
return fetchConfig;
}
/**
* Return the expressions used to filter on this path. This should be a many path to use this
* method.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public <T> SpiExpressionList<T> filterMany(Query<T> 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
markForQueryJoin = true;
}
return filterMany;
}
/**
* Return the filterMany expression list (can be null).
*/
private 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;
this.markForQueryJoin = true;
}
/**
* Define the select and joins for this query.
*/
@SuppressWarnings("unchecked")
public void configureBeanQuery(SpiQuery<?> query) {
if (trimmedProperties != null && !trimmedProperties.isEmpty()) {
query.select(trimmedProperties);
}
if (filterMany != null) {
SpiExpressionList<?> trimPath = filterMany.trimPath(path.length() + 1);
List<SpiExpression> underlyingList = trimPath.getUnderlyingList();
for (SpiExpression spiExpression : underlyingList) {
query.where().add(spiExpression);
}
}
if (secondaryChildren != null) {
int trimPath = path.length() + 1;
for (OrmQueryProperties p : secondaryChildren) {
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));
}
}
public boolean hasSelectClause() {
if ("*".equals(trimmedProperties)) {
// explicitly selected all properties
return true;
}
// explicitly selected some properties
return included != null || filterMany != null;
}
/**
* Return true if the properties and configuration are empty.
*/
public boolean isEmpty() {
return rawProperties == null || rawProperties.isEmpty();
}
public String toString() {
StringBuilder sb = new StringBuilder(40);
append("", sb);
return sb.toString();
}
public String append(String prefix, StringBuilder sb) {
sb.append(prefix);
if (path != null) {
sb.append(path).append(" ");
}
if (!isEmpty()) {
sb.append("(").append(rawProperties).append(") ");
}
return sb.toString();
}
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);
}
/**
* Return the raw properties.
*/
public String getProperties() {
return rawProperties;
}
/**
* Return true if this includes all properties on the path.
*/
public boolean allProperties() {
return included == null;
}
/**
* Return true if this property is included as a bean join.
* <p>
* If a property is included as a bean join then it should not be included as a reference/proxy to
* avoid duplication.
* </p>
*/
public boolean isIncludedBeanJoin(String propertyName) {
return includedBeanJoin != null && includedBeanJoin.contains(propertyName);
}
/**
* Add a bean join property.
*/
void includeBeanJoin(String propertyName) {
if (includedBeanJoin == null) {
includedBeanJoin = new HashSet<>();
}
includedBeanJoin.add(propertyName);
}
/**
* This excludes the bean joined properties.
* <p>
* This is because bean joins will have there own node in the SqlTree.
* </p>
*/
public Set<String> getSelectProperties() {
if (secondaryQueryJoins == null) {
return included;
}
LinkedHashSet<String> temp = new LinkedHashSet<>(2 * (secondaryQueryJoins.size() + included.size()));
temp.addAll(included);
temp.addAll(secondaryQueryJoins);
return temp;
}
void addSecondaryQueryJoin(String property) {
if (secondaryQueryJoins == null) {
secondaryQueryJoins = new HashSet<>(4);
}
secondaryQueryJoins.add(property);
}
/**
* Return the property set.
*/
public Set<String> getIncluded() {
return included;
}
boolean isIncluded(String propName) {
if (includedBeanJoin != null && includedBeanJoin.contains(propName)) {
return false;
}
// all properties included
return included == null || included.contains(propName);
}
/**
* Mark this path as needing to be a query join.
*/
void markForQueryJoin() {
markForQueryJoin = true;
}
/**
* Return true if this path is a 'query join'.
*/
public boolean isQueryFetch() {
return markForQueryJoin || getQueryFetchBatch() > -1;
}
/**
* Return true if this path is a 'fetch join'.
*/
boolean isFetchJoin() {
return !isQueryFetch() && !isLazyFetch();
}
/**
* Return true if this path is a lazy fetch.
*/
boolean isLazyFetch() {
return getLazyFetchBatch() > -1;
}
/**
* Return the batch size to use for the query join.
*/
public int getQueryFetchBatch() {
return fetchConfig.getQueryBatchSize();
}
/**
* Return true if a query join should eagerly fetch 'all' rather than the 'first'.
*/
public boolean isQueryFetchAll() {
return fetchConfig.isQueryAll();
}
/**
* Return the batch size to use for lazy loading.
*/
public int getLazyFetchBatch() {
return fetchConfig.getLazyBatchSize();
}
/**
* Return true if this path has the +readonly option.
*/
public boolean isReadOnly() {
return readOnly;
}
/**
* Return true if this path has the +cache option to hit the cache.
*/
public boolean isCache() {
return cache;
}
/**
* Return the parent path.
*/
String getParentPath() {
return parentPath;
}
/**
* Return the path relative to the root of the graph.
*/
public String getPath() {
return path;
}
/**
* Return true if the properties are the same for autoTune purposes.
*/
boolean isSameByAutoTune(OrmQueryProperties p2) {
if (included == null) {
return p2 == null || p2.included == null;
} else if (p2 == null) {
return false;
}
return included.equals(p2.included);
}
/**
* Properties are the same for query plan purposes.
*/
public boolean isSameByPlan(OrmQueryProperties p2) {
if (!Same.sameByValue(secondaryQueryJoins, p2.secondaryQueryJoins)) return false;
if (!Same.sameByValue(included, p2.included)) return false;
if (!Same.sameByNull(filterMany, p2.filterMany)) return false;
if (filterMany != null && !filterMany.isSameByPlan(p2.filterMany)) return false;
return fetchConfig.equals(p2.fetchConfig);
}
/**
* Calculate the query plan hash.
*/
public void queryPlanHash(HashQueryPlanBuilder builder) {
builder.add(path);
builder.addOrdered(included);
builder.add(secondaryQueryJoins);
builder.add(filterMany != null);
if (filterMany != null) {
filterMany.queryPlanHash(builder);
}
builder.add(fetchConfig.hashCode());
}
}
@@ -0,0 +1,185 @@
package io.ebeaninternal.server.querydefn;
import io.ebean.FetchConfig;
import io.ebeaninternal.server.lib.util.StringHelper;
import java.util.LinkedHashSet;
/**
* Parses the path properties string.
*/
class OrmQueryPropertiesParser {
private static Response EMPTY = new Response();
/**
* Immutable response of the parsed properties and options.
*/
static class Response {
final boolean readOnly;
final boolean cache;
final FetchConfig fetchConfig;
final String properties;
final LinkedHashSet<String> included;
Response(boolean readOnly, boolean cache, int queryFetchBatch, int lazyFetchBatch, String properties, LinkedHashSet<String> included) {
this.readOnly = readOnly;
this.cache = cache;
this.properties = properties;
this.included = included;
if (lazyFetchBatch > -1 || queryFetchBatch > -1) {
this.fetchConfig = new FetchConfig().lazy(lazyFetchBatch).query(queryFetchBatch);
} else {
this.fetchConfig = OrmQueryProperties.DEFAULT_FETCH;
}
}
Response() {
this.readOnly = false;
this.cache = false;
this.fetchConfig = OrmQueryProperties.DEFAULT_FETCH;
this.properties = "";
this.included = null;
}
}
/**
* Parses the path properties string returning the parsed properties and options.
* In general it is comma delimited with some special strings like +lazy(20).
*/
public static Response parse(String rawProperties) {
return new OrmQueryPropertiesParser(rawProperties).parse();
}
private String inputProperties;
private String outputProperties = "";
private boolean allProperties;
private boolean readOnly;
private boolean cache;
private int queryFetchBatch = -1;
private int lazyFetchBatch = -1;
private OrmQueryPropertiesParser(String inputProperties) {
this.inputProperties = inputProperties;
}
/**
* Parse the raw string properties input.
*/
private Response parse() {
if (inputProperties == null || inputProperties.isEmpty()) {
return EMPTY;
}
int pos = inputProperties.indexOf("+readonly");
if (pos > -1) {
inputProperties = StringHelper.replaceString(inputProperties, "+readonly", "");
readOnly = true;
}
pos = inputProperties.indexOf("+cache");
if (pos > -1) {
inputProperties = StringHelper.replaceString(inputProperties, "+cache", "");
cache = true;
}
pos = inputProperties.indexOf("+query");
if (pos > -1) {
queryFetchBatch = parseBatchHint(pos, "+query");
}
pos = inputProperties.indexOf("+lazy");
if (pos > -1) {
lazyFetchBatch = parseBatchHint(pos, "+lazy");
}
LinkedHashSet<String> included = parseIncluded();
String properties = (allProperties) ? "*" : outputProperties;
return new Response(readOnly, cache, queryFetchBatch, lazyFetchBatch, properties, included);
}
/**
* Parse the include separating by comma or semicolon.
*/
private LinkedHashSet<String> parseIncluded() {
inputProperties = inputProperties.trim();
if (inputProperties.isEmpty()) {
// default properties
return null;
}
if (inputProperties.equals("*")) {
// explicit all properties
allProperties = true;
return null;
}
String[] res = inputProperties.split(",");
StringBuilder sb = new StringBuilder(70);
LinkedHashSet<String> propertySet = new LinkedHashSet<>(res.length * 2);
int count = 0;
String temp;
for (String re : res) {
temp = re.trim();
if (!temp.isEmpty()) {
if (count > 0) {
sb.append(",");
}
sb.append(temp);
propertySet.add(temp);
count++;
}
}
if (propertySet.isEmpty()) {
// default properties
return null;
}
if (propertySet.contains("*")) {
// explicit all properties
allProperties = true;
return null;
}
// partial properties
outputProperties = sb.toString();
return propertySet;
}
private int parseBatchHint(int pos, String option) {
int startPos = pos + option.length();
int endPos = findEndPos(startPos, inputProperties);
if (endPos == -1) {
inputProperties = StringHelper.replaceString(inputProperties, option, "");
return 0;
} else {
String batchParam = inputProperties.substring(startPos + 1, endPos);
if (endPos + 1 >= inputProperties.length()) {
inputProperties = inputProperties.substring(0, pos);
} else {
inputProperties = inputProperties.substring(0, pos) + inputProperties.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) {
throw new RuntimeException("Error could not find ')' in " + props + " after position " + pos);
}
return endPara;
}
}
return -1;
}
}
@@ -0,0 +1,39 @@
package io.ebeaninternal.server.querydefn;
import io.ebeaninternal.api.SpiQuerySecondary;
import java.util.List;
/**
* The secondary query paths for 'query joins' and 'lazy loading'.
*/
class OrmQuerySecondary implements SpiQuerySecondary {
private final List<OrmQueryProperties> queryJoins;
private final List<OrmQueryProperties> lazyJoins;
/**
* Construct with the 'query join' and 'lazy join' path properties.
*/
OrmQuerySecondary(List<OrmQueryProperties> queryJoins, List<OrmQueryProperties> lazyJoins) {
this.queryJoins = queryJoins;
this.lazyJoins = lazyJoins;
}
/**
* Return a list of path/properties that are query join loaded.
*/
@Override
public List<OrmQueryProperties> getQueryJoins() {
return queryJoins;
}
/**
* Return the list of path/properties that are lazy loaded.
*/
@Override
public List<OrmQueryProperties> getLazyJoins() {
return lazyJoins;
}
}
@@ -0,0 +1,211 @@
package io.ebeaninternal.server.querydefn;
import io.ebeaninternal.api.HashQueryPlanBuilder;
import io.ebeaninternal.server.deploy.DeployParser;
import io.ebeaninternal.server.persist.Binder;
import io.ebeaninternal.server.type.DataBind;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Set properties for a UpdateQuery.
*/
public class OrmUpdateProperties {
private static final NullValue NULL_VALUE = new NullValue();
private static final NoneValue NONE_VALUE = new NoneValue();
/**
* Bind value used in the set clause for update query.
* It may/may not have bind values etc.
*/
public static abstract class Value {
public void bind(Binder binder, DataBind dataBind) throws SQLException {
// default to no bind values
}
public String bindClause() {
return "";
}
public int getBindCount() {
return 0;
}
}
/**
* Set property to null.
*/
private static class NullValue extends Value {
@Override
public String bindClause() {
return "=null";
}
}
/**
* Set property to a simple value.
*/
private static class SimpleValue extends Value {
final Object value;
SimpleValue(Object value) {
this.value = value;
}
@Override
public int getBindCount() {
return 1;
}
@Override
public String bindClause() {
return "=?";
}
@Override
public void bind(Binder binder, DataBind dataBind) throws SQLException {
binder.bindObject(dataBind, value);
dataBind.append(value).append(",");
}
}
/**
* Set using an expression with no bind value.
*/
private static class NoneValue extends Value {
@Override
public String bindClause() {
return "";
}
}
/**
* Set using an expression with many bind values.
*/
private static class RawArrayValue extends Value {
final Object[] bindValues;
RawArrayValue(Object[] bindValues) {
this.bindValues = bindValues;
}
@Override
public int getBindCount() {
return bindValues.length;
}
@Override
public void bind(Binder binder, DataBind dataBind) throws SQLException {
for (Object val : bindValues) {
binder.bindObject(dataBind, val);
dataBind.append(val).append(",");
}
}
}
/**
* The set properties/expressions and their bind values.
*/
private LinkedHashMap<String, Value> values = new LinkedHashMap<>();
/**
* Normal set property.
*/
public void set(String propertyName, Object value) {
if (value == null) {
values.put(propertyName, NULL_VALUE);
} else {
values.put(propertyName, new SimpleValue(value));
}
}
/**
* Set a raw expression with no bind values.
*/
public void setRaw(String propertyName) {
values.put(propertyName, NONE_VALUE);
}
/**
* Set a raw expression with many bind values.
*/
void setRaw(String propertyExpression, Object... vals) {
if (vals.length == 0) {
setRaw(propertyExpression);
} else {
values.put(propertyExpression, new RawArrayValue(vals));
}
}
/**
* Return true if this update has the same logical set clause.
*/
public boolean isSameByPlan(OrmUpdateProperties that) {
return that.values.size() == values.size()
&& logicalSetClause().equals(that.logicalSetClause());
}
/**
* Build the hash for the query plan caching.
*/
void buildQueryPlanHash(HashQueryPlanBuilder builder) {
builder.add(OrmUpdateProperties.class);
Set<Map.Entry<String, Value>> entries = values.entrySet();
for (Map.Entry<String, Value> entry : entries) {
builder.add(entry.getKey());
builder.bind(entry.getValue().getBindCount());
}
}
/**
* Bind all the bind values for the update set clause.
*/
public void bind(Binder binder, DataBind dataBind) throws SQLException {
for (Value bindValue : values.values()) {
bindValue.bind(binder, dataBind);
}
}
/**
* Build the actual set clause converting logical property names to db columns etc.
*/
public String buildSetClause(DeployParser deployParser) {
int setCount = 0;
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Value> entry : values.entrySet()) {
String property = entry.getKey();
if (setCount++ > 0) {
sb.append(", ");
}
// translate to db columns and remove table alias placeholders
sb.append(deployParser.parse(property).replace("${}", ""));
sb.append(entry.getValue().bindClause());
}
return sb.toString();
}
/**
* Return a logical set clause to use for isSameByPlan() use.
*/
private String logicalSetClause() {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, Value> entry : values.entrySet()) {
sb.append(", ");
sb.append(entry.getKey()).append(entry.getValue().bindClause());
}
return sb.toString();
}
}
@@ -0,0 +1,169 @@
package io.ebeaninternal.server.querydefn;
public class SimpleTextParser {
private final String oql;
private final char[] chars;
private final int eof;
private int pos;
private String word;
private String lowerWord;
public SimpleTextParser(String oql) {
this.oql = oql;
this.chars = oql.toCharArray();
this.eof = oql.length();
}
public boolean isEmpty() {
return oql.isEmpty();
}
public int getPos() {
return pos;
}
String getOql() {
return oql;
}
public String getWord() {
return word;
}
private String peekNextWord() {
int origPos = pos;
String nw = nextWordInternal();
pos = origPos;
return nw;
}
/**
* Match the current and the next word.
*/
public boolean isMatch(String lowerMatch, String nextWordMatch) {
if (isMatch(lowerMatch)) {
String nw = peekNextWord();
if (nw != null) {
nw = nw.toLowerCase();
return nw.equals(nextWordMatch);
}
}
return false;
}
public boolean isFinished() {
return word == null;
}
public int findWordLower(String lowerMatch, int afterPos) {
this.pos = afterPos;
return findWordLower(lowerMatch);
}
public int findWordLower(String lowerMatch) {
do {
if (nextWord() == null) {
return -1;
}
if (lowerMatch.equals(lowerWord)) {
return pos - lowerWord.length();
}
} while (true);
}
/**
* Match the current word.
*/
public boolean isMatch(String lowerMatch) {
return lowerMatch.equals(lowerWord);
}
public String nextWord() {
word = nextWordInternal();
if (word != null) {
lowerWord = word.toLowerCase();
}
return word;
}
private String nextWordInternal() {
trimLeadingWhitespace();
if (pos >= eof) {
return null;
}
int start = pos;
if (chars[pos] == '(') {
moveToClose();
} else {
moveToEndOfWord();
}
return oql.substring(start, pos);
}
private void moveToClose() {
pos++;
int openParenthesisCount = 0;
for (; pos < eof; pos++) {
char c = chars[pos];
if (c == '(') {
// count nested parenthesis
openParenthesisCount++;
} else if (c == ')') {
if (openParenthesisCount > 0) {
// still in nested parenthesis
--openParenthesisCount;
} else {
// we have found the end
pos++;
return;
}
}
}
}
private void moveToEndOfWord() {
char c = chars[pos];
boolean isOperator = isOperator(c);
for (; pos < eof; pos++) {
c = chars[pos];
if (isWordTerminator(c, isOperator)) {
return;
}
}
}
private boolean isWordTerminator(char c, boolean isOperator) {
return Character.isWhitespace(c) || (isOperator(c) ? !isOperator : c == '(' || isOperator);
}
private boolean isOperator(char c) {
switch (c) {
case '<':
return true;
case '>':
return true;
case '=':
return true;
case '!':
return true;
default:
return false;
}
}
private void trimLeadingWhitespace() {
for (; pos < eof; pos++) {
char c = chars[pos];
if (!Character.isWhitespace(c)) {
break;
}
}
}
}
@@ -0,0 +1 @@
package io.ebeaninternal.server.querydefn;