blob: 97db506f1232993a26926769644b2ef533ac18d9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.openjpa.kernel.jpql;
import java.io.PrintStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.security.AccessController;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import org.apache.openjpa.conf.Compatibility;
import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.kernel.BrokerFactory;
import org.apache.openjpa.kernel.ExpressionStoreQuery;
import org.apache.openjpa.kernel.FillStrategy;
import org.apache.openjpa.kernel.QueryContext;
import org.apache.openjpa.kernel.QueryOperations;
import org.apache.openjpa.kernel.ResultShape;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.kernel.exps.AbstractExpressionBuilder;
import org.apache.openjpa.kernel.exps.Context;
import org.apache.openjpa.kernel.exps.Expression;
import org.apache.openjpa.kernel.exps.ExpressionFactory;
import org.apache.openjpa.kernel.exps.Literal;
import org.apache.openjpa.kernel.exps.Parameter;
import org.apache.openjpa.kernel.exps.Path;
import org.apache.openjpa.kernel.exps.QueryExpressions;
import org.apache.openjpa.kernel.exps.Resolver;
import org.apache.openjpa.kernel.exps.Subquery;
import org.apache.openjpa.kernel.exps.Value;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Localizer.Message;
import org.apache.openjpa.lib.util.OrderedMap;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.MetaDataRepository;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.UserException;
/**
* Builder for JPQL expressions. This class takes the query parsed
* in {@link JPQL} and converts it to an expression tree using
* an {@link ExpressionFactory}. Public for unit testing purposes.
*
* @author Marc Prud'hommeaux
* @author Patrick Linskey
*/
public class JPQLExpressionBuilder
extends AbstractExpressionBuilder
implements JPQLTreeConstants {
private static final int VAR_PATH = 1;
private static final int VAR_ERROR = 2;
private static final Localizer _loc = Localizer.forPackage
(JPQLExpressionBuilder.class);
private final Stack<Context> contexts = new Stack<>();
private OrderedMap<Object, Class<?>> parameterTypes;
private int aliasCount = 0;
private boolean inAssignSubselectProjection = false;
private boolean hasParameterizedInExpression = false;
/**
* Constructor.
*
* @param factory the expression factory to use
* @param query used to resolve variables, parameters,
* and class names used in the query
* @param parsedQuery the parsed query
*/
public JPQLExpressionBuilder(ExpressionFactory factory,
ExpressionStoreQuery query, Object parsedQuery) {
super(factory, query.getResolver());
contexts.push(new Context(parsedQuery instanceof ParsedJPQL
? (ParsedJPQL) parsedQuery
: parsedQuery instanceof String
? getParsedQuery((String) parsedQuery)
: null, null, null));
if (ctx().parsed == null)
throw new InternalException(parsedQuery + "");
}
@Override
protected Localizer getLocalizer() {
return _loc;
}
@Override
protected ClassLoader getClassLoader() {
// we don't resolve in the context of anything but ourselves
return getClass().getClassLoader();
}
protected ParsedJPQL getParsedQuery() {
return ctx().parsed;
}
protected ParsedJPQL getParsedQuery(String jpql) {
return new ParsedJPQL(jpql);
}
private void setCandidate(ClassMetaData cmd, String schemaAlias) {
addAccessPath(cmd);
if (cmd != null)
ctx().meta = cmd;
if (schemaAlias != null)
ctx().schemaAlias = schemaAlias;
}
private String nextAlias() {
return "jpqlalias" + (++aliasCount);
}
protected ClassMetaData resolveClassMetaData(JPQLNode node) {
// handle looking up alias names
String schemaName = assertSchemaName(node);
ClassMetaData cmd = getClassMetaData(schemaName, false);
if (cmd != null)
return cmd;
// we might be referencing a collection field of a subquery's parent
if (isPath(node)) {
Path path = getPath(node);
FieldMetaData fmd = path.last();
cmd = getFieldType(fmd);
if (cmd == null && fmd.isElementCollection())
cmd = fmd.getDefiningMetaData();
return cmd;
}
// now run again to throw the correct exception
return getClassMetaData(schemaName, true);
}
private ClassMetaData getClassMetaData(String alias, boolean assertValid) {
ClassLoader loader = getClassLoader();
MetaDataRepository repos = resolver.getConfiguration().
getMetaDataRepositoryInstance();
// first check for the alias
ClassMetaData cmd = repos.getMetaData(alias, loader, false);
if (cmd != null)
return cmd;
// now check for the class name; this is not technically permitted
// by the JPA spec, but is required in order to be able to execute
// JPQL queries from other facades (like JDO) that do not have
// the concept of entity names or aliases
Class<?> c = resolver.classForName(alias, null);
if (c != null)
cmd = repos.getMetaData(c, loader, assertValid);
else if (assertValid)
cmd = repos.getMetaData(alias, loader, false);
if (cmd == null && assertValid) {
String close = repos.getClosestAliasName(alias);
if (close != null)
throw parseException(EX_USER, "not-schema-name-hint",
new Object[]{ alias, close, repos.getAliasNames() }, null);
else
throw parseException(EX_USER, "not-schema-name",
new Object[]{ alias, repos.getAliasNames() }, null);
}
return cmd;
}
private Class<?> getCandidateType() {
return getCandidateMetaData().getDescribedType();
}
private ClassMetaData getCandidateMetaData() {
if (ctx().meta != null)
return ctx().meta;
ClassMetaData cls = getCandidateMetaData(root());
if (cls == null)
throw parseException(EX_USER, "not-schema-name",
new Object[]{ root() }, null);
setCandidate(cls, null);
return cls;
}
protected ClassMetaData getCandidateMetaData(JPQLNode node) {
// examing the node to find the candidate query
// ### this should actually be the primary SELECT instance
// resolved against the from variable declarations
JPQLNode from = node.findChildByID(JJTFROMITEM, true);
if (from == null) {
// OPENJPA-15 allow subquery without a FROMITEM
if (node.id == JJTSUBSELECT) {
from = node.findChildByID(JJTFROM, true);
}
else {
throw parseException(EX_USER, "no-from-clause", null, null);
}
}
for (int i = 0; i < from.children.length; i++) {
JPQLNode n = from.children[i];
if (n.id == JJTABSTRACTSCHEMANAME) {
// we simply return the first abstract schema child
// as resolved into a class
ClassMetaData cmd = resolveClassMetaData(n);
if (cmd != null)
return cmd;
// not a schema: treat it as a class
String cls = assertSchemaName(n);
if (cls == null)
throw parseException(EX_USER, "not-schema-name",
new Object[]{ root() }, null);
return getClassMetaData(cls, true);
}
// OPENJPA-15 support subquery's from clause do not start with
// identification_variable_declaration()
if (node.id == JJTSUBSELECT) {
if (n.id == JJTINNERJOIN) {
n = n.getChild(0);
}
if (n.id == JJTPATH) {
Path path = getPath(n);
FieldMetaData fmd = path.last();
ClassMetaData cmd = getFieldType(fmd);
if (cmd == null && fmd.isElementCollection())
cmd = fmd.getDefiningMetaData();
if (cmd != null) {
return cmd;
}
else {
throw parseException(EX_USER, "no-alias",
new Object[]{ n }, null);
}
}
}
}
return null;
}
@Override
protected String currentQuery() {
return ctx().parsed == null || root().parser == null ? null
: root().parser.jpql;
}
QueryExpressions getQueryExpressions() {
QueryExpressions exps = new QueryExpressions();
exps.setContexts(contexts);
evalQueryOperation(exps);
Expression filter = null;
Expression from = ctx().from;
if (from == null)
from = evalFromClause(root().id == JJTSELECT);
filter = and(from, filter);
filter = and(evalWhereClause(), filter);
filter = and(evalSelectClause(exps), filter);
exps.filter = filter == null ? factory.emptyExpression() : filter;
evalGroupingClause(exps);
evalHavingClause(exps);
evalFetchJoins(exps);
evalSetClause(exps);
evalOrderingClauses(exps);
if (parameterTypes != null)
exps.parameterTypes = parameterTypes;
exps.accessPath = getAccessPath();
exps.hasInExpression = this.hasParameterizedInExpression;
// verify parameters are consistent.
validateParameters();
return exps;
}
private Expression and(Expression e1, Expression e2) {
return e1 == null ? e2 : e2 == null ? e1 : factory.and(e1, e2);
}
private static String assemble(JPQLNode node) {
return assemble(node, ".", 0);
}
/**
* Assemble the children of the specific node by appending each
* child, separated by the delimiter.
*/
private static String assemble(JPQLNode node, String delimiter, int last) {
StringBuilder result = new StringBuilder();
JPQLNode[] parts = node.children;
for (int i = 0; parts != null && i < parts.length - last; i++)
result.append(result.length() > 0 ? delimiter : "").
append(parts[i].text);
return result.toString();
}
private Expression assignSubselectProjection(JPQLNode node,
QueryExpressions exps) {
inAssignSubselectProjection = true;
exps.projections = new Value[1];
exps.projectionClauses = new String[1];
exps.projectionAliases = new String[1];
Value val = getValue(node);
exps.projections[0] = val;
exps.projectionClauses[0] =
projectionClause(node.id == JJTSCALAREXPRESSION ?
firstChild(node) : node);
inAssignSubselectProjection = false;
return null;
}
/**
* Assign projections for NEW contructor in selection list.
* Example: SELECT NEW Person(p.name) FROM Person p WHERE ...
*/
private Expression assignProjections(JPQLNode parametersNode,
QueryExpressions exps, List<Value> projections,
List<String> projectionClauses, List<String> projectionAliases) {
int count = parametersNode.getChildCount();
Expression exp = null;
for (int i = 0; i < count; i++) {
JPQLNode parent = parametersNode.getChild(i);
JPQLNode node = firstChild(parent);
JPQLNode aliasNode = parent.children.length > 1 ? right(parent)
: null;
Value proj = getValue(node);
String alias = aliasNode != null ? aliasNode.text :
projectionClause(node.id == JJTSCALAREXPRESSION ?
firstChild(node) : node);
if (aliasNode != null)
proj.setAlias(alias);
projections.add(proj);
projectionClauses.add(alias);
projectionAliases.add(alias);
}
return exp;
}
private void evalProjectionsResultShape(JPQLNode selectionsNode,
QueryExpressions exps,
List<Value> projections,
List<String> projectionClauses,
List<String> projectionAliases) {
int count = selectionsNode.getChildCount();
Class<?> resultClass = null;
ResultShape<?> resultShape = null;
if (count > 1) {
// muti-selection
resultClass = Object[].class;
resultShape = new ResultShape(resultClass, new FillStrategy.Array<>(Object[].class));
}
for (int i = 0; i < count; i++) {
JPQLNode parent = selectionsNode.getChild(i);
JPQLNode node = firstChild(parent);
if (node.id == JJTCONSTRUCTOR) {
// build up the fully-qualified result class name by
// appending together the components of the children
String resultClassName = assemble(left(node));
Class<?> constructor = resolver.classForName(resultClassName, null);
if (constructor == null) {
// try resolve it again using simple name
int n = left(node).getChildCount();
String baseName = left(node).getChild(n-1).text;
constructor = resolver.classForName(baseName, null);
}
if (constructor == null && resolver.getConfiguration().getUseTCCLinSelectNew()) {
try {
if (System.getSecurityManager() != null) {
constructor = AccessController.doPrivileged(
J2DoPrivHelper.getForNameAction(resultClassName, false,
AccessController.doPrivileged(J2DoPrivHelper.getContextClassLoaderAction())));
}
else {
constructor = Thread.currentThread().getContextClassLoader().loadClass(resultClassName);
}
} catch (Exception e) {
// ignore
}
}
if (constructor == null)
throw parseException(EX_USER, "no-constructor",
new Object[]{ resultClassName }, null);
List<Value> terms = new ArrayList<>();
List<String> aliases = new ArrayList<>();
List<String> clauses = new ArrayList<>();
// now assign the arguments to the select clause as the projections
assignProjections(right(node), exps, terms, aliases, clauses);
FillStrategy fill = new FillStrategy.NewInstance(constructor);
ResultShape<?> cons = new ResultShape(constructor, fill);
for (Value val : terms) {
Class<?> type = val.getType();
cons.nest(new ResultShape(type, new FillStrategy.Assign(), type.isPrimitive()));
}
if (count == 1) {
resultClass = constructor;
resultShape = cons;
}
else
resultShape.nest(cons);
projections.addAll(terms);
projectionAliases.addAll(aliases);
projectionClauses.addAll(clauses);
} else {
JPQLNode aliasNode = parent.children.length > 1 ? right(parent)
: null;
Value proj = getValue(node);
String alias = aliasNode != null ? aliasNode.text :
projectionClause(node.id == JJTSCALAREXPRESSION ?
firstChild(node) : node);
if (aliasNode != null)
proj.setAlias(alias);
projections.add(proj);
projectionClauses.add(alias);
projectionAliases.add(alias);
Class<?> type = proj.getType();
ResultShape<?> projShape = new ResultShape(type, new FillStrategy.Assign(), type.isPrimitive());
if (count == 1)
resultShape = projShape;
else
resultShape.nest(projShape);
}
}
exps.shape = resultShape;
exps.resultClass = resultClass;
}
private String projectionClause(JPQLNode node) {
switch (node.id) {
case JJTTYPE:
return projectionClause(firstChild(node));
default:
return assemble(node);
}
}
private void evalQueryOperation(QueryExpressions exps) {
// determine whether we want to select, delete, or update
if (root().id == JJTSELECT || root().id == JJTSUBSELECT)
exps.operation = QueryOperations.OP_SELECT;
else if (root().id == JJTDELETE)
exps.operation = QueryOperations.OP_DELETE;
else if (root().id == JJTUPDATE)
exps.operation = QueryOperations.OP_UPDATE;
else
throw parseException(EX_UNSUPPORTED, "unrecognized-operation",
new Object[]{ root() }, null);
}
private void evalGroupingClause(QueryExpressions exps) {
// handle GROUP BY clauses
JPQLNode groupByNode = root().findChildByID(JJTGROUPBY, false);
if (groupByNode == null)
return;
int groupByCount = groupByNode.getChildCount();
exps.grouping = new Value[groupByCount];
for (int i = 0; i < groupByCount; i++) {
JPQLNode node = groupByNode.getChild(i);
Value val = getValue(node);
if (val instanceof Path) {
FieldMetaData fmd = ((Path) val).last();
if (fmd != null && fmd.getValue().getTypeMetaData() != null && fmd.getValue().isEmbedded())
throw parseException(EX_USER, "cant-groupby-embeddable",
new Object[]{ node.getChildCount() > 1 ? assemble(node) : node.text }, null);
}
exps.grouping[i] = val;
}
}
private void evalHavingClause(QueryExpressions exps) {
// handle HAVING clauses
JPQLNode havingNode = root().findChildByID(JJTHAVING, false);
if (havingNode == null)
return;
exps.having = getExpression(onlyChild(havingNode));
}
private void evalOrderingClauses(QueryExpressions exps) {
// handle ORDER BY clauses
JPQLNode orderby = root().findChildByID(JJTORDERBY, false);
if (orderby != null) {
int ordercount = orderby.getChildCount();
exps.ordering = new Value[ordercount];
exps.orderingClauses = new String[ordercount];
exps.orderingAliases = new String[ordercount];
exps.ascending = new boolean[ordercount];
for (int i = 0; i < ordercount; i++) {
JPQLNode node = orderby.getChild(i);
JPQLNode firstChild = firstChild(node);
exps.ordering[i] = getValue(firstChild);
exps.orderingClauses[i] = assemble(firstChild);
exps.orderingAliases[i] = firstChild.text;
// ommission of ASC/DESC token implies ascending
exps.ascending[i] = node.getChildCount() <= 1 ||
lastChild(node).id == JJTASCENDING ? true : false;
}
// check if order by select item result alias
for (int i = 0; i < ordercount; i++) {
if (exps.orderingClauses[i] != null &&
!exps.orderingClauses[i].equals(""))
continue;
for (int j = 0; j < exps.projections.length; j++) {
if (exps.projectionAliases[j].equalsIgnoreCase(
exps.orderingAliases[i])) {
exps.ordering[i] = exps.projections[j];
break;
}
}
}
}
}
private Expression evalSelectClause(QueryExpressions exps) {
if (exps.operation != QueryOperations.OP_SELECT)
return null;
JPQLNode selectNode = root();
JPQLNode selectClause = selectNode.
findChildByID(JJTSELECTCLAUSE, false);
if (selectClause != null && selectClause.hasChildID(JJTDISTINCT))
exps.distinct = QueryExpressions.DISTINCT_TRUE
| QueryExpressions.DISTINCT_AUTO;
else
exps.distinct = QueryExpressions.DISTINCT_FALSE;
// handle SELECT clauses
JPQLNode expNode = selectNode.
findChildByID(JJTSELECTEXPRESSIONS, true);
if (expNode == null) {
return null;
}
int selectCount = expNode.getChildCount();
JPQLNode selectChild = firstChild(expNode);
if (selectClause.parent.id == JJTSUBSELECT) {
exps.distinct &= ~QueryExpressions.DISTINCT_AUTO;
return assignSubselectProjection(onlyChild(selectChild), exps);
}
// if we are selecting just one thing and that thing is the
// schema's alias, then do not treat it as a projection
if (selectCount == 1 && selectChild != null &&
selectChild.getChildCount() == 1 &&
onlyChild(selectChild) != null) {
JPQLNode child = onlyChild(selectChild);
if (child.id == JJTSCALAREXPRESSION)
child = onlyChild(child);
if (assertSchemaAlias().equalsIgnoreCase(child.text)) {
return null;
}
}
// JPQL does not filter relational joins for projections
exps.distinct &= ~QueryExpressions.DISTINCT_AUTO;
exps.projections = new Value[selectCount];
List<Value> projections = new ArrayList<>();
List<String> aliases = new ArrayList<>();
List<String> clauses = new ArrayList<>();
evalProjectionsResultShape(expNode, exps, projections, aliases, clauses);
exps.projections = projections.toArray(new Value[projections.size()]);
exps.projectionAliases = aliases.toArray(new String[aliases.size()]);
exps.projectionClauses = clauses.toArray(new String[clauses.size()]);
return null;
}
private String assertSchemaAlias() {
String alias = ctx().schemaAlias;
if (alias == null)
throw parseException(EX_USER, "alias-required",
new Object[]{ ctx().meta }, null);
return alias;
}
protected Expression evalFetchJoins(QueryExpressions exps) {
Expression filter = null;
// handle JOIN FETCH
Set<String> joins = null;
Set<String> innerJoins = null;
JPQLNode[] outers = root().findChildrenByID(JJTOUTERFETCHJOIN);
for (int i = 0; outers != null && i < outers.length; i++)
(joins == null ? joins = new TreeSet<>() : joins).
add(getPath(onlyChild(outers[i])).last().getFullName(false));
JPQLNode[] inners = root().findChildrenByID(JJTINNERFETCHJOIN);
for (int i = 0; inners != null && i < inners.length; i++) {
String path = getPath(onlyChild(inners[i])).last()
.getFullName(false);
(joins == null ? joins = new TreeSet<>() : joins).add(path);
(innerJoins == null
? innerJoins = new TreeSet<>()
: innerJoins).add(path);
}
if (joins != null)
exps.fetchPaths = joins.
toArray(new String[joins.size()]);
if (innerJoins != null)
exps.fetchInnerPaths = innerJoins.
toArray(new String[innerJoins.size()]);
return filter;
}
protected void evalSetClause(QueryExpressions exps) {
// handle SET field = value
JPQLNode[] nodes = root().findChildrenByID(JJTUPDATEITEM);
for (int i = 0; nodes != null && i < nodes.length; i++) {
Path path = getPath(firstChild(nodes[i]));
if (path.last().getValue().getEmbeddedMetaData() != null)
throw parseException(EX_USER, "cant-bulk-update-embeddable",
new Object[]{assemble(firstChild(nodes[i]))}, null);
JPQLNode lastChild = lastChild(nodes[i]);
Value val = (lastChild.children == null)
? null : getValue(onlyChild(lastChild));
exps.putUpdate(path, val);
}
}
private Expression evalWhereClause() {
// evaluate the WHERE clause
JPQLNode whereNode = root().findChildByID(JJTWHERE, false);
if (whereNode == null)
return null;
return (Expression) eval(whereNode);
}
private Expression evalFromClause(boolean needsAlias) {
// build up the alias map in the FROM clause
JPQLNode from = root().findChildByID(JJTFROM, false);
if (from == null)
throw parseException(EX_USER, "no-from-clause", null, null);
return evalFromClause(from, needsAlias);
}
private Expression evalFromClause(JPQLNode from, boolean needsAlias) {
Expression exp = null;
for (int i = 0; i < from.children.length; i++) {
JPQLNode node = from.children[i];
if (node.id == JJTFROMITEM)
exp = evalFromItem(exp, node, needsAlias);
else if (node.id == JJTOUTERJOIN)
exp = addJoin(node, false, exp);
else if (node.id == JJTINNERJOIN)
exp = addJoin(node, true, exp);
else if (node.id == JJTINNERFETCHJOIN)
; // we handle inner fetch joins in the evalFetchJoins() method
else if (node.id == JJTOUTERFETCHJOIN)
; // we handle outer fetch joins in the evalFetchJoins() method
else
throw parseException(EX_USER, "not-schema-name",
new Object[]{ node }, null);
}
return exp;
}
private Expression getSubquery(String alias, Path path, Expression exp) {
Value var = getVariable(alias, true);
// this bind is for validateMapPath to resolve alias
Expression bindVar = factory.bindVariable(var, path);
FieldMetaData fmd = path.last();
ClassMetaData candidate = getFieldType(fmd);
if (candidate == null && fmd.isElementCollection())
candidate = fmd.getDefiningMetaData();
setCandidate(candidate, alias);
Context subContext = ctx();
Subquery subquery = ctx().getSubquery();
if (subquery == null){
subquery = factory.newSubquery(candidate, true, alias);
subContext.setSubquery(subquery);
}
else {
subquery.setSubqAlias(alias);
}
Path subpath = factory.newPath(subquery);
subpath.setSchemaAlias(path.getCorrelationVar());
subpath.setMetaData(candidate);
subquery.setMetaData(candidate);
if (fmd.isElementCollection())
exp = and(exp, bindVar);
else
exp = and(exp, factory.equal(path, subpath));
return exp;
}
/**
* Adds a join condition to the given expression.
*
* @param node the node to check
* @param inner whether or not the join should be an inner join
* @param exp an existing expression to AND, or null if none
* @return the Expression with the join condition added
*/
private Expression addJoin(JPQLNode node, boolean inner, Expression exp) {
// the type will be the declared type for the field
JPQLNode firstChild = firstChild(node);
Path path = null;
if (firstChild.id == JJTQUALIFIEDPATH)
path = getQualifiedPath(firstChild);
else
path = getPath(firstChild, false, inner);
JPQLNode alias = node.getChildCount() >= 2 ? right(node) : null;
// OPENJPA-15 support subquery's from clause do not start with
// identification_variable_declaration()
if (inner && ctx().getParent() != null && ctx().schemaAlias == null) {
return getSubquery(alias.text, path, exp);
}
return addJoin(path, alias, exp);
}
private Expression addJoin(Path path, JPQLNode aliasNode,
Expression exp) {
FieldMetaData fmd = path.last();
if (fmd == null)
throw parseException(EX_USER, "path-no-meta",
new Object[]{ path, null }, null);
String alias = aliasNode != null ? aliasNode.text : nextAlias();
Value var = getVariable(alias, true);
var.setMetaData(getFieldType(fmd));
Expression join = null;
// if the variable is already bound, get the var's value and
// do a regular contains with that
boolean bound = isBound(var);
if (bound) {
var = getValue(aliasNode, VAR_PATH);
} else {
bind(var);
join = and(join, factory.bindVariable(var, path));
}
if (!fmd.isTypePC()) // multi-valued relation
{
if (bound)
join = and(join, factory.contains(path, var));
setImplicitContainsTypes(path, var, CONTAINS_TYPE_ELEMENT);
}
return and(exp, join);
}
private Expression evalFromItem(Expression exp, JPQLNode node,
boolean needsAlias) {
ClassMetaData cmd = resolveClassMetaData(firstChild(node));
String alias = null;
if (node.getChildCount() < 2) {
if (needsAlias)
throw parseException(EX_USER, "alias-required",
new Object[]{ cmd }, null);
} else {
alias = right(node).text;
JPQLNode left = left(node);
addSchemaToContext(alias, cmd);
// check to see if the we are referring to a path in the from
// clause, since we might be in a subquery against a collection
if (isPath(left)) {
Path path = getPath(left);
return getSubquery(alias, path, exp);
} else {
// we have an alias: bind it as a variable
Value var = getVariable(alias, true);
var.setMetaData(cmd);
bind(var);
}
}
// ### we assign the first FROMITEM instance we see as
// the global candidate, which is incorrect: we should
// instead be mapping this to the SELECTITEM to see
// which is the desired candidate
if (ctx().schemaAlias == null)
setCandidate(cmd, alias);
else
addAccessPath(cmd);
return exp;
}
@Override
protected boolean isDeclaredVariable(String name) {
// JPQL doesn't support declaring variables
return false;
}
/**
* Check to see if the specific node is a path (vs. a schema name)
*/
boolean isPath(JPQLNode node) {
if (node.getChildCount() < 2)
return false;
final String name = firstChild(node).text;
if (name == null)
return false;
// handle the case where the class name is the alias
// for the candidate (we don't use variables for this)
if (getMetaDataForAlias(name) != null)
return true;
if (!isSeenVariable(name))
return false;
final Value var = getVariable(name, false);
if (var != null)
return isBound(var);
return false;
}
private static ClassMetaData getFieldType(FieldMetaData fmd) {
if (fmd == null)
return null;
ClassMetaData cmd = null;
ValueMetaData vmd;
if ((vmd = fmd.getElement()) != null)
cmd = vmd.getDeclaredTypeMetaData();
else if ((vmd = fmd.getKey()) != null)
cmd = vmd.getDeclaredTypeMetaData();
else if ((vmd = fmd.getValue()) != null)
cmd = vmd.getDeclaredTypeMetaData();
if (cmd == null || cmd.getDescribedType() == Object.class)
cmd = fmd.getDeclaredTypeMetaData();
return cmd;
}
/**
* Identification variables in JPQL are case insensitive, so lower-case
* all variables we are going to bind.
*/
@Override
protected Value getVariable(String id, boolean bind) {
if (id == null)
return null;
if (bind && getDefinedVariable(id) == null)
return createVariable(id, bind);
return super.getVariable(id.toLowerCase(), bind);
}
protected Value getDefinedVariable(String id) {
return ctx().getVariable(id);
}
@Override
protected boolean isSeenVariable(String var) {
Context c = ctx().findContext(var);
if (c != null)
return true;
return false;
}
/**
* Returns the class name using the children of the JPQLNode.
*/
private String assertSchemaName(JPQLNode node) {
if (node.id != JJTABSTRACTSCHEMANAME)
throw parseException(EX_USER, "not-identifer",
new Object[]{ node }, null);
return assemble(node);
}
private void checkEmbeddable(Value val) {
checkEmbeddable(val, currentQuery());
}
public static void checkEmbeddable(Value val, String currentQuery) {
Path path = val instanceof Path ? (Path) val : null;
if (path == null)
return;
FieldMetaData fmd = path.last();
if (fmd == null)
return;
ValueMetaData vm = fmd.isElementCollection() ? fmd.getElement()
: fmd.getValue();
if (vm.getEmbeddedMetaData() != null) {
//throw parseException(EX_USER, "bad-predicate",
// new Object[]{ currentQuery() }, null);
String argStr = _loc.get("bad-predicate",
new Object[] {fmd.getName()}).getMessage();
Message msg = _loc.get("parse-error", argStr, currentQuery);
throw new UserException(msg, null);
}
}
/**
* Recursive helper method to evaluate the given node.
*/
private Object eval(JPQLNode node) {
Value val1 = null;
Value val2 = null;
Value val3 = null;
boolean not = node.not;
switch (node.id) {
case JJTSCALAREXPRESSION:
return eval(onlyChild(node));
case JJTTYPE:
return getType(onlyChild(node));
case JJTTYPELITERAL:
return getTypeLiteral(node);
case JJTCLASSNAME:
return getPathOrConstant(node);
case JJTCASE:
return eval(onlyChild(node));
case JJTSIMPLECASE:
return getSimpleCaseExpression(node);
case JJTGENERALCASE:
return getGeneralCaseExpression(node);
case JJTWHEN:
return getWhenCondition(node);
case JJTWHENSCALAR:
return getWhenScalar(node);
case JJTCOALESCE:
return getCoalesceExpression(node);
case JJTNULLIF:
return getNullIfExpression(node);
case JJTWHERE: // top-level WHERE clause
return getExpression(onlyChild(node));
case JJTBOOLEANLITERAL:
return factory.newLiteral("true".equalsIgnoreCase
(node.text) ? Boolean.TRUE : Boolean.FALSE,
Literal.TYPE_BOOLEAN);
case JJTINTEGERLITERAL:
// use BigDecimal because it can handle parsing exponents
BigDecimal intlit = new BigDecimal
(node.text.endsWith("l") || node.text.endsWith("L")
? node.text.substring(0, node.text.length() - 1)
: node.text).
multiply(new BigDecimal(negative(node)));
return factory.newLiteral(intlit.longValue(),
Literal.TYPE_NUMBER);
case JJTDECIMALLITERAL:
BigDecimal declit = new BigDecimal
(node.text.endsWith("d") || node.text.endsWith("D") ||
node.text.endsWith("f") || node.text.endsWith("F")
? node.text.substring(0, node.text.length() - 1)
: node.text).
multiply(new BigDecimal(negative(node)));
return factory.newLiteral(declit, Literal.TYPE_NUMBER);
case JJTSTRINGLITERAL:
case JJTTRIMCHARACTER:
case JJTESCAPECHARACTER:
return factory.newLiteral(trimQuotes(node.text),
Literal.TYPE_SQ_STRING);
case JJTSTRINGLITERAL2:
return factory.newLiteral(trimDoubleQuotes(node.text),
Literal.TYPE_SQ_STRING);
case JJTPATTERNVALUE:
return eval(firstChild(node));
case JJTNAMEDINPUTPARAMETER:
return getParameter(onlyChild(node).text, false, false);
case JJTPOSITIONALINPUTPARAMETER:
return getParameter(node.text, true, false);
case JJTCOLLECTIONPARAMETER:
JPQLNode child = onlyChild(node);
boolean positional = child.id == JJTPOSITIONALINPUTPARAMETER;
if (!positional)
child = onlyChild(child);
return getParameter(child.text,
positional, true);
case JJTOR: // x OR y
return factory.or(getExpression(left(node)),
getExpression(right(node)));
case JJTAND: // x AND y
return and(getExpression(left(node)),
getExpression(right(node)));
case JJTEQUALS: // x = y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, null);
return factory.equal(val1, val2);
case JJTNOTEQUALS: // x <> y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, null);
return factory.notEqual(val1, val2);
case JJTLESSTHAN: // x < y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, null);
return factory.lessThan(val1, val2);
case JJTLESSOREQUAL: // x <= y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, null);
return factory.lessThanEqual(val1, val2);
case JJTGREATERTHAN: // x > y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, null);
return factory.greaterThan(val1, val2);
case JJTGREATEROREQUAL: // x >= y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, null);
return factory.greaterThanEqual(val1, val2);
case JJTADD: // x + y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, TYPE_NUMBER);
return factory.add(val1, val2);
case JJTSUBTRACT: // x - y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, TYPE_NUMBER);
return factory.subtract(val1, val2);
case JJTMULTIPLY: // x * y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, TYPE_NUMBER);
return factory.multiply(val1, val2);
case JJTDIVIDE: // x / y
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, TYPE_NUMBER);
return factory.divide(val1, val2);
case JJTBETWEEN: // x.field [NOT] BETWEEN 5 AND 10
val1 = getValue(child(node, 0, 3));
val2 = getValue(child(node, 1, 3));
val3 = getValue(child(node, 2, 3));
setImplicitTypes(val1, val2, null);
setImplicitTypes(val1, val3, null);
return evalNot(not, and(factory.greaterThanEqual(val1, val2),
factory.lessThanEqual(val1, val3)));
case JJTIN: // x.field [NOT] IN ('a', 'b', 'c')
// TYPE(x...) [NOT] IN (entityTypeLiteral1,...)
Expression inExp = null;
Iterator<JPQLNode> inIterator = node.iterator();
// the first child is the path
JPQLNode first = inIterator.next();
val1 = getValue(first);
while (inIterator.hasNext()) {
JPQLNode next = inIterator.next();
if (first.id == JJTTYPE && next.id == JJTTYPELITERAL)
val2 = getTypeLiteral(next);
else
val2 = getValue(next);
if (val2 instanceof Parameter) {
hasParameterizedInExpression = true;
}
// special case for <value> IN (<subquery>) or
// <value> IN (<single value>)
if (useContains(not, val1, val2, node))
return evalNot(not, factory.contains(val2, val1));
// this is currently a sequence of OR expressions, since we
// do not have support for IN expressions
setImplicitTypes(val1, val2, null);
if (isVerticalTypeInExpr(val1, node) && not) {
if (inExp == null)
inExp = factory.notEqual(val1, val2);
else
inExp = factory.and(inExp, factory.notEqual(val1, val2));
} else {
if (inExp == null)
inExp = factory.equal(val1, val2);
else
inExp = factory.or(inExp, factory.equal(val1, val2));
}
}
// we additionally need to add in a "NOT NULL" clause, since
// the IN behavior that is expected by the CTS also expects
// to filter our NULLs
if (isVerticalTypeInExpr(val1, node))
return inExp;
else
return and(evalNot(not, inExp),
factory.notEqual(val1, factory.getNull()));
case JJTISNULL: // x.field IS [NOT] NULL
val1 = getValue(onlyChild(node));
checkEmbeddable(val1);
if (not)
return factory.notEqual
(val1, factory.getNull());
else
return factory.equal
(val1, factory.getNull());
case JJTPATH:
return getPathOrConstant(node);
case JJTIDENTIFIER:
case JJTIDENTIFICATIONVARIABLE:
return getIdentifier(node);
case JJTQUALIFIEDPATH:
return getQualifiedPath(node);
case JJTQUALIFIEDIDENTIFIER:
// KEY(e), VALUE(e), ENTRY(e)
return getQualifiedIdentifier(node);
case JJTGENERALIDENTIFIER:
// KEY(e), VALUE(e)
if (node.parent.parent.id == JJTWHERE || node.parent.id == JJTGROUPBY)
return getGeneralIdentifier(onlyChild(node), true);
return getQualifiedIdentifier(onlyChild(node));
case JJTNOT:
return factory.not(getExpression(onlyChild(node)));
case JJTLIKE: // field LIKE '%someval%'
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitType(val1, TYPE_STRING);
setImplicitType(val2, TYPE_STRING);
// look for an escape character beneath the node
String escape = null;
JPQLNode escapeNode = right(node).
findChildByID(JJTESCAPECHARACTER, true);
if (escapeNode != null)
escape = trimQuotes(onlyChild(escapeNode).text);
if (not)
return factory.notMatches(val1, val2, "_", "%", escape);
else
return factory.matches(val1, val2, "_", "%", escape);
case JJTISEMPTY:
return evalNot(not,
factory.isEmpty(getValue(onlyChild(node))));
case JJTSIZE:
return factory.size(getValue(onlyChild(node)));
case JJTINDEX:
return factory.index(getValue(onlyChild(node)));
case JJTUPPER:
val1 = getValue(onlyChild(node));
setImplicitType(val1, TYPE_STRING);
return factory.toUpperCase(val1);
case JJTLOWER:
return factory.toLowerCase(getStringValue(onlyChild(node)));
case JJTLENGTH:
return factory.stringLength(getStringValue(onlyChild(node)));
case JJTABS:
return factory.abs(getNumberValue(onlyChild(node)));
case JJTSQRT:
return factory.sqrt(getNumberValue(onlyChild(node)));
case JJTMOD:
val1 = getValue(left(node));
val2 = getValue(right(node));
setImplicitTypes(val1, val2, TYPE_NUMBER);
return factory.mod(val1, val2);
case JJTTRIM: // TRIM([[where] [char] FROM] field)
val1 = getValue(lastChild(node));
setImplicitType(val1, TYPE_STRING);
Boolean trimWhere = null;
JPQLNode firstTrimChild = firstChild(node);
if (node.getChildCount() > 1) {
trimWhere =
firstTrimChild.id == JJTTRIMLEADING ? Boolean.TRUE
:
firstTrimChild.id == JJTTRIMTRAILING ? Boolean.FALSE
: null;
}
Value trimChar;
// if there are 3 children, then we know the trim
// char is the second node
if (node.getChildCount() == 3)
trimChar = getValue(secondChild(node));
// if there are two children, then we need to check to see
// if the first child is a leading/trailing/both node,
// or the trim character node
else if (node.getChildCount() == 2
&& firstTrimChild.id != JJTTRIMLEADING
&& firstTrimChild.id != JJTTRIMTRAILING
&& firstTrimChild.id != JJTTRIMBOTH)
trimChar = getValue(firstChild(node));
// othwerwise, we default to trimming the space character
else
trimChar = factory.newLiteral(" ", Literal.TYPE_STRING);
return factory.trim(val1, trimChar, trimWhere);
case JJTCONCAT:
if (node.children.length < 2)
throw parseException(EX_USER, "less-child-count",
new Object[]{2, node,
Arrays.asList(node.children) }, null);
val1 = getValue(firstChild(node));
val2 = getValue(secondChild(node));
setImplicitType(val1, TYPE_STRING);
setImplicitType(val2, TYPE_STRING);
Value concat = factory.concat(val1, val2);
for (int i = 2; i < node.children.length; i++) {
val2 = getValue(node.children[i]);
setImplicitType(val2, TYPE_STRING);
concat = factory.concat(concat, val2);
}
return concat;
case JJTSUBSTRING:
// Literals are forced to be Integers because PostgreSQL rejects Longs in SUBSTRING parameters.
// This however does not help if an expression like 1+1 is passed as parameter.
val1 = getValue(firstChild(node));
JPQLNode child2 = secondChild(node);
if (child2.id == JJTINTEGERLITERAL) {
val2 = getIntegerValue(child2);
} else {
val2 = getValue(child2);
}
if (node.getChildCount() == 3) {
JPQLNode child3 = thirdChild(node);
if (child3.id == JJTINTEGERLITERAL) {
val3 = getIntegerValue(child3);
} else {
val3 = getValue(child3);
}
}
setImplicitType(val1, TYPE_STRING);
setImplicitType(val2, Integer.TYPE);
if (node.children.length == 3)
setImplicitType(val3, Integer.TYPE);
return convertSubstringArguments(factory, val1, val2, val3);
case JJTLOCATE:
Value locatePath = getValue(firstChild(node));
Value locateSearch = getValue(secondChild(node));
Value locateFromIndex = null;
// Literals are forced to be Integers because PostgreSQL rejects Longs in POSITION parameters.
// This however does not help if an expression like 1+1 is passed as parameter.
if (node.getChildCount() > 2) { // optional start index arg
JPQLNode child3 = thirdChild(node);
if (child3.id == JJTINTEGERLITERAL) {
locateFromIndex = getIntegerValue(child3);
} else
locateFromIndex = getValue(child3);
}
setImplicitType(locatePath, TYPE_STRING);
setImplicitType(locateSearch, TYPE_STRING);
if (locateFromIndex != null)
setImplicitType(locateFromIndex, Integer.TYPE);
return factory.indexOf(locateSearch,
locateFromIndex == null ? locatePath
: factory.newArgumentList(locatePath, locateFromIndex));
case JJTAGGREGATE:
// simply pass-through while asserting a single child
return eval(onlyChild(node));
case JJTCOUNT:
JPQLNode c = lastChild(node);
if (c.id == JJTIDENTIFIER)
// count(e)
return factory.count(getPath(node, false, true));
return factory.count(getValue(c));
case JJTMAX:
return factory.max(getNumberValue(onlyChild(node)));
case JJTMIN:
return factory.min(getNumberValue(onlyChild(node)));
case JJTSUM:
return factory.sum(getNumberValue(onlyChild(node)));
case JJTAVERAGE:
return factory.avg(getNumberValue(onlyChild(node)));
case JJTDISTINCTPATH:
return factory.distinct(getValue(onlyChild(node)));
case JJTEXISTS:
return factory.isNotEmpty((Value) eval(onlyChild(node)));
case JJTANY:
return factory.any((Value) eval(onlyChild(node)));
case JJTALL:
return factory.all((Value) eval(onlyChild(node)));
case JJTSUBSELECT:
return getSubquery(node);
case JJTMEMBEROF:
val1 = getValue(left(node), VAR_PATH);
val2 = getValue(right(node), VAR_PATH);
checkEmbeddable(val2);
setImplicitContainsTypes(val2, val1, CONTAINS_TYPE_ELEMENT);
return evalNot(not, factory.contains(val2, val1));
case JJTCURRENTDATE:
return factory.getCurrentDate(Date.class);
case JJTCURRENTTIME:
return factory.getCurrentTime(Time.class);
case JJTCURRENTTIMESTAMP:
return factory.getCurrentTimestamp(Timestamp.class);
case JJTSELECTEXTENSION:
assertQueryExtensions("SELECT");
return eval(onlyChild(node));
case JJTGROUPBYEXTENSION:
assertQueryExtensions("GROUP BY");
return eval(onlyChild(node));
case JJTORDERBYEXTENSION:
assertQueryExtensions("ORDER BY");
return eval(onlyChild(node));
case JJTDATELITERAL:
return factory.newLiteral(node.text, Literal.TYPE_DATE);
case JJTTIMELITERAL:
return factory.newLiteral(node.text, Literal.TYPE_TIME);
case JJTTIMESTAMPLITERAL:
return factory.newLiteral(node.text, Literal.TYPE_TIMESTAMP);
default:
throw parseException(EX_FATAL, "bad-tree",
new Object[]{ node }, null);
}
}
private boolean useContains(boolean not, Value val1, Value val2, JPQLNode node) {
if (isVerticalTypeInExpr(val1, node) && not)
return false;
else
return (!(val2 instanceof Literal) && node.getChildCount() == 2);
}
private boolean isVerticalTypeInExpr(Value val, JPQLNode node) {
if (node.id != JJTIN)
return false;
return factory.isVerticalType(val);
}
private Value getIntegerValue(JPQLNode node) {
BigDecimal bigdec = new BigDecimal
(node.text.endsWith("l") || node.text.endsWith("L")
? node.text.substring(0, node.text.length() - 1)
: node.text).
multiply(new BigDecimal(negative(node)));
return factory.newLiteral(bigdec.intValue(),
Literal.TYPE_NUMBER);
}
/**
* Converts JPQL substring() function to OpenJPA ExpressionFactory
* substring() arguments.
*
* @param val1 the original String
* @param val2 the 1-based start index as per JPQL substring() semantics
* @param val3 the length of the returned string as per JPQL semantics
*
*/
public static Value convertSubstringArguments(ExpressionFactory factory,
Value val1, Value val2, Value val3) {
if (val3 != null)
return factory.substring(val1, factory.newArgumentList(val2, val3));
else
return factory.substring(val1, val2);
}
private void assertQueryExtensions(String clause) {
OpenJPAConfiguration conf = resolver.getConfiguration();
switch(conf.getCompatibilityInstance().getJPQL()) {
case Compatibility.JPQL_WARN:
// check if we've already warned for this query-factory combo
StoreContext ctx = resolver.getQueryContext().getStoreContext();
String query = currentQuery();
if (ctx.getBroker() != null && query != null) {
String key = getClass().getName() + ":" + query;
BrokerFactory factory = ctx.getBroker().getBrokerFactory();
Object hasWarned = factory.getUserObject(key);
if (hasWarned != null)
break;
else
factory.putUserObject(key, Boolean.TRUE);
}
Log log = conf.getLog(OpenJPAConfiguration.LOG_QUERY);
if (log.isWarnEnabled())
log.warn(_loc.get("query-extensions-warning", clause,
currentQuery()));
break;
case Compatibility.JPQL_STRICT:
throw new ParseException(_loc.get("query-extensions-error",
clause, currentQuery()).getMessage());
case Compatibility.JPQL_EXTENDED:
break;
default:
throw new IllegalStateException(
"Compatibility.getJPQL() == "
+ conf.getCompatibilityInstance().getJPQL());
}
}
@Override
public void setImplicitTypes(Value val1, Value val2,
Class<?> expected) {
String currQuery = currentQuery();
setImplicitTypes(val1, val2, expected, resolver, parameterTypes,
currQuery);
}
public static void setImplicitTypes(Value val1, Value val2,
Class<?> expected, Resolver resolver, OrderedMap<Object,Class<?>> parameterTypes,
String currentQuery) {
AbstractExpressionBuilder.setImplicitTypes(val1, val2, expected,
resolver);
// as well as setting the types for conversions, we also need to
// ensure that any parameters are declared with the correct type,
// since the JPA spec expects that these will be validated
Parameter param = val1 instanceof Parameter ? (Parameter) val1
: val2 instanceof Parameter ? (Parameter) val2 : null;
Path path = val1 instanceof Path ? (Path) val1
: val2 instanceof Path ? (Path) val2 : null;
// we only check for parameter-to-path comparisons
if (param == null || path == null || parameterTypes == null)
return;
FieldMetaData fmd = path.last();
if (fmd == null)
return;
if (expected == null)
checkEmbeddable(path, currentQuery);
Class<?> type = path.getType();
if (type == null)
return;
Object paramKey = param.getParameterKey();
if (paramKey == null)
return;
// make sure we have already declared the parameter
if (parameterTypes.containsKey(paramKey))
parameterTypes.put(paramKey, type);
}
private Value getStringValue(JPQLNode node) {
return getTypeValue(node, TYPE_STRING);
}
private Value getNumberValue(JPQLNode node) {
return getTypeValue(node, TYPE_NUMBER);
}
private Value getTypeValue(JPQLNode node, Class<?> implicitType) {
Value val = getValue(node);
setImplicitType(val, implicitType);
return val;
}
private Value getSubquery(JPQLNode node) {
final boolean subclasses = true;
// parse the subquery
ParsedJPQL parsed = new ParsedJPQL(node.parser.jpql, node);
Context subContext = new Context(parsed, null, ctx());
contexts.push(subContext);
ClassMetaData candidate = getCandidateMetaData(node);
Subquery subq = subContext.getSubquery();
if (subq == null) {
subq = factory.newSubquery(candidate, subclasses, nextAlias());
subContext.setSubquery(subq);
}
subq.setMetaData(candidate);
// evaluate from clause for resolving variables defined in subquery
JPQLNode from = node.getChild(1);
subContext.from = evalFromClause(from, true);
try {
QueryExpressions subexp = getQueryExpressions();
subq.setQueryExpressions(subexp);
if (subexp.projections.length > 0)
checkEmbeddable(subexp.projections[0]);
return subq;
} finally {
// remove the subquery parse context
contexts.pop();
}
}
/**
* Creates and records the names and order of parameters. The parameters are
* identified by a key with its type preserved. The second argument
* determines whether the first argument is used as-is or converted to
* an Integer as parameter key.
*
* @param id the text as it appears in the parsed node
* @param positional if true the first argument is converted to an integer
* @param isCollectionValued true for collection-valued parameters
*/
private Parameter getParameter(String id, boolean positional,
boolean isCollectionValued) {
if (parameterTypes == null)
parameterTypes = new OrderedMap<>();
Object paramKey = positional ? Integer.parseInt(id) : id;
if (!parameterTypes.containsKey(paramKey))
parameterTypes.put(paramKey, TYPE_OBJECT);
ClassMetaData meta = null;
int index;
if (positional) {
try {
// indexes in JPQL are 1-based, as opposed to 0-based in
// the core ExpressionFactory
index = Integer.parseInt(id) - 1;
} catch (NumberFormatException e) {
throw parseException(EX_USER, "bad-positional-parameter",
new Object[]{ id }, e);
}
if (index < 0)
throw parseException(EX_USER, "bad-positional-parameter",
new Object[]{ id }, null);
} else {
index = parameterTypes.indexOf(id);
}
Parameter param = isCollectionValued
? factory.newCollectionValuedParameter(paramKey, TYPE_OBJECT)
: factory.newParameter(paramKey, TYPE_OBJECT);
param.setMetaData(meta);
param.setIndex(index);
return param;
}
/**
* Checks to see if we should evaluate for a NOT expression.
*/
private Expression evalNot(boolean not, Expression exp) {
return not ? factory.not(exp) : exp;
}
/**
* Trim off leading and trailing single-quotes, and then
* replace any internal '' instances with ' (since repeating the
* quote is the JPQL mechanism of escaping a single quote).
*/
private String trimQuotes(String str) {
if (str == null || str.length() <= 1)
return str;
if (str.startsWith("'") && str.endsWith("'"))
str = str.substring(1, str.length() - 1);
int index = -1;
while ((index = str.indexOf("''", index + 1)) != -1)
str = str.substring(0, index + 1) + str.substring(index + 2);
return str;
}
/**
* Trim off leading and trailing double-quotes.
*/
private String trimDoubleQuotes(String str) {
if (str == null || str.length() <= 1)
return str;
if (str.startsWith("\"") && str.endsWith("\""))
str = str.substring(1, str.length() - 1);
return str;
}
/**
* An IntegerLiteral and DecimalLiteral node will
* have a child node of Negative if it is negative:
* if so, this method returns -1, else it returns 1.
*/
private short negative(JPQLNode node) {
if (node.children != null && node.children.length == 1
&& firstChild(node).id == JJTNEGATIVE)
return -1;
else
return 1;
}
private Value getIdentifier(JPQLNode node) {
final String name = node.text;
final Value val = getVariable(name, false);
ClassMetaData cmd = getMetaDataForAlias(name);
if (cmd != null) {
// handle the case where the class name is the alias
// for the candidate (we don't use variables for this)
Value thiz = null;
if (ctx().subquery == null ||
ctx().getSchema(name.toLowerCase()) == null) {
if (ctx().subquery != null && inAssignSubselectProjection)
thiz = factory.newPath(ctx().subquery);
else
thiz = factory.getThis();
} else {
thiz = factory.newPath(ctx().subquery);
}
((Path)thiz).setSchemaAlias(name);
thiz.setMetaData(cmd);
return thiz;
} else if (val instanceof Path) {
return val;
} else if (val instanceof Value) {
if (val.isVariable()) {
// can be an entity type literal
Class<?> c = resolver.classForName(name, null);
if (c != null) {
Value lit = factory.newTypeLiteral(c, Literal.TYPE_CLASS);
Class<?> candidate = getCandidateType();
ClassMetaData can = getClassMetaData(candidate.getName(),
false);
ClassMetaData meta = getClassMetaData(name, false);
if (candidate.isAssignableFrom(c))
lit.setMetaData(meta);
else
lit.setMetaData(can);
return lit;
}
}
return val;
}
throw parseException(EX_USER, "unknown-identifier",
new Object[]{ name }, null);
}
private Path validateMapPath(JPQLNode node, JPQLNode id) {
Path path = (Path) getValue(id);
FieldMetaData fld = path.last();
if (fld == null && ctx().subquery != null) {
Value var = getVariable(id.text, false);
if (var != null) {
path = factory.newPath(var);
fld = path.last();
}
}
if (fld != null) {
// validate the field is of type java.util.Map
if (fld.getDeclaredTypeCode() != JavaTypes.MAP) {
String oper = "VALUE";
if (node.id == JJTENTRY)
oper = "ENTRY";
else if (node.id == JJTKEY)
oper = "KEY";
throw parseException(EX_USER, "bad-qualified-identifier",
new Object[]{ id.text, oper}, null);
}
}
else
throw parseException(EX_USER, "unknown-type",
new Object[]{ id.text}, null);
return path;
}
private Value getGeneralIdentifier(JPQLNode node, boolean verifyEmbeddable) {
JPQLNode id = onlyChild(node);
Path path = validateMapPath(node, id);
if (node.id == JJTKEY)
path = (Path) factory.getKey(path);
FieldMetaData fld = path.last();
ClassMetaData meta = fld.getKey().getTypeMetaData();
if (verifyEmbeddable &&
(node.id == JJTKEY && meta != null && fld.getKey().isEmbedded()) ||
(node.id == JJTVALUE && fld.isElementCollection() &&
fld.getElement().getEmbeddedMetaData() != null)) {
// check basic type
if (node.parent.parent.id == JJTGROUPBY)
throw parseException(EX_USER, "cant-groupby-key-value-embeddable",
new Object[]{ node.id == JJTVALUE ? "VALUE" : "KEY", id.text }, null);
else
throw parseException(EX_USER, "bad-general-identifier",
new Object[]{ node.id == JJTVALUE ? "VALUE" : "KEY", id.text }, null);
}
return path;
}
private Value getQualifiedIdentifier(JPQLNode node) {
JPQLNode id = onlyChild(node);
Path path = validateMapPath(node, id);
if (node.id == JJTVALUE)
return path;
Value value = getValue(id);
if (node.id == JJTKEY)
return factory.mapKey(path, value);
else
return factory.mapEntry(path, value);
}
private Path getQualifiedPath(JPQLNode node) {
return getQualifiedPath(node, false, true);
}
private Path getQualifiedPath(JPQLNode node, boolean pcOnly, boolean inner)
{
int nChild = node.getChildCount();
JPQLNode firstChild = firstChild(node);
JPQLNode id = firstChild.id == JJTKEY ? onlyChild(firstChild) :
firstChild;
Path path = validateMapPath(firstChild, id);
if (firstChild.id == JJTIDENTIFIER)
return getPath(node);
FieldMetaData fld = path.last();
path = (Path) factory.getKey(path);
ClassMetaData meta = fld.getKey().getTypeMetaData();
if (meta == null)
throw parseException(EX_USER, "bad-qualified-path",
new Object[]{ id.text }, null);
path.setMetaData(meta);
// walk through the children and assemble the path
boolean allowNull = !inner;
for (int i = 1; i < nChild; i++) {
path = (Path) traversePath(path, node.children[i].text, pcOnly,
allowNull);
// all traversals but the first one will always be inner joins
allowNull = false;
}
return path;
}
private Value getTypeLiteral(JPQLNode node) {
JPQLNode type = onlyChild(node);
final String name = type.text;
final Value val = getVariable(name, false);
if (val instanceof Value && val.isVariable()) {
Class<?> c = resolver.classForName(name, null);
if (c != null) {
Value typeLit = factory.newTypeLiteral(c, Literal.TYPE_CLASS);
typeLit.setMetaData(getClassMetaData(name, false));
return typeLit;
}
}
throw parseException(EX_USER, "not-type-literal",
new Object[]{ name }, null);
}
private Value getPathOrConstant(JPQLNode node) {
// first check to see if the path is an enum or static field, and
// if so, load it
String className = assemble(node, ".", 1);
Class<?> c = resolver.classForName(className, null);
if (c != null) {
String fieldName = lastChild(node).text;
int type = (c.isEnum() ? Literal.TYPE_ENUM : Literal.TYPE_UNKNOWN);
try {
Field field = c.getField(fieldName);
Object value = field.get(null);
return factory.newLiteral(value, type);
} catch (NoSuchFieldException nsfe) {
if (node.inEnumPath)
throw parseException(EX_USER, "no-field",
new Object[]{ c.getName(), fieldName }, nsfe);
else
return getPath(node, false, true);
} catch (Exception e) {
throw parseException(EX_USER, "unaccessible-field",
new Object[]{ className, fieldName }, e);
}
} else {
return getPath(node, false, true);
}
}
/**
* Process type_discriminator
* type_discriminator ::=
* TYPE(general_identification_variable |
* single_valued_object_path_expression |
* input_parameter )
*/
private Value getType(JPQLNode node) {
switch (node.id) {
case JJTIDENTIFIER:
return factory.type(getValue(node));
case JJTNAMEDINPUTPARAMETER:
return factory.type(getParameter(node.text, false, false));
case JJTPOSITIONALINPUTPARAMETER:
return factory.type(getParameter(node.text, true, false));
case JJTGENERALIDENTIFIER:
return factory.type(getQualifiedIdentifier(onlyChild(node)));
default:
// TODO: enforce jpa2.0 spec rules.
// A single_valued_object_field is designated by the name of
// an association field in a one-to-one or many-to-one relationship
// or a field of embeddable class type.
// The type of a single_valued_object_field is the abstract schema
// type of the related entity or embeddable class
Value path = getPath(node, false, true);
return factory.type(path);
}
}
private Path getPath(JPQLNode node) {
return getPath(node, false, true);
}
private Path getPath(JPQLNode node, boolean pcOnly, boolean inner) {
// resolve the first element against the aliases map ...
// i.e., the path "SELECT x.id FROM SomeClass x where x.id > 10"
// will need to have "x" in the alias map in order to resolve
Path path = null;
final String name = firstChild(node).text;
final Value val = getVariable(name, false);
// handle the case where the class name is the alias
// for the candidate (we don't use variables for this)
if (name.equalsIgnoreCase(ctx().schemaAlias)) {
if (ctx().subquery != null) {
path = factory.newPath(ctx().subquery);
path.setMetaData(ctx().subquery.getMetaData());
} else {
path = factory.newPath();
path.setMetaData(ctx().meta);
}
} else if (getMetaDataForAlias(name) != null)
path = newPath(null, getMetaDataForAlias(name));
else if (val instanceof Path)
path = (Path) val;
else if (val.getMetaData() != null)
path = newPath(val, val.getMetaData());
else
throw parseException(EX_USER, "path-invalid",
new Object[]{ assemble(node), name }, null);
path.setSchemaAlias(name);
// walk through the children and assemble the path
boolean allowNull = !inner;
for (int i = 1; i < node.children.length; i++) {
if (path.isXPath()) {
for (int j = i; j <node.children.length; j++)
path = (Path) traverseXPath(path, node.children[j].text);
return path;
}
path = (Path) traversePath(path, node.children[i].text, pcOnly,
allowNull);
if (ctx().getParent() != null && ctx().getVariable(path.getSchemaAlias()) == null) {
path.setSubqueryContext(ctx(), name);
}
// all traversals but the first one will always be inner joins
allowNull = false;
}
return path;
}
@Override
protected Class<?> getDeclaredVariableType(String name) {
ClassMetaData cmd = getMetaDataForAlias(name);
if (cmd != null)
return cmd.getDescribedType();
if (name != null && name.equals(ctx().schemaAlias))
return getCandidateType();
// JPQL has no declared variables
return null;
}
/**
* Returns an Expression for the given node by eval'ing it.
*/
private Expression getExpression(JPQLNode node) {
Object exp = eval(node);
// check for boolean values used as expressions
if (!(exp instanceof Expression))
return factory.asExpression((Value) exp);
return (Expression) exp;
}
/**
* Returns a Simple Case Expression for the given node by eval'ing it.
*/
private Value getSimpleCaseExpression(JPQLNode node) {
Object caseOperand = eval(node.getChild(0));
int nChild = node.getChildCount();
Object val = eval(lastChild(node));
Object exp[] = new Expression[nChild - 2];
for (int i = 1; i < nChild - 1; i++)
exp[i-1] = eval(node.children[i]);
return factory.simpleCaseExpression((Value) caseOperand,
(Expression[]) exp, (Value) val);
}
/**
* Returns a General Case Expression for the given node by eval'ing it.
*/
private Value getGeneralCaseExpression(JPQLNode node) {
int nChild = node.getChildCount();
Object val = eval(lastChild(node));
Object exp[] = new Expression[nChild - 1];
for (int i = 0; i < nChild - 1; i++)
exp[i] = eval(node.children[i]);
return factory.generalCaseExpression((Expression[]) exp, (Value) val);
}
private Expression getWhenCondition(JPQLNode node) {
Object exp = eval(firstChild(node));
Object val = eval(secondChild(node));
return factory.whenCondition((Expression) exp, (Value) val);
}
private Expression getWhenScalar(JPQLNode node) {
Object val1 = eval(firstChild(node));
Object val2 = eval(secondChild(node));
return factory.whenScalar((Value) val1, (Value) val2);
}
private Value getCoalesceExpression(JPQLNode node) {
int nChild = node.getChildCount();
Object vals[] = new Value[nChild];
for (int i = 0; i < nChild; i++)
vals[i] = eval(node.children[i]);
return factory.coalesceExpression((Value[]) vals);
}
private Value getNullIfExpression(JPQLNode node) {
Object val1 = eval(firstChild(node));
Object val2 = eval(secondChild(node));
return factory.nullIfExpression((Value) val1, (Value) val2);
}
private Value getValue(JPQLNode node) {
if (node.id == JJTQUALIFIEDIDENTIFIER)
return getQualifiedIdentifier(onlyChild(node));
return getValue(node, VAR_PATH);
}
private Path newPath(Value val, ClassMetaData meta) {
Path path = val == null ? factory.newPath() : factory.newPath(val);
if (meta != null)
path.setMetaData(meta);
return path;
}
/**
* Returns a Value for the given node by eval'ing it.
*/
private Value getValue(JPQLNode node, int handleVar) {
Value val = (Value) eval(node);
// determined how to evaluate a variable
if (!val.isVariable())
return val;
else if (handleVar == VAR_PATH && !(val instanceof Path))
return newPath(val, val.getMetaData());
else if (handleVar == VAR_ERROR)
throw parseException(EX_USER, "unexpected-var",
new Object[]{ node.text }, null);
else
return val;
}
////////////////////////////
// Parse Context Management
////////////////////////////
private Context ctx() {
return contexts.peek();
}
private JPQLNode root() {
return ctx().parsed.root;
}
private ClassMetaData getMetaDataForAlias(String alias) {
for (int i = contexts.size() - 1; i >= 0; i--) {
Context context = contexts.get(i);
if (alias.equalsIgnoreCase(context.schemaAlias))
return context.meta;
}
return null;
}
@Override
protected void addSchemaToContext(String id, ClassMetaData meta) {
ctx().addSchema(id.toLowerCase(), meta);
}
@Override
protected void addVariableToContext(String id, Value var) {
ctx().addVariable(id, var);
}
@Override
protected Value getVariable(String var) {
Context c = ctx();
Value v = c.getVariable(var);
if (v != null)
return v;
if (c.getParent() != null)
return c.getParent().findVariable(var);
return null;
}
////////////////////////////
// Node traversal utilities
////////////////////////////
private JPQLNode onlyChild(JPQLNode node)
throws UserException {
JPQLNode child = firstChild(node);
if (node.children.length > 1)
throw parseException(EX_USER, "multi-children",
new Object[]{ node, Arrays.asList(node.children) }, null);
return child;
}
/**
* Returns the left node (the first of the children), and asserts
* that there are exactly two children.
*/
private JPQLNode left(JPQLNode node) {
return child(node, 0, 2);
}
/**
* Returns the right node (the second of the children), and asserts
* that there are exactly two children.
*/
private JPQLNode right(JPQLNode node) {
return child(node, 1, 2);
}
private JPQLNode child(JPQLNode node, int childNum, int assertCount) {
if (node.children.length != assertCount)
throw parseException(EX_USER, "wrong-child-count",
new Object[]{assertCount, node,
Arrays.asList(node.children) }, null);
return node.children[childNum];
}
private JPQLNode firstChild(JPQLNode node) {
if (node.children == null || node.children.length == 0)
throw parseException(EX_USER, "no-children",
new Object[]{ node }, null);
return node.children[0];
}
private static JPQLNode secondChild(JPQLNode node) {
return node.children[1];
}
private static JPQLNode thirdChild(JPQLNode node) {
return node.children[2];
}
private static JPQLNode lastChild(JPQLNode node) {
return lastChild(node, 0);
}
/**
* The Nth from the last child. E.g.,
* lastChild(1) will return the second-to-the-last child.
*/
private static JPQLNode lastChild(JPQLNode node, int fromLast) {
return node.children[node.children.length - (1 + fromLast)];
}
/**
* Base node that will be generated by the JPQLExpressionBuilder; base
* class of the {@link SimpleNode} that is used by {@link JPQL}.
*
* @author Marc Prud'hommeaux
* @see Node
* @see SimpleNode
*/
protected abstract static class JPQLNode
implements Node, Serializable {
private static final long serialVersionUID = 1L;
final int id;
final JPQL parser;
JPQLNode parent;
JPQLNode[] children;
String text;
boolean not = false;
boolean inEnumPath = false;
public JPQLNode(JPQL parser, int id) {
this.id = id;
this.parser = parser;
this.inEnumPath = parser.inEnumPath;
}
public void jjtOpen() {
}
public void jjtClose() {
}
JPQLNode[] findChildrenByID(int id) {
Collection<JPQLNode> set = new LinkedHashSet<>();
findChildrenByID(id, set);
return set.toArray(new JPQLNode[set.size()]);
}
private void findChildrenByID(int id, Collection<JPQLNode> set) {
for (int i = 0; children != null && i < children.length; i++) {
if (children[i].id == id)
set.add(children[i]);
children[i].findChildrenByID(id, set);
}
}
boolean hasChildID(int id) {
return findChildByID(id, false) != null;
}
JPQLNode findChildByID(int id, boolean recurse) {
for (int i = 0; children != null && i < children.length; i++) {
JPQLNode child = children[i];
if (child.id == id)
return children[i];
if (recurse) {
JPQLNode found = child.findChildByID(id, recurse);
if (found != null)
return found;
}
}
// not found
return null;
}
public void jjtSetParent(Node parent) {
this.parent = (JPQLNode) parent;
}
public Node jjtGetParent() {
return this.parent;
}
public void jjtAddChild(Node n, int i) {
if (children == null) {
children = new JPQLNode[i + 1];
} else if (i >= children.length) {
JPQLNode c[] = new JPQLNode[i + 1];
System.arraycopy(children, 0, c, 0, children.length);
children = c;
}
children[i] = (JPQLNode) n;
}
public Node jjtGetChild(int i) {
return children[i];
}
public int getChildCount() {
return jjtGetNumChildren();
}
public JPQLNode getChild(int index) {
return (JPQLNode) jjtGetChild(index);
}
public Iterator<JPQLNode> iterator() {
return Arrays.asList(children).iterator();
}
public int jjtGetNumChildren() {
return (children == null) ? 0 : children.length;
}
void setText(String text) {
this.text = text;
}
void setToken(Token t) {
setText(t.image);
}
@Override
public String toString() {
return JPQLTreeConstants.jjtNodeName[this.id];
}
public String toString(String prefix) {
return prefix + toString();
}
/**
* Debugging method.
*
* @see #dump(java.io.PrintStream,String)
*/
public void dump(String prefix) {
dump(System.out, prefix);
}
public void dump() {
dump(" ");
}
/**
* Debugging method to output a parse tree.
*
* @param out the stream to which to write the debugging info
* @param prefix the prefix to write out before lines
*/
public void dump(PrintStream out, String prefix) {
dump(out, prefix, false);
}
public void dump(PrintStream out, String prefix, boolean text) {
out.println(toString(prefix)
+ (text && this.text != null ? " [" + this.text + "]" : ""));
if (children != null) {
for (int i = 0; i < children.length; ++i) {
JPQLNode n = children[i];
if (n != null) {
n.dump(out, prefix + " ", text);
}
}
}
}
}
/**
* Public for unit testing purposes.
*/
public static class ParsedJPQL implements Serializable {
private static final long serialVersionUID = 1L;
// This is only ever used during parse; when ParsedJPQL instances
// are serialized, they will have already been parsed.
private final transient JPQLNode root;
private final String query;
// cache of candidate type data. This is stored here in case this
// parse tree is reused in a context that does not know what the
// candidate type is already.
private Class<?> _candidateType;
ParsedJPQL(String jpql) {
this(jpql, parse(jpql));
}
ParsedJPQL(String query, JPQLNode root) {
this.root = root;
this.query = query;
}
private static JPQLNode parse(String jpql) {
if (jpql == null)
jpql = "";
try {
return (JPQLNode) new JPQL(jpql).parseQuery();
} catch (Error e) {
// special handling for Error subclasses, which the
// parser may sometimes (unfortunately) throw
throw new UserException(_loc.get("parse-error",
new Object[]{ e.toString(), jpql }));
}
}
void populate(ExpressionStoreQuery query) {
QueryContext ctx = query.getContext();
// if the owning query's context does not have
// any candidate class, then set it here
if (ctx.getCandidateType() == null) {
if (_candidateType == null)
_candidateType = new JPQLExpressionBuilder
(null, query, this).getCandidateType();
ctx.setCandidateType(_candidateType, true);
}
}
/**
* Public for unit testing purposes.
*/
public Class<?> getCandidateType() {
return _candidateType;
}
@Override
public String toString ()
{
return this.query;
}
}
// throws an exception if there are numeric parameters which do not start with 1.
private void validateParameters() {
if (parameterTypes == null || parameterTypes.isEmpty()) {
return;
}
boolean numericParms = false;
boolean namedParms = false;
for (Object key : parameterTypes.keySet()) {
if (key instanceof Number) {
if (namedParms) {
throw new UserException(_loc.get("mixed-parameter-types", resolver.getQueryContext()
.getQueryString(), parameterTypes.keySet().toString()));
}
numericParms = true;
} else {
if (numericParms) {
throw new UserException(_loc.get("mixed-parameter-types", resolver.getQueryContext()
.getQueryString(), parameterTypes.keySet().toString()));
}
namedParms = true;
}
}
if (numericParms) {
if (!parameterTypes.keySet().contains(1)) {
throw new UserException(_loc.get("missing-positional-parameter", resolver.getQueryContext()
.getQueryString(), parameterTypes.keySet().toString()));
}
}
}
}