/*****************************************************************
* 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.cayenne.access.translator.ejbql;
import org.apache.cayenne.dba.QuotingStrategy;
import org.apache.cayenne.ejbql.EJBQLCompiledExpression;
import org.apache.cayenne.ejbql.EJBQLException;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.query.EJBQLQuery;
import org.apache.cayenne.query.EntityResultSegment;
import org.apache.cayenne.query.QueryMetadata;
import org.apache.cayenne.query.SQLTemplate;
import org.apache.cayenne.query.ScalarResultSegment;
import org.apache.cayenne.reflect.ClassDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* A context used for translating of EJBQL to SQL.
*
* @since 3.0
*/
public class EJBQLTranslationContext {
private EJBQLCompiledExpression compiledExpression;
protected Map<String, Object> namedParameters;
protected Map<Integer, Object> positionalParameters;
private EJBQLTranslatorFactory translatorFactory;
private QuotingStrategy quotingStrategy;
private EntityResolver entityResolver;
private List<Object> resultSetMetadata;
private Map<String, String> tableAliases;
private Map<String, Object> boundParameters;
private Map<String, Object> attributes;
private Map<String, String> idAliases;
private int resultDescriptorPosition;
private boolean usingAliases;
private boolean caseInsensitive;
private List<StringBuilder> bufferStack;
private List<StringBuilder> bufferChain;
private StringBuilder stackTop;
private int subselectCount;
private QueryMetadata queryMetadata;
// a flag indicating whether column expressions should be treated as result columns or
// not.
private boolean appendingResultColumns;
public EJBQLTranslationContext(EntityResolver entityResolver, EJBQLQuery query,
EJBQLCompiledExpression compiledExpression,
EJBQLTranslatorFactory translatorFactory, QuotingStrategy quotingStrategy) {
this.entityResolver = entityResolver;
this.compiledExpression = compiledExpression;
this.resultSetMetadata = query.getMetaData(entityResolver).getResultSetMapping();
this.namedParameters = query.getNamedParameters();
this.positionalParameters = query.getPositionalParameters();
this.translatorFactory = translatorFactory;
this.usingAliases = true;
this.caseInsensitive = false;
this.queryMetadata = query.getMetaData(entityResolver);
this.quotingStrategy = quotingStrategy;
// buffer stack will hold named buffers during translation in the order they were
// requested
this.bufferStack = new ArrayList<>();
// buffer chain will hold named and unnamed buffers in the order they should be
// concatenated
this.bufferChain = new ArrayList<>();
stackTop = new StringBuilder();
bufferChain.add(stackTop);
bufferStack.add(stackTop);
}
public SQLTemplate getQuery() {
// concatenate buffers...
StringBuilder main = bufferChain.get(0);
for (int i = 1; i < bufferChain.size(); i++) {
main.append(bufferChain.get(i));
}
String sql = main.length() > 0 ? main.toString() : null;
SQLTemplate query = new SQLTemplate(compiledExpression
.getRootDescriptor()
.getObjectClass(), sql);
query.setParams(boundParameters);
return query;
}
public QueryMetadata getMetadata(){
return queryMetadata;
}
private String resolveId(String id) {
if (idAliases == null) {
return id;
}
String resolvedAlias = idAliases.get(id);
if (resolvedAlias != null) {
return resolvedAlias;
}
return id;
}
EJBQLTranslatorFactory getTranslatorFactory() {
return translatorFactory;
}
EntityResolver getEntityResolver() {
return entityResolver;
}
/**
* Looks up entity descriptor for an identifier that can be a compiled expression id
* or one of the aliases.
*/
public ClassDescriptor getEntityDescriptor(String id) {
return compiledExpression.getEntityDescriptor(resolveId(id));
}
List<DbRelationship> getIncomingRelationships(EJBQLTableId id) {
List<DbRelationship> incoming = compiledExpression
.getIncomingRelationships(resolveId(id.getEntityId()));
// append tail of flattened relationships...
if (id.getDbPath() != null) {
DbEntity entity;
if (incoming == null || incoming.isEmpty()) {
entity = compiledExpression
.getEntityDescriptor(id.getEntityId())
.getEntity()
.getDbEntity();
}
else {
DbRelationship last = incoming.get(incoming.size() - 1);
entity = last.getTargetEntity();
}
incoming = new ArrayList<>(incoming);
Iterator<?> it = entity.resolvePathComponents(id.getDbPath());
while (it.hasNext()) {
incoming.add((DbRelationship) it.next());
}
}
return incoming;
}
/**
* Creates a previously unused id alias for an entity identified by an id.
*/
String createIdAlias(String id) {
if (idAliases == null) {
idAliases = new HashMap<>();
}
for (int i = 0; i < 1000; i++) {
String alias = id + "_alias" + i;
if (idAliases.containsKey(alias)) {
continue;
}
if (compiledExpression.getEntityDescriptor(alias) != null) {
continue;
}
idAliases.put(alias, id);
return alias;
}
throw new EJBQLException("Failed to create id alias");
}
/**
* Inserts a marker in the SQL, mapped to a StringBuilder that can be later filled
* with content.
*/
void markCurrentPosition(String marker) {
StringBuilder buffer = findOrCreateMarkedBuffer(marker);
bufferChain.add(buffer);
// immediately create unmarked buffer after the marked one and replace the bottom
// of the stack with it
StringBuilder tailBuffer = new StringBuilder();
bufferChain.add(tailBuffer);
bufferStack.set(0, tailBuffer);
stackTop = bufferStack.get(bufferStack.size() - 1);
}
/**
* Switches the current buffer to a marked buffer, pushing the currently used buffer
* on the stack. Note that this can be done even before the marker is inserted in the
* main buffer. If "reset" is true, any previous contents of the marker are cleared.
*/
public void pushMarker(String marker, boolean reset) {
stackTop = findOrCreateMarkedBuffer(marker);
if (reset) {
stackTop.delete(0, stackTop.length());
}
bufferStack.add(stackTop);
}
/**
* Pops a marker stack, switching to the previously used marker.
*/
void popMarker() {
int lastIndex = bufferStack.size() - 1;
bufferStack.remove(lastIndex);
stackTop = bufferStack.get(lastIndex - 1);
}
StringBuilder findOrCreateMarkedBuffer(String marker) {
StringBuilder buffer = (StringBuilder) getAttribute(marker);
if (buffer == null) {
buffer = new StringBuilder();
// register mapping of internal to external marker
setAttribute(marker, buffer);
}
return buffer;
}
/**
* Returns a context "attribute" stored for the given name. Attributes is a state
* preservation mechanism used by translators and have the same scope as the context.
*/
Object getAttribute(String name) {
return attributes != null ? attributes.get(name) : null;
}
/**
* Sets a context "attribute". Attributes is a state preservation mechanism used by
* translators and have the same scope as the context.
*/
void setAttribute(String var, Object value) {
if (attributes == null) {
attributes = new HashMap<>();
}
attributes.put(var, value);
}
/**
* Appends a piece of SQL to the internal buffer.
*/
public EJBQLTranslationContext append(String chunk) {
stackTop.append(chunk);
return this;
}
/**
* Appends a piece of SQL to the internal buffer.
*/
public EJBQLTranslationContext append(char chunk) {
stackTop.append(chunk);
return this;
}
/**
* Deletes a specified number of characters from the end of the current buffer.
*/
EJBQLTranslationContext trim(int n) {
int len = stackTop.length();
if (len >= n) {
stackTop.delete(len - n, len);
}
return this;
}
EJBQLCompiledExpression getCompiledExpression() {
return compiledExpression;
}
String bindPositionalParameter(int position) {
return bindParameter(positionalParameters.get(position));
}
/**
* <p>This is used in the processing of parameters into lists for the IN clause and
* is able to return a list of values that can be used to represent the bound
* parameter.</p>
*/
List<String> bindPositionalParameterFlatteningCollection(int position) {
return bindParameters(positionalParameters.get(position));
}
String bindNamedParameter(String name) {
return bindParameter(namedParameters.get(name));
}
/**
* <p>This is used in the processing of parameters into lists for the IN clause and
* is able to return a list of values that can be used to represent the bound
* parameter.</p>
*/
List<String> bindNamedParameterFlatteningCollection(String name) {
return bindParameters(namedParameters.get(name));
}
/**
* <p>This method takes a value object which may be a collection or a non-collection. If it
* is a collection then it will bind all of the values in the collection. If it is a non-
* collection then it will bind that single object.</p>
* @param value
* @return
*/
List<String> bindParameters(Object value) {
if(Collection.class.isAssignableFrom(value.getClass())) {
Iterator<?> parameterValueIterator = ((Collection<?>) value).iterator();
List<String> result = new ArrayList<>();
while(parameterValueIterator.hasNext()) {
result.add(bindParameter(parameterValueIterator.next()));
}
return result;
}
return Collections.singletonList(bindParameter(value));
}
/**
* Creates a new parameter variable, binding provided value to it.
*/
String bindParameter(Object value) {
return bindParameter(value, "id");
}
void rebindParameter(String boundName, Object newValue) {
boundParameters.put(boundName, newValue);
}
/**
* Creates a new parameter variable with the specified prefix, binding provided value
* to it.
*/
String bindParameter(Object value, String prefix) {
if (boundParameters == null) {
boundParameters = new HashMap<>();
}
String var = prefix + boundParameters.size();
boundParameters.put(var, value);
return var;
}
Object getBoundParameter(String name) {
return boundParameters != null ? boundParameters.get(name) : null;
}
/**
* Retrieves a SQL alias for the combination of EJBQL id variable and a table name. If
* such alias hasn't been used, it is created on the fly.
*/
protected String getTableAlias(String idPath, String tableName) {
if (!isUsingAliases()) {
return tableName;
}
StringBuilder keyBuffer = new StringBuilder();
// per JPA spec, 4.4.2, "Identification variables are case insensitive.", while
// relationship path is case-sensitive
int dot = idPath.indexOf('.');
if (dot > 0) {
keyBuffer.append(idPath.substring(0, dot).toLowerCase()).append(
idPath.substring(dot));
}
else {
keyBuffer.append(idPath.toLowerCase());
}
String key = keyBuffer.append(':').append(tableName).toString();
String alias;
if (tableAliases != null) {
alias = tableAliases.get(key);
}
else {
tableAliases = new HashMap<>();
alias = null;
}
if (alias == null) {
alias = "t" + tableAliases.size();
tableAliases.put(key, alias);
}
return alias;
}
/**
* Returns a positional EntityResult, incrementing position index on each call.
*/
EntityResultSegment nextEntityResult() {
if (resultSetMetadata == null) {
throw new EJBQLException(
"No result set mapping exists for expression, can't map EntityResult");
}
return (EntityResultSegment) resultSetMetadata.get(resultDescriptorPosition++);
}
/**
* Returns a positional column alias, incrementing position index on each call.
*/
String nextColumnAlias() {
if (resultSetMetadata == null) {
throw new EJBQLException(
"No result set mapping exists for expression, can't map column aliases");
}
return ((ScalarResultSegment) resultSetMetadata.get(resultDescriptorPosition++))
.getColumn();
}
public boolean isAppendingResultColumns() {
return appendingResultColumns;
}
void setAppendingResultColumns(boolean appendingResultColumns) {
this.appendingResultColumns = appendingResultColumns;
}
public boolean isUsingAliases() {
return usingAliases;
}
public void setUsingAliases(boolean useAliases) {
this.usingAliases = useAliases;
}
public boolean isCaseInsensitive() {
return caseInsensitive;
}
public void setCaseInsensitive(boolean caseInsensitive) {
this.caseInsensitive = caseInsensitive;
}
public QuotingStrategy getQuotingStrategy() {
return quotingStrategy;
}
public void onSubselect() {
subselectCount++;
}
public String makeDistinctMarker() {
return "DISTINCT_MARKER" + subselectCount;
}
String makeWhereMarker() {
return "WHERE_MARKER" + subselectCount;
}
String makeEntityQualifierMarker() {
return "ENTITY_QUALIIER" + subselectCount;
}
}