/*
* 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;
import com.blazebit.persistence.BaseCTECriteriaBuilder;
import com.blazebit.persistence.SelectBuilder;
import com.blazebit.persistence.impl.expression.Expression;
import com.blazebit.persistence.impl.expression.NullExpression;
import com.blazebit.persistence.impl.expression.PathExpression;
import com.blazebit.persistence.impl.expression.PropertyExpression;
import com.blazebit.persistence.impl.query.CTEQuerySpecification;
import com.blazebit.persistence.impl.query.CustomSQLQuery;
import com.blazebit.persistence.impl.query.EntityFunctionNode;
import com.blazebit.persistence.impl.query.QuerySpecification;
import com.blazebit.persistence.spi.DbmsStatementType;
import com.blazebit.persistence.spi.SetOperationType;
import javax.persistence.Query;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*
* @param <Y> The criteria builder returned after the cte builder
* @param <X> The concrete builder type
* @param <Z> The builder type that should be returned on set operations
* @author Christian Beikov
* @since 1.1.0
*/
public abstract class AbstractCTECriteriaBuilder<Y, X extends BaseCTECriteriaBuilder<X>, Z, W> extends AbstractCommonQueryBuilder<Object, X, Z, W, BaseFinalSetOperationCTECriteriaBuilderImpl<Object, ?>> implements BaseCTECriteriaBuilder<X>, SelectBuilder<X>, CTEInfoBuilder {
protected static final Integer EMPTY = Integer.valueOf(-1);
protected final Y result;
protected final CTEBuilderListener listener;
protected final String cteName;
protected final EntityType<?> cteType;
protected final Map<String, Map.Entry<AttributePath, String[]>> attributeColumnMappings;
protected final Map<String, Integer> bindingMap;
protected final Map<String, String> columnBindingMap;
protected final CTEBuilderListenerImpl subListener;
private CTEInfo info;
public AbstractCTECriteriaBuilder(MainQuery mainQuery, String cteName, Class<Object> clazz, Y result, CTEBuilderListener listener, BaseFinalSetOperationCTECriteriaBuilderImpl<Object, ?> finalSetOperationBuilder) {
super(mainQuery, false, DbmsStatementType.SELECT, clazz, null, finalSetOperationBuilder, false);
this.result = result;
this.listener = listener;
this.cteType = mainQuery.metamodel.entity(clazz);
this.attributeColumnMappings = mainQuery.metamodel.getAttributeColumnNameMapping(clazz);
this.cteName = cteName;
this.bindingMap = new LinkedHashMap<String, Integer>(attributeColumnMappings.size());
this.columnBindingMap = new LinkedHashMap<String, String>(attributeColumnMappings.size());
this.subListener = new CTEBuilderListenerImpl();
}
public CTEBuilderListenerImpl getSubListener() {
return subListener;
}
@Override
protected void buildExternalQueryString(StringBuilder sbSelectFrom) {
buildBaseQueryString(sbSelectFrom, true);
}
@Override
protected Query getQuery() {
// NOTE: This must happen first because it generates implicit joins
String baseQueryString = getBaseQueryStringWithCheck();
Set<JoinNode> keyRestrictedLeftJoins = joinManager.getKeyRestrictedLeftJoins();
Query query;
if (hasLimit() || joinManager.hasEntityFunctions() || !keyRestrictedLeftJoins.isEmpty()) {
// We need to change the underlying sql when doing a limit
query = em.createQuery(baseQueryString);
Set<String> parameterListNames = parameterManager.getParameterListNames(query);
String limit = null;
String offset = null;
// The main query will handle that separately
if (!isMainQuery) {
if (firstResult != 0) {
offset = Integer.toString(firstResult);
}
if (maxResults != Integer.MAX_VALUE) {
limit = Integer.toString(maxResults);
}
}
List<String> keyRestrictedLeftJoinAliases = getKeyRestrictedLeftJoinAliases(query, keyRestrictedLeftJoins, Collections.EMPTY_SET);
List<EntityFunctionNode> entityFunctionNodes = getEntityFunctionNodes(query);
QuerySpecification querySpecification = new CTEQuerySpecification(
this,
query,
parameterListNames,
limit,
offset,
keyRestrictedLeftJoinAliases,
entityFunctionNodes
);
query = new CustomSQLQuery(
querySpecification,
query,
parameterManager.getValuesParameters(),
parameterManager.getValuesBinders()
);
} else {
query = em.createQuery(baseQueryString);
}
parameterManager.parameterizeQuery(query);
return query;
}
public SelectBuilder<X> bind(String cteAttribute) {
Map.Entry<AttributePath, String[]> attributeEntry = attributeColumnMappings.get(cteAttribute);
if (attributeEntry == null) {
if (cteType.getAttribute(cteAttribute) != null) {
throw new IllegalArgumentException("Can't bind the embeddable cte attribute [" + cteAttribute + "] directly! Please bind the respective sub attributes.");
}
throw new IllegalArgumentException("The cte attribute [" + cteAttribute + "] does not exist!");
}
if (bindingMap.put(cteAttribute, selectManager.getSelectInfos().size()) != null) {
throw new IllegalArgumentException("The cte attribute [" + cteAttribute + "] has already been bound!");
}
for (String column : attributeEntry.getValue()) {
if (columnBindingMap.put(column, cteAttribute) != null) {
throw new IllegalArgumentException("The cte column [" + column + "] has already been bound!");
}
}
return this;
}
public Y end() {
listener.onBuilderEnded(this);
return result;
}
@Override
protected void prepareAndCheck() {
if (!needsCheck) {
return;
}
try {
List<String> attributes = prepareAndGetAttributes();
List<String> columns = prepareAndGetColumnNames();
super.prepareAndCheck();
info = new CTEInfo(cteName, cteType, attributes, columns, false, false, this, null);
} catch (RuntimeException ex) {
needsCheck = true;
throw ex;
}
}
public CTEInfo createCTEInfo() {
prepareAndCheck();
return info;
}
protected List<String> prepareAndGetAttributes() {
List<String> attributes = new ArrayList<String>(bindingMap.size());
for (Map.Entry<String, Integer> bindingEntry : bindingMap.entrySet()) {
final String attributeName = bindingEntry.getKey();
AttributePath attributePath = attributeColumnMappings.get(attributeName).getKey();
attributes.add(attributeName);
if (JpaUtils.isJoinable(attributePath.getAttributes().get(attributePath.getAttributes().size() - 1))) {
// We have to map *-to-one relationships to their ids
EntityType<?> type = mainQuery.metamodel.entity(attributePath.getAttributeClass());
Attribute<?, ?> idAttribute = JpaUtils.getIdAttribute(type);
// NOTE: Since we are talking about *-to-ones, the expression can only be a path to an object
// so it is safe to just append the id to the path
Expression selectExpression = selectManager.getSelectInfos().get(bindingEntry.getValue()).getExpression();
// TODO: Maybe also allow Treat, Case-When, Array?
if (selectExpression instanceof NullExpression) {
// When binding null, we don't have to adapt anything
} else if (selectExpression instanceof PathExpression) {
PathExpression pathExpression = (PathExpression) selectExpression;
// Only append the id if it's not already there
if (!idAttribute.getName().equals(pathExpression.getExpressions().get(pathExpression.getExpressions().size() - 1).toString())) {
pathExpression.getExpressions().add(new PropertyExpression(idAttribute.getName()));
}
} else {
throw new IllegalArgumentException("Illegal expression '" + selectExpression.toString() + "' for binding relation '" + attributeName + "'!");
}
}
}
return attributes;
}
protected List<String> prepareAndGetColumnNames() {
StringBuilder sb = null;
for (Map.Entry<AttributePath, String[]> entry : attributeColumnMappings.values()) {
for (String column : entry.getValue()) {
if (!columnBindingMap.containsKey(column)) {
if (sb == null) {
sb = new StringBuilder();
sb.append("[");
} else {
sb.append(", ");
}
sb.append(column);
}
}
}
if (sb != null) {
sb.insert(0, "The following column names have not been bound: ");
sb.append("]");
throw new IllegalStateException(sb.toString());
}
return new ArrayList<>(columnBindingMap.keySet());
}
protected BaseFinalSetOperationCTECriteriaBuilderImpl<Object, ?> createFinalSetOperationBuilder(SetOperationType operator, boolean nested, boolean isSubquery) {
FullSelectCTECriteriaBuilderImpl<?> newInitiator = finalSetOperationBuilder == null ? null : finalSetOperationBuilder.getInitiator();
return createFinalSetOperationBuilder(operator, nested, isSubquery, newInitiator);
}
@SuppressWarnings("unchecked")
protected BaseFinalSetOperationCTECriteriaBuilderImpl<Object, ?> createFinalSetOperationBuilder(SetOperationType operator, boolean nested, boolean isSubquery, FullSelectCTECriteriaBuilderImpl<?> initiator) {
CTEBuilderListener newListener = finalSetOperationBuilder == null ? listener : finalSetOperationBuilder.getSubListener();
Y newResult = finalSetOperationBuilder == null ? result : (Y) finalSetOperationBuilder.getResult();
if (isSubquery) {
return new OngoingFinalSetOperationCTECriteriaBuilderImpl<Object>(mainQuery, (Class<Object>) cteType.getJavaType(), newResult, operator, nested, newListener, initiator);
} else {
return new FinalSetOperationCTECriteriaBuilderImpl<Object>(mainQuery, (Class<Object>) cteType.getJavaType(), newResult, operator, nested, newListener, initiator);
}
}
@SuppressWarnings("unchecked")
protected LeafOngoingSetOperationCTECriteriaBuilderImpl<Y> createLeaf(BaseFinalSetOperationCTECriteriaBuilderImpl<Object, ?> finalSetOperationBuilder) {
CTEBuilderListener newListener = finalSetOperationBuilder.getSubListener();
LeafOngoingSetOperationCTECriteriaBuilderImpl<Y> next = new LeafOngoingSetOperationCTECriteriaBuilderImpl<Y>(mainQuery, cteName, (Class<Object>) cteType.getJavaType(), result, newListener, (FinalSetOperationCTECriteriaBuilderImpl<Object>) finalSetOperationBuilder);
newListener.onBuilderStarted(next);
return next;
}
@SuppressWarnings("unchecked")
protected <T extends AbstractCommonQueryBuilder<?, ?, ?, ?, ?>> StartOngoingSetOperationCTECriteriaBuilderImpl<Y, T> createStartOngoing(BaseFinalSetOperationCTECriteriaBuilderImpl<Object, ?> finalSetOperationBuilder, T endSetResult) {
// TODO: This is such an ugly hack, but I don't know how else to fix this generics issue for now
finalSetOperationBuilder.setEndSetResult((T) endSetResult);
CTEBuilderListener newListener = finalSetOperationBuilder.getSubListener();
StartOngoingSetOperationCTECriteriaBuilderImpl<Y, T> next = new StartOngoingSetOperationCTECriteriaBuilderImpl<Y, T>(mainQuery, cteName, (Class<Object>) cteType.getJavaType(), result, newListener, (OngoingFinalSetOperationCTECriteriaBuilderImpl<Object>) finalSetOperationBuilder, endSetResult);
newListener.onBuilderStarted(next);
return next;
}
@SuppressWarnings("unchecked")
protected <T extends AbstractCommonQueryBuilder<?, ?, ?, ?, ?>> OngoingSetOperationCTECriteriaBuilderImpl<Y, T> createOngoing(BaseFinalSetOperationCTECriteriaBuilderImpl<Object, ?> finalSetOperationBuilder, T endSetResult) {
// TODO: This is such an ugly hack, but I don't know how else to fix this generics issue for now
finalSetOperationBuilder.setEndSetResult((T) endSetResult);
CTEBuilderListener newListener = finalSetOperationBuilder.getSubListener();
OngoingSetOperationCTECriteriaBuilderImpl<Y, T> next = new OngoingSetOperationCTECriteriaBuilderImpl<Y, T>(mainQuery, cteName, (Class<Object>) cteType.getJavaType(), result, newListener, (OngoingFinalSetOperationCTECriteriaBuilderImpl<Object>) finalSetOperationBuilder, endSetResult);
newListener.onBuilderStarted(next);
return next;
}
}