/*
* Copyright 2014 - 2017 Blazebit.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.blazebit.persistence.impl.hibernate;
import antlr.collections.AST;
import com.blazebit.apt.service.ServiceProvider;
import com.blazebit.persistence.ReturningResult;
import com.blazebit.persistence.spi.ConfigurationSource;
import com.blazebit.persistence.spi.CteQueryWrapper;
import com.blazebit.persistence.spi.DbmsDialect;
import com.blazebit.persistence.spi.DbmsStatementType;
import com.blazebit.persistence.spi.ExtendedQuerySupport;
import com.blazebit.reflection.ReflectionUtils;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.LockOptions;
import org.hibernate.MappingException;
import org.hibernate.TypeMismatchException;
import org.hibernate.engine.query.spi.HQLQueryPlan;
import org.hibernate.engine.query.spi.QueryPlanCache;
import org.hibernate.engine.spi.Mapping;
import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.RowSelection;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.TypedValue;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.AutoFlushEvent;
import org.hibernate.event.spi.AutoFlushEventListener;
import org.hibernate.event.spi.EventSource;
import org.hibernate.event.spi.EventType;
import org.hibernate.hql.internal.QueryExecutionRequestException;
import org.hibernate.hql.internal.antlr.SqlTokenTypes;
import org.hibernate.hql.internal.ast.ParameterTranslationsImpl;
import org.hibernate.hql.internal.ast.exec.BasicExecutor;
import org.hibernate.hql.internal.ast.exec.DeleteExecutor;
import org.hibernate.hql.internal.ast.exec.StatementExecutor;
import org.hibernate.hql.internal.ast.tree.DotNode;
import org.hibernate.hql.internal.ast.tree.FromElement;
import org.hibernate.hql.internal.ast.tree.QueryNode;
import org.hibernate.hql.internal.ast.tree.SelectClause;
import org.hibernate.hql.spi.ParameterTranslations;
import org.hibernate.hql.spi.QueryTranslator;
import org.hibernate.internal.util.collections.BoundedConcurrentHashMap;
import org.hibernate.loader.hql.QueryLoader;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Table;
import org.hibernate.param.ParameterSpecification;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.JoinedSubclassEntityPersister;
import org.hibernate.persister.entity.SingleTableEntityPersister;
import org.hibernate.persister.entity.UnionSubclassEntityPersister;
import org.hibernate.type.ManyToOneType;
import org.hibernate.type.Type;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.metamodel.EntityType;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Logger;
@ServiceProvider(ExtendedQuerySupport.class)
public class HibernateExtendedQuerySupport implements ExtendedQuerySupport {
private static final Logger LOG = Logger.getLogger(HibernateExtendedQuerySupport.class.getName());
private static final String[] KNOWN_STATEMENTS = { "select ", "insert ", "update ", "delete " };
private final ConcurrentMap<SessionFactoryImplementor, BoundedConcurrentHashMap<QueryPlanCacheKey, HQLQueryPlan>> queryPlanCachesCache = new ConcurrentHashMap<SessionFactoryImplementor, BoundedConcurrentHashMap<QueryPlanCacheKey, HQLQueryPlan>>();
private final HibernateAccess hibernateAccess;
public HibernateExtendedQuerySupport() {
Iterator<HibernateAccess> serviceIter = ServiceLoader.load(HibernateAccess.class).iterator();
if (!serviceIter.hasNext()) {
throw new IllegalStateException("Hibernate integration was not found on the class path!");
}
this.hibernateAccess = serviceIter.next();
}
@Override
public boolean supportsAdvancedSql() {
return true;
}
@Override
public String getSql(EntityManager em, Query query) {
SessionImplementor session = em.unwrap(SessionImplementor.class);
HQLQueryPlan queryPlan = getOriginalQueryPlan(session, query);
if (queryPlan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
QueryTranslator queryTranslator = queryPlan.getTranslators()[0];
String[] sqls;
if (queryTranslator.isManipulationStatement()) {
StatementExecutor executor = getStatementExecutor(queryTranslator);
if (!(executor instanceof BasicExecutor)) {
throw new IllegalArgumentException("Using polymorphic deletes/updates with CTEs is not yet supported");
}
sqls = executor.getSqlStatements();
} else {
sqls = queryPlan.getSqlStrings();
}
// TODO: have to handle multiple sql strings which happens when having e.g. a polymorphic UPDATE/DELETE
for (int i = 0; i < sqls.length; i++) {
if (sqls[i] != null) {
return sqls[i];
}
}
return null;
}
@Override
public List<String> getCascadingDeleteSql(EntityManager em, Query query) {
SessionImplementor session = em.unwrap(SessionImplementor.class);
HQLQueryPlan queryPlan = getOriginalQueryPlan(session, query);
if (queryPlan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
// TODO: check if this is actually a delete statement
QueryTranslator queryTranslator = queryPlan.getTranslators()[0];
StatementExecutor executor = getStatementExecutor(queryTranslator);
if (executor == null || !(executor instanceof DeleteExecutor)) {
return Collections.EMPTY_LIST;
}
List<String> deletes = getField(executor, "deletes");
if (deletes == null) {
return Collections.EMPTY_LIST;
}
return deletes;
}
private HQLQueryPlan getOriginalQueryPlan(SessionImplementor session, Query query) {
SessionFactoryImplementor sfi = session.getFactory();
org.hibernate.Query hibernateQuery = query.unwrap(org.hibernate.Query.class);
Map<String, TypedValue> namedParams = new HashMap<String, TypedValue>(hibernateAccess.getNamedParams(hibernateQuery));
String queryString = hibernateAccess.expandParameterLists(session, hibernateQuery, namedParams);
return sfi.getQueryPlanCache().getHQLQueryPlan(queryString, false, Collections.EMPTY_MAP);
}
@Override
public String[] getColumnNames(EntityManager em, EntityType<?> entityType, String attributeName) {
SessionImplementor session = em.unwrap(SessionImplementor.class);
SessionFactoryImplementor sfi = session.getFactory();
try {
return ((AbstractEntityPersister) sfi.getClassMetadata(entityType.getJavaType())).getPropertyColumnNames(attributeName);
} catch (MappingException e) {
throw new RuntimeException("Unknown property [" + attributeName + "] of entity [" + entityType.getJavaType() + "]", e);
}
}
@Override
public String[] getColumnTypes(EntityManager em, EntityType<?> entityType, String attributeName) {
SessionImplementor session = em.unwrap(SessionImplementor.class);
SessionFactoryImplementor sfi = session.getFactory();
AbstractEntityPersister entityPersister = (AbstractEntityPersister) sfi.getClassMetadata(entityType.getJavaType());
String[] columnNames = entityPersister.getPropertyColumnNames(attributeName);
Database database = sfi.getServiceRegistry().locateServiceBinding(Database.class).getService();
Table[] tables;
if (entityPersister instanceof JoinedSubclassEntityPersister) {
tables = new Table[((JoinedSubclassEntityPersister) entityPersister).getTableSpan()];
for (int i = 0; i < tables.length; i++) {
tables[i] = database.getTable(entityPersister.getSubclassTableName(i));
}
} else if (entityPersister instanceof UnionSubclassEntityPersister) {
tables = new Table[((UnionSubclassEntityPersister) entityPersister).getTableSpan()];
for (int i = 0; i < tables.length; i++) {
tables[i] = database.getTable(entityPersister.getSubclassTableName(i));
}
} else if (entityPersister instanceof SingleTableEntityPersister) {
tables = new Table[((SingleTableEntityPersister) entityPersister).getTableSpan()];
for (int i = 0; i < tables.length; i++) {
tables[i] = database.getTable(entityPersister.getSubclassTableName(i));
}
} else {
tables = new Table[] { database.getTable(entityPersister.getTableName()) };
}
// In this case, the property might represent a formula
if (columnNames.length == 1 && columnNames[0] == null) {
Type propertyType = entityPersister.getPropertyType(attributeName);
long length;
int precision;
int scale;
try {
Method m = Type.class.getMethod("dictatedSizes", Mapping.class);
Object size = ((Object[]) m.invoke(propertyType, sfi))[0];
length = (long) size.getClass().getMethod("getLength").invoke(size);
precision = (int) size.getClass().getMethod("getPrecision").invoke(size);
scale = (int) size.getClass().getMethod("getScale").invoke(size);
} catch (Exception ex) {
throw new RuntimeException("Could not determine the column type of the attribute: " + attributeName + " of the entity: " + entityType.getName());
}
return new String[] {
sfi.getDialect().getTypeName(
propertyType.sqlTypes(sfi)[0],
length,
precision,
scale
)
};
}
String[] columnTypes = new String[columnNames.length];
for (int i = 0; i < columnNames.length; i++) {
Column column = null;
for (int j = 0; j < tables.length; j++) {
column = tables[j].getColumn(new Column(columnNames[i]));
if (column != null) {
break;
}
}
if (column == null) {
throw new IllegalArgumentException("Could not find column '" + columnNames[i] + "' in for entity: " + entityType.getName());
}
columnTypes[i] = column.getSqlType();
}
return columnTypes;
}
@Override
public String getSqlAlias(EntityManager em, Query query, String alias) {
SessionImplementor session = em.unwrap(SessionImplementor.class);
HQLQueryPlan plan = getOriginalQueryPlan(session, query);
if (plan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
QueryTranslator translator = plan.getTranslators()[0];
QueryNode queryNode = getField(translator, "sqlAst");
FromElement fromElement = queryNode.getFromClause().getFromElement(alias);
if (fromElement == null) {
throw new IllegalArgumentException("The alias " + alias + " could not be found in the query: " + query);
}
return fromElement.getTableAlias();
}
@Override
public int getSqlSelectAliasPosition(EntityManager em, Query query, String alias) {
SessionImplementor session = em.unwrap(SessionImplementor.class);
HQLQueryPlan plan = getOriginalQueryPlan(session, query);
if (plan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
QueryTranslator translator = plan.getTranslators()[0];
try {
QueryNode queryNode = getField(translator, "sqlAst");
String[] aliases = queryNode.getSelectClause().getQueryReturnAliases();
for (int i = 0; i < aliases.length; i++) {
if (alias.equals(aliases[i])) {
// the ordinal is 1 based
return i + 1;
}
}
return -1;
} catch (Exception e1) {
throw new RuntimeException(e1);
}
}
@Override
public int getSqlSelectAttributePosition(EntityManager em, Query query, String expression) {
if (expression.contains(".")) {
// TODO: implement
throw new UnsupportedOperationException("Embeddables are not yet supported!");
}
SessionImplementor session = em.unwrap(SessionImplementor.class);
HQLQueryPlan plan = getOriginalQueryPlan(session, query);
if (plan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
QueryTranslator translator = plan.getTranslators()[0];
try {
QueryNode queryNode = getField(translator, "sqlAst");
SelectClause selectClause = queryNode.getSelectClause();
Type[] queryReturnTypes = selectClause.getQueryReturnTypes();
boolean found = false;
// The ordinal is 1 based
int position = 1;
for (int i = 0; i < queryReturnTypes.length; i++) {
Type t = queryReturnTypes[i];
if (t instanceof ManyToOneType) {
ManyToOneType manyToOneType = (ManyToOneType) t;
AbstractEntityPersister persister = (AbstractEntityPersister) session.getFactory().getEntityPersister(manyToOneType.getAssociatedEntityName());
int propertyIndex = persister.getPropertyIndex(expression);
found = true;
for (int j = 0; j < propertyIndex; j++) {
position += persister.getPropertyColumnNames(j).length;
}
// Increment to the actual property position
position++;
} else {
position++;
}
}
if (found) {
return position;
}
AST selectItem = selectClause.getFirstChild();
while (selectItem != null && (selectItem.getType() == SqlTokenTypes.DISTINCT || selectItem.getType() == SqlTokenTypes.ALL)) {
selectItem = selectItem.getNextSibling();
}
position = 1;
for (AST n = selectItem; n != null; n = n.getNextSibling()) {
if (n instanceof DotNode) {
DotNode dot = (DotNode) n;
if (expression.equals(dot.getPropertyPath())) {
// Check if the property is an embeddable
if (dot.getText().contains(",")) {
throw new IllegalStateException("Can't order by the embeddable: " + expression);
}
found = true;
break;
}
}
position++;
}
if (found) {
return position;
}
return -1;
} catch (Exception e1) {
throw new RuntimeException(e1);
}
}
@Override
@SuppressWarnings("rawtypes")
public List getResultList(com.blazebit.persistence.spi.ServiceProvider serviceProvider, List<Query> participatingQueries, Query query, String sqlOverride) {
EntityManager em = serviceProvider.getService(EntityManager.class);
try {
return list(serviceProvider, em, participatingQueries, query, sqlOverride);
} catch (QueryExecutionRequestException he) {
LOG.severe("Could not execute the following SQL query: " + sqlOverride);
throw new IllegalStateException(he);
} catch (TypeMismatchException e) {
LOG.severe("Could not execute the following SQL query: " + sqlOverride);
throw new IllegalArgumentException(e);
} catch (HibernateException he) {
LOG.severe("Could not execute the following SQL query: " + sqlOverride);
throw hibernateAccess.convert(em, he);
}
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public Object getSingleResult(com.blazebit.persistence.spi.ServiceProvider serviceProvider, List<Query> participatingQueries, Query query, String sqlOverride) {
EntityManager em = serviceProvider.getService(EntityManager.class);
try {
final List result = list(serviceProvider, em, participatingQueries, query, sqlOverride);
if (result.size() == 0) {
NoResultException nre = new NoResultException("No entity found for query");
hibernateAccess.handlePersistenceException(em, nre);
throw nre;
} else if (result.size() > 1) {
final Set uniqueResult = new HashSet(result);
if (uniqueResult.size() > 1) {
NonUniqueResultException nure = new NonUniqueResultException("result returns more than one element");
hibernateAccess.handlePersistenceException(em, nure);
throw nure;
} else {
return uniqueResult.iterator().next();
}
} else {
return result.get(0);
}
} catch (QueryExecutionRequestException he) {
LOG.severe("Could not execute the following SQL query: " + sqlOverride);
throw new IllegalStateException(he);
} catch (TypeMismatchException e) {
LOG.severe("Could not execute the following SQL query: " + sqlOverride);
throw new IllegalArgumentException(e);
} catch (HibernateException he) {
LOG.severe("Could not execute the following SQL query: " + sqlOverride);
throw hibernateAccess.convert(em, he);
}
}
@SuppressWarnings("rawtypes")
private List list(com.blazebit.persistence.spi.ServiceProvider serviceProvider, EntityManager em, List<Query> participatingQueries, Query query, String finalSql) {
SessionImplementor session = em.unwrap(SessionImplementor.class);
SessionFactoryImplementor sfi = session.getFactory();
if (session.isClosed()) {
throw new PersistenceException("Entity manager is closed!");
}
// Create combined query parameters
List<String> queryStrings = new ArrayList<>(participatingQueries.size());
QueryParamEntry queryParametersEntry = createQueryParameters(em, participatingQueries, queryStrings);
QueryParameters queryParameters = queryParametersEntry.queryParameters;
QueryPlanCacheKey cacheKey = createCacheKey(queryStrings);
CacheEntry<HQLQueryPlan> queryPlanEntry = getQueryPlan(sfi, query, cacheKey);
HQLQueryPlan queryPlan = queryPlanEntry.getValue();
if (!queryPlanEntry.isFromCache()) {
prepareQueryPlan(queryPlan, queryParametersEntry.specifications, finalSql, session, participatingQueries.get(participatingQueries.size() - 1), false, serviceProvider.getService(DbmsDialect.class));
queryPlan = putQueryPlanIfAbsent(sfi, cacheKey, queryPlan);
}
return hibernateAccess.performList(queryPlan, session, queryParameters);
}
@Override
public int executeUpdate(com.blazebit.persistence.spi.ServiceProvider serviceProvider, List<Query> participatingQueries, Query query, String finalSql) {
DbmsDialect dbmsDialect = serviceProvider.getService(DbmsDialect.class);
EntityManager em = serviceProvider.getService(EntityManager.class);
SessionImplementor session = em.unwrap(SessionImplementor.class);
SessionFactoryImplementor sfi = session.getFactory();
if (session.isClosed()) {
throw new PersistenceException("Entity manager is closed!");
}
Integer firstResult = null;
Integer maxResults = null;
if (query.getFirstResult() > 0) {
firstResult = query.getFirstResult();
}
if (query.getMaxResults() != Integer.MAX_VALUE) {
maxResults = query.getMaxResults();
}
// Create combined query parameters
List<String> queryStrings = new ArrayList<>(participatingQueries.size());
QueryParamEntry queryParametersEntry = createQueryParameters(em, participatingQueries, queryStrings);
QueryParameters queryParameters = queryParametersEntry.queryParameters;
QueryPlanCacheKey cacheKey = createCacheKey(queryStrings, firstResult, maxResults);
CacheEntry<HQLQueryPlan> queryPlanEntry = getQueryPlan(sfi, query, cacheKey);
HQLQueryPlan queryPlan = queryPlanEntry.getValue();
if (!queryPlanEntry.isFromCache()) {
prepareQueryPlan(queryPlan, queryParametersEntry.specifications, finalSql, session, participatingQueries.get(participatingQueries.size() - 1), true, dbmsDialect);
queryPlan = putQueryPlanIfAbsent(sfi, cacheKey, queryPlan);
}
if (queryPlan.getReturnMetadata() == null) {
return hibernateAccess.performExecuteUpdate(queryPlan, session, queryParameters);
}
boolean caseInsensitive = !Boolean.valueOf(serviceProvider.getService(ConfigurationSource.class).getProperty("com.blazebit.persistence.returning_clause_case_sensitive"));
String exampleQuerySql = queryPlan.getSqlStrings()[0];
String[][] returningColumns = getReturningColumns(caseInsensitive, exampleQuerySql);
int[] returningColumnTypes = dbmsDialect.needsReturningSqlTypes() ? getReturningColumnTypes(queryPlan, sfi) : null;
try {
@SuppressWarnings("unchecked")
List<Object> results = hibernateAccess.performList(queryPlan, wrapSession(session, dbmsDialect, returningColumns, returningColumnTypes, null), queryParameters);
if (results.size() != 1) {
throw new IllegalArgumentException("Expected size 1 but was: " + results.size());
}
Number count = (Number) results.get(0);
return count.intValue();
} catch (QueryExecutionRequestException he) {
LOG.severe("Could not execute the following SQL query: " + finalSql);
throw new IllegalStateException(he);
} catch (TypeMismatchException e) {
LOG.severe("Could not execute the following SQL query: " + finalSql);
throw new IllegalArgumentException(e);
} catch (HibernateException he) {
LOG.severe("Could not execute the following SQL query: " + finalSql);
hibernateAccess.throwPersistenceException(em, he);
return 0;
}
}
@Override
@SuppressWarnings("unchecked")
public ReturningResult<Object[]> executeReturning(com.blazebit.persistence.spi.ServiceProvider serviceProvider, List<Query> participatingQueries, Query exampleQuery, String sqlOverride) {
DbmsDialect dbmsDialect = serviceProvider.getService(DbmsDialect.class);
EntityManager em = serviceProvider.getService(EntityManager.class);
SessionImplementor session = em.unwrap(SessionImplementor.class);
SessionFactoryImplementor sfi = session.getFactory();
if (session.isClosed()) {
throw new PersistenceException("Entity manager is closed!");
}
// Create combined query parameters
List<String> queryStrings = new ArrayList<>(participatingQueries.size());
QueryParamEntry queryParametersEntry = createQueryParameters(em, participatingQueries, queryStrings);
QueryParameters queryParameters = queryParametersEntry.queryParameters;
// Create plan for example query
QueryPlanCacheKey cacheKey = createCacheKey(queryStrings);
CacheEntry<HQLQueryPlan> queryPlanEntry = getQueryPlan(sfi, exampleQuery, cacheKey);
HQLQueryPlan queryPlan = queryPlanEntry.getValue();
String exampleQuerySql = queryPlan.getSqlStrings()[0];
StringBuilder sqlSb = new StringBuilder(sqlOverride.length() + 100);
sqlSb.append(sqlOverride);
boolean caseInsensitive = !Boolean.valueOf(serviceProvider.getService(ConfigurationSource.class).getProperty("com.blazebit.persistence.returning_clause_case_sensitive"));
String[][] returningColumns = getReturningColumns(caseInsensitive, exampleQuerySql);
int[] returningColumnTypes = dbmsDialect.needsReturningSqlTypes() ? getReturningColumnTypes(queryPlan, sfi) : null;
String finalSql = sqlSb.toString();
try {
HibernateReturningResult<Object[]> returningResult = new HibernateReturningResult<Object[]>();
if (!queryPlanEntry.isFromCache()) {
prepareQueryPlan(queryPlan, queryParametersEntry.specifications, finalSql, session, participatingQueries.get(participatingQueries.size() - 1), true, dbmsDialect);
queryPlan = putQueryPlanIfAbsent(sfi, cacheKey, queryPlan);
}
if (queryPlan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
QueryTranslator queryTranslator = queryPlan.getTranslators()[0];
// Extract query loader for native listing
QueryLoader queryLoader = getField(queryTranslator, "queryLoader");
// Do the native list operation with custom session and combined parameters
/*
* NATIVE LIST START
*/
hibernateAccess.checkTransactionSynchStatus(session);
queryParameters.validateParameters();
AutoFlushEvent event = new AutoFlushEvent(queryPlan.getQuerySpaces(), (EventSource) session);
for (AutoFlushEventListener listener : listeners(sfi, EventType.AUTO_FLUSH) ) {
listener.onAutoFlush( event );
}
List<Object[]> results = Collections.EMPTY_LIST;
boolean success = false;
try {
results = hibernateAccess.list(queryLoader, wrapSession(session, dbmsDialect, returningColumns, returningColumnTypes, returningResult), queryParameters);
success = true;
} catch (QueryExecutionRequestException he) {
LOG.severe("Could not execute the following SQL query: " + finalSql);
throw new IllegalStateException(he);
} catch (TypeMismatchException e) {
LOG.severe("Could not execute the following SQL query: " + finalSql);
throw new IllegalArgumentException(e);
} catch (HibernateException he) {
LOG.severe("Could not execute the following SQL query: " + finalSql);
throw hibernateAccess.convert(em, he);
} finally {
hibernateAccess.afterTransaction(session, success);
}
/*
* NATIVE LIST END
*/
returningResult.setResultList(results);
return returningResult;
} catch (Exception e1) {
throw new RuntimeException(e1);
}
}
private static String[][] getReturningColumns(boolean caseInsensitive, String exampleQuerySql) {
int fromIndex = exampleQuerySql.indexOf("from");
int selectIndex = exampleQuerySql.indexOf("select");
String[] selectItems = splitSelectItems(exampleQuerySql.subSequence(selectIndex + "select".length() + 1, fromIndex));
String[][] returningColumns = new String[selectItems.length][2];
for (int i = 0; i < selectItems.length; i++) {
String selectItemWithAlias = selectItems[i].substring(selectItems[i].lastIndexOf('.') + 1);
if (caseInsensitive) {
returningColumns[i][0] = selectItemWithAlias.substring(0, selectItemWithAlias.indexOf(' ')).toLowerCase();
} else {
returningColumns[i][0] = selectItemWithAlias.substring(0, selectItemWithAlias.indexOf(' '));
}
returningColumns[i][1] = selectItemWithAlias.substring(selectItemWithAlias.lastIndexOf(' ') + 1);
}
return returningColumns;
}
private static int[] getReturningColumnTypes(HQLQueryPlan queryPlan, SessionFactoryImplementor sfi) {
List<Integer> sqlTypes = new ArrayList<>();
Type[] types = queryPlan.getReturnMetadata().getReturnTypes();
for (int i = 0; i < types.length; i++) {
int[] sqlTypeArray = types[i].sqlTypes(sfi);
for (int j = 0; j < sqlTypeArray.length; j++) {
sqlTypes.add(sqlTypeArray[j]);
}
}
int[] returningColumnTypes = new int[sqlTypes.size()];
for (int i = 0; i < sqlTypes.size(); i++) {
returningColumnTypes[i] = sqlTypes.get(i);
}
return returningColumnTypes;
}
private static String[] splitSelectItems(CharSequence itemsString) {
List<String> selectItems = new ArrayList<String>();
StringBuilder sb = new StringBuilder();
int parenthesis = 0;
boolean text = false;
int i = 0;
int length = itemsString.length();
while (i < length) {
char c = itemsString.charAt(i);
if (text) {
if (c == '(') {
parenthesis++;
} else if (c == ')') {
parenthesis--;
} else if (parenthesis == 0 && c == ',') {
selectItems.add(trim(sb));
sb.setLength(0);
text = false;
i++;
continue;
}
sb.append(c);
} else {
if (Character.isWhitespace(c)) {
// skip whitespace
} else {
sb.append(c);
text = true;
}
}
i++;
}
if (text) {
selectItems.add(trim(sb));
}
return selectItems.toArray(new String[selectItems.size()]);
}
private static String trim(StringBuilder sb) {
int i = sb.length() - 1;
while (i >= 0) {
if (!Character.isWhitespace(sb.charAt(i))) {
break;
} else {
i--;
}
}
return sb.substring(0, i + 1);
}
private <E> Iterable<E> listeners(SessionFactoryImplementor factory, EventType<E> type) {
return factory.getServiceRegistry().getService(EventListenerRegistry.class).getEventListenerGroup(type).listeners();
}
private QueryParamEntry createQueryParameters(EntityManager em, List<Query> participatingQueries, List<String> queryStrings) {
List<ParameterSpecification> parameterSpecifications = new ArrayList<ParameterSpecification>();
List<Type> types = new ArrayList<Type>();
List<Object> values = new ArrayList<Object>();
Map<String, TypedValue> namedParams = new LinkedHashMap<String, TypedValue>();
Serializable collectionKey = null;
LockOptions lockOptions = new LockOptions();
RowSelection rowSelection = new RowSelection();
boolean readOnly = false; // TODO: readonly?
boolean cacheable = false; // TODO: cacheable?
String cacheRegion = null;
String comment = null;
List<String> queryHints = null;
for (QueryParamEntry queryParamEntry : getQueryParamEntries(em, participatingQueries)) {
queryStrings.add(queryParamEntry.queryString);
QueryParameters participatingQueryParameters = queryParamEntry.queryParameters;
// Merge parameters
Collections.addAll(types, participatingQueryParameters.getPositionalParameterTypes());
Collections.addAll(values, participatingQueryParameters.getPositionalParameterValues());
namedParams.putAll(participatingQueryParameters.getNamedParameters());
parameterSpecifications.addAll(queryParamEntry.specifications);
// Merge row selections
if (participatingQueryParameters.hasRowSelection()) {
RowSelection original = queryParamEntry.queryParameters.getRowSelection();
// Check for defaults
/***************************************************************************
* TODO: Either we do it like this, or let these values be passed in separately
**************************************************************************/
if (rowSelection.getFirstRow() == null || rowSelection.getFirstRow() < 1) {
rowSelection.setFirstRow(original.getFirstRow());
} else if (original.getFirstRow() != null && original.getFirstRow() > 0 && !original.getFirstRow().equals(rowSelection.getFirstRow())) {
throw new IllegalStateException("Multiple row selections not allowed!");
}
if (rowSelection.getMaxRows() == null || rowSelection.getMaxRows() == Integer.MAX_VALUE) {
rowSelection.setMaxRows(original.getMaxRows());
} else if (original.getMaxRows() != null && original.getMaxRows() != Integer.MAX_VALUE && !original.getMaxRows().equals(rowSelection.getMaxRows())) {
throw new IllegalStateException("Multiple row selections not allowed!");
}
if (rowSelection.getFetchSize() == null) {
rowSelection.setFetchSize(original.getFetchSize());
} else if (original.getFetchSize() != null && !original.getFetchSize().equals(rowSelection.getFetchSize())) {
throw new IllegalStateException("Multiple row selections not allowed!");
}
if (rowSelection.getTimeout() == null) {
rowSelection.setTimeout(original.getTimeout());
} else if (original.getTimeout() != null && !original.getTimeout().equals(rowSelection.getTimeout())) {
throw new IllegalStateException("Multiple row selections not allowed!");
}
}
// Merge lock options
LockOptions originalLockOptions = participatingQueryParameters.getLockOptions();
if (originalLockOptions.getScope()) {
lockOptions.setScope(true);
}
if (originalLockOptions.getLockMode() != LockMode.NONE) {
if (lockOptions.getLockMode() != LockMode.NONE && lockOptions.getLockMode() != originalLockOptions.getLockMode()) {
throw new IllegalStateException("Multiple different lock modes!");
}
lockOptions.setLockMode(originalLockOptions.getLockMode());
}
if (originalLockOptions.getTimeOut() != -1) {
if (lockOptions.getTimeOut() != -1 && lockOptions.getTimeOut() != originalLockOptions.getTimeOut()) {
throw new IllegalStateException("Multiple different lock timeouts!");
}
lockOptions.setTimeOut(originalLockOptions.getTimeOut());
}
@SuppressWarnings("unchecked")
Iterator<Map.Entry<String, LockMode>> aliasLockIter = participatingQueryParameters.getLockOptions().getAliasLockIterator();
while (aliasLockIter.hasNext()) {
Map.Entry<String, LockMode> entry = aliasLockIter.next();
lockOptions.setAliasSpecificLockMode(entry.getKey(), entry.getValue());
}
}
QueryParameters queryParameters = hibernateAccess.createQueryParameters(
types.toArray(new Type[types.size()]),
values.toArray(new Object[values.size()]),
namedParams,
lockOptions,
rowSelection,
true,
readOnly,
cacheable,
cacheRegion,
comment,
queryHints,
collectionKey == null ? null : new Serializable[] { collectionKey }
);
return new QueryParamEntry(null, queryParameters, parameterSpecifications);
}
private SessionImplementor wrapSession(SessionImplementor session, DbmsDialect dbmsDialect, String[][] columns, int[] returningSqlTypes, HibernateReturningResult<?> returningResult) {
// We do all this wrapping to change the StatementPreparer that is returned by the JdbcCoordinator
// Instead of calling executeQuery, we delegate to executeUpdate and then return the generated keys in the prepared statement wrapper that we apply
return hibernateAccess.wrapSession(session, dbmsDialect, columns, returningSqlTypes, returningResult);
}
private List<QueryParamEntry> getQueryParamEntries(EntityManager em, List<Query> queries) {
SessionImplementor session = em.unwrap(SessionImplementor.class);
SessionFactoryImplementor sfi = session.getFactory();
List<QueryParamEntry> result = new ArrayList<QueryParamEntry>(queries.size());
Deque<Query> queryQueue = new LinkedList<Query>(queries);
while (queryQueue.size() > 0) {
Query q = queryQueue.remove();
if (q instanceof CteQueryWrapper) {
List<Query> participatingQueries = ((CteQueryWrapper) q).getParticipatingQueries();
for (int i = participatingQueries.size() - 1; i > -1; i--) {
queryQueue.addFirst(participatingQueries.get(i));
}
continue;
}
org.hibernate.Query hibernateQuery = q.unwrap(org.hibernate.Query.class);
Map<String, TypedValue> namedParams = new HashMap<String, TypedValue>(hibernateAccess.getNamedParams(hibernateQuery));
String queryString = hibernateAccess.expandParameterLists(session, hibernateQuery, namedParams);
HQLQueryPlan queryPlan = sfi.getQueryPlanCache().getHQLQueryPlan(queryString, false, Collections.EMPTY_MAP);
if (queryPlan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
QueryTranslator queryTranslator = queryPlan.getTranslators()[0];
QueryParameters queryParameters;
List<ParameterSpecification> specifications;
try {
queryParameters = hibernateAccess.getQueryParameters(hibernateQuery, namedParams);
specifications = getField(queryTranslator, "collectedParameterSpecifications");
// This only happens for modification queries
if (specifications == null) {
StatementExecutor executor = getStatementExecutor(queryTranslator);
if (!(executor instanceof BasicExecutor)) {
throw new IllegalArgumentException("Using polymorphic deletes/updates with CTEs is not yet supported");
}
specifications = getField(executor, "parameterSpecifications");
}
} catch (Exception e1) {
throw new RuntimeException(e1);
}
result.add(new QueryParamEntry(queryString, queryParameters, specifications));
}
return result;
}
private StatementExecutor getStatementExecutor(QueryTranslator queryTranslator) {
return getField(queryTranslator, "statementExecutor");
}
private QueryTranslator prepareQueryPlan(HQLQueryPlan queryPlan, List<ParameterSpecification> queryParameterSpecifications, String finalSql, SessionImplementor session, Query lastQuery, boolean isModification, DbmsDialect dbmsDialect) {
try {
if (queryPlan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
QueryTranslator queryTranslator = queryPlan.getTranslators()[0];
// Override the sql in the query plan
setField(queryTranslator, "sql", finalSql);
QueryLoader queryLoader = getField(queryTranslator, "queryLoader");
// INSERT statement does not have a query loader
if (queryLoader != null) {
setField(queryLoader, "factory", hibernateAccess.wrapSessionFactory(queryLoader.getFactory(), dbmsDialect));
}
// Modification queries keep the sql in the executor
StatementExecutor executor = null;
Field statementExectuor = null;
boolean madeAccessible = false;
try {
statementExectuor = ReflectionUtils.getField(queryTranslator.getClass(), "statementExecutor");
madeAccessible = !statementExectuor.isAccessible();
if (madeAccessible) {
statementExectuor.setAccessible(true);
}
executor = (StatementExecutor) statementExectuor.get(queryTranslator);
if (executor == null && isModification) {
// We have to set an executor
org.hibernate.Query lastHibernateQuery = lastQuery.unwrap(org.hibernate.Query.class);
Map<String, TypedValue> namedParams = new HashMap<String, TypedValue>(hibernateAccess.getNamedParams(lastHibernateQuery));
String queryString = hibernateAccess.expandParameterLists(session, lastHibernateQuery, namedParams);
// Extract the executor from the last query which is the actual main query
HQLQueryPlan lastQueryPlan = session.getFactory().getQueryPlanCache().getHQLQueryPlan(queryString, false, Collections.EMPTY_MAP);
if (lastQueryPlan.getTranslators().length > 1) {
throw new IllegalArgumentException("No support for multiple translators yet!");
}
QueryTranslator lastQueryTranslator = lastQueryPlan.getTranslators()[0];
executor = (StatementExecutor) statementExectuor.get(lastQueryTranslator);
// Now we use this executor for our example query
statementExectuor.set(queryTranslator, executor);
}
} finally {
if (madeAccessible) {
statementExectuor.setAccessible(false);
}
}
if (executor != null) {
setField(executor, "sql", finalSql);
setField(executor, BasicExecutor.class, "parameterSpecifications", queryParameterSpecifications);
if (executor instanceof DeleteExecutor) {
int withIndex;
if (dbmsDialect.supportsModificationQueryInWithClause()) {
setField(executor, "deletes", new ArrayList<String>());
} else if ((withIndex = finalSql.indexOf("with ")) != -1) {
int end = getCTEEnd(finalSql, withIndex);
List<String> originalDeletes = getField(executor, "deletes");
int maxLength = 0;
for (String s : originalDeletes) {
maxLength = Math.max(maxLength, s.length());
}
List<String> deletes = new ArrayList<String>(originalDeletes.size());
StringBuilder newSb = new StringBuilder(end + maxLength);
// Prefix properly with cte
StringBuilder withClauseSb = new StringBuilder(end - withIndex);
withClauseSb.append(finalSql, withIndex, end);
for (String s : originalDeletes) {
// TODO: The strings should also receive the simple CTE name instead of the complex one
newSb.append(s);
dbmsDialect.appendExtendedSql(newSb, DbmsStatementType.DELETE, false, false, withClauseSb, null, null, null, null);
deletes.add(newSb.toString());
newSb.setLength(0);
}
setField(executor, "deletes", deletes);
}
}
}
// Prepare queryTranslator for aggregated parameters
ParameterTranslations translations = new ParameterTranslationsImpl(queryParameterSpecifications);
setField(queryTranslator, "paramTranslations", translations);
setField(queryTranslator, "collectedParameterSpecifications", queryParameterSpecifications);
return queryTranslator;
} catch (Exception e1) {
throw new RuntimeException(e1);
}
}
private int getCTEEnd(String sql, int start) {
int parenthesis = 0;
QuoteMode mode = QuoteMode.NONE;
boolean started = false;
int i = start;
int end = sql.length();
OUTER: while (i < end) {
final char c = sql.charAt(i);
mode = mode.onChar(c);
if (mode == QuoteMode.NONE) {
if (c == '(') {
started = true;
parenthesis++;
} else if (c == ')') {
parenthesis--;
} else if (started && parenthesis == 0 && c != ',' && !Character.isWhitespace(c)) {
for (String statementType : KNOWN_STATEMENTS) {
if (sql.regionMatches(true, i, statementType, 0, statementType.length())) {
break OUTER;
}
}
}
}
i++;
}
return i;
}
private CacheEntry<HQLQueryPlan> getQueryPlan(SessionFactoryImplementor sfi, Query query, QueryPlanCacheKey cacheKey) {
BoundedConcurrentHashMap<QueryPlanCacheKey, HQLQueryPlan> queryPlanCache = getQueryPlanCache(sfi);
HQLQueryPlan queryPlan = queryPlanCache.get(cacheKey);
boolean fromCache = true;
if (queryPlan == null) {
fromCache = false;
queryPlan = createQueryPlan(sfi, query);
}
return new CacheEntry<HQLQueryPlan>(queryPlan, fromCache);
}
private HQLQueryPlan putQueryPlanIfAbsent(SessionFactoryImplementor sfi, QueryPlanCacheKey cacheKey, HQLQueryPlan queryPlan) {
BoundedConcurrentHashMap<QueryPlanCacheKey, HQLQueryPlan> queryPlanCache = getQueryPlanCache(sfi);
HQLQueryPlan oldQueryPlan = queryPlanCache.putIfAbsent(cacheKey, queryPlan);
if (oldQueryPlan != null) {
queryPlan = oldQueryPlan;
}
return queryPlan;
}
private HQLQueryPlan createQueryPlan(SessionFactoryImplementor sfi, Query query) {
org.hibernate.Query hibernateQuery = query.unwrap(org.hibernate.Query.class);
String queryString = hibernateQuery.getQueryString();
return new HQLQueryPlan(queryString, false, Collections.EMPTY_MAP, sfi);
}
private BoundedConcurrentHashMap<QueryPlanCacheKey, HQLQueryPlan> getQueryPlanCache(SessionFactoryImplementor sfi) {
BoundedConcurrentHashMap<QueryPlanCacheKey, HQLQueryPlan> queryPlanCache = queryPlanCachesCache.get(sfi);
if (queryPlanCache == null) {
queryPlanCache = new BoundedConcurrentHashMap<QueryPlanCacheKey, HQLQueryPlan>(QueryPlanCache.DEFAULT_QUERY_PLAN_MAX_COUNT, 20, BoundedConcurrentHashMap.Eviction.LIRS);
BoundedConcurrentHashMap<QueryPlanCacheKey, HQLQueryPlan> oldQueryPlanCache = queryPlanCachesCache.putIfAbsent(sfi, queryPlanCache);
if (oldQueryPlanCache != null) {
queryPlanCache = oldQueryPlanCache;
}
}
return queryPlanCache;
}
private QueryPlanCacheKey createCacheKey(List<String> queries) {
return createCacheKey(queries, null, null);
}
private QueryPlanCacheKey createCacheKey(List<String> queries, Integer firstResult, Integer maxResults) {
return new QueryPlanCacheKey(queries, firstResult, maxResults);
}
private void addAll(List<Query> queries, List<String> parts) {
for (int i = 0; i < queries.size(); i++) {
Query query = queries.get(i);
if (query instanceof CteQueryWrapper) {
addAll(((CteQueryWrapper) query).getParticipatingQueries(), parts);
} else {
parts.add(query.unwrap(org.hibernate.Query.class).getQueryString());
}
}
}
private static class QueryParamEntry {
final String queryString;
final QueryParameters queryParameters;
final List<ParameterSpecification> specifications;
public QueryParamEntry(String queryString, QueryParameters queryParameters, List<ParameterSpecification> specifications) {
this.queryString = queryString;
this.queryParameters = queryParameters;
this.specifications = specifications;
}
}
private static class QueryPlanCacheKey {
final List<String> cacheKeyParts;
final Integer firstResult;
final Integer maxResults;
public QueryPlanCacheKey(List<String> cacheKeyParts) {
this.cacheKeyParts = cacheKeyParts;
this.firstResult = null;
this.maxResults = null;
}
public QueryPlanCacheKey(List<String> cacheKeyParts, Integer firstResult, Integer maxResults) {
this.cacheKeyParts = cacheKeyParts;
this.firstResult = firstResult;
this.maxResults = maxResults;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof QueryPlanCacheKey)) {
return false;
}
QueryPlanCacheKey that = (QueryPlanCacheKey) o;
if (!cacheKeyParts.equals(that.cacheKeyParts)) {
return false;
}
if (firstResult != null ? !firstResult.equals(that.firstResult) : that.firstResult != null) {
return false;
}
return maxResults != null ? maxResults.equals(that.maxResults) : that.maxResults == null;
}
@Override
public int hashCode() {
int result = cacheKeyParts.hashCode();
result = 31 * result + (firstResult != null ? firstResult.hashCode() : 0);
result = 31 * result + (maxResults != null ? maxResults.hashCode() : 0);
return result;
}
}
private static class CacheEntry<T> {
private final T value;
private final boolean fromCache;
public CacheEntry(T value, boolean fromCache) {
this.value = value;
this.fromCache = fromCache;
}
public T getValue() {
return value;
}
public boolean isFromCache() {
return fromCache;
}
}
@SuppressWarnings("unchecked")
private <T> T getField(Object object, String field) {
boolean madeAccessible = false;
Field f = null;
try {
f = ReflectionUtils.getField(object.getClass(), field);
madeAccessible = !f.isAccessible();
if (madeAccessible) {
f.setAccessible(true);
}
return (T) f.get(object);
} catch (Exception e1) {
throw new RuntimeException(e1);
} finally {
if (madeAccessible) {
f.setAccessible(false);
}
}
}
private void setField(Object object, String field, Object value) {
setField(object, object.getClass(), field, value);
}
private void setField(Object object, Class<?> clazz, String field, Object value) {
boolean madeAccessible = false;
Field f = null;
try {
f = ReflectionUtils.getField(clazz, field);
madeAccessible = !f.isAccessible();
if (madeAccessible) {
f.setAccessible(true);
}
f.set(object, value);
} catch (Exception e1) {
throw new RuntimeException(e1);
} finally {
if (madeAccessible) {
f.setAccessible(false);
}
}
}
}