/*
* Copyright (c) 2010-2015 Evolveum
*
* 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.evolveum.midpoint.repo.sql.query2;
import com.evolveum.midpoint.prism.Containerable;
import com.evolveum.midpoint.prism.ItemDefinition;
import com.evolveum.midpoint.prism.PrismContext;
import com.evolveum.midpoint.prism.path.ItemPath;
import com.evolveum.midpoint.prism.polystring.PolyString;
import com.evolveum.midpoint.prism.query.*;
import com.evolveum.midpoint.repo.sql.ObjectPagingAfterOid;
import com.evolveum.midpoint.repo.sql.SqlRepositoryConfiguration;
import com.evolveum.midpoint.repo.sql.data.common.embedded.RPolyString;
import com.evolveum.midpoint.repo.sql.query.QueryException;
import com.evolveum.midpoint.repo.sql.query2.definition.*;
import com.evolveum.midpoint.repo.sql.query2.hqm.CountProjectionElement;
import com.evolveum.midpoint.repo.sql.query2.hqm.RootHibernateQuery;
import com.evolveum.midpoint.repo.sql.query2.hqm.condition.Condition;
import com.evolveum.midpoint.repo.sql.query2.matcher.DefaultMatcher;
import com.evolveum.midpoint.repo.sql.query2.matcher.Matcher;
import com.evolveum.midpoint.repo.sql.query2.matcher.PolyStringMatcher;
import com.evolveum.midpoint.repo.sql.query2.matcher.StringMatcher;
import com.evolveum.midpoint.repo.sql.query2.resolution.ItemPathResolver;
import com.evolveum.midpoint.repo.sql.query2.resolution.ProperDataSearchResult;
import com.evolveum.midpoint.repo.sql.query2.restriction.*;
import com.evolveum.midpoint.repo.sql.util.GetCertificationWorkItemResult;
import com.evolveum.midpoint.repo.sql.util.GetContainerableResult;
import com.evolveum.midpoint.repo.sql.util.GetObjectResult;
import com.evolveum.midpoint.repo.sql.util.ResultStyle;
import com.evolveum.midpoint.schema.GetOperationOptions;
import com.evolveum.midpoint.schema.SelectorOptions;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
import com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationCaseType;
import com.evolveum.midpoint.xml.ns._public.common.common_3.AccessCertificationWorkItemType;
import org.hibernate.Session;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.stream.Collectors;
/**
* Interprets midPoint queries by translating them to hibernate (HQL) ones.
* <p/>
* There are two parts:
* - filter translation,
* - paging translation.
* <p/>
* As for filter translation, we traverse the filter depth-first, creating an isomorphic structure of Restrictions.
* While creating them, we continually build a set of entity references that are necessary to evaluate the query;
* these references are in the form of cartesian join of entities from which each one can have a set of entities
* connected to it via left outer join. An example:
* <p/>
* from
* RUser u
* left join u.assignments a with ...
* left join u.organization o,
* RRole r
* left join r.assignments a2 with ...
* <p/>
* This structure is maintained in InterpretationContext, namely in the HibernateQuery being prepared. (In order to
* produce HQL, we use ad-hoc "hibernate query model" in hqm package, rooted in HibernateQuery class.)
* <p/>
* Paging translation is done after filters are translated. It may add some entity references as well, if they are not
* already present.
*
* @author lazyman
* @author mederly
*/
public class QueryInterpreter2 {
private static final Trace LOGGER = TraceManager.getTrace(QueryInterpreter2.class);
private static final Map<Class, Matcher> AVAILABLE_MATCHERS;
static {
Map<Class, Matcher> matchers = new HashMap<>();
//default matcher with null key
matchers.put(null, new DefaultMatcher());
matchers.put(PolyString.class, new PolyStringMatcher());
matchers.put(String.class, new StringMatcher());
AVAILABLE_MATCHERS = Collections.unmodifiableMap(matchers);
}
private SqlRepositoryConfiguration repoConfiguration;
public QueryInterpreter2(SqlRepositoryConfiguration repoConfiguration) {
this.repoConfiguration = repoConfiguration;
}
public SqlRepositoryConfiguration getRepoConfiguration() {
return repoConfiguration;
}
public RootHibernateQuery interpret(ObjectQuery query, @NotNull Class<? extends Containerable> type,
Collection<SelectorOptions<GetOperationOptions>> options, @NotNull PrismContext prismContext,
boolean countingObjects, @NotNull Session session) throws QueryException {
boolean distinct = GetOperationOptions.isDistinct(SelectorOptions.findRootOptions(options));
LOGGER.trace("Interpreting query for type '{}' (counting={}, distinct={}), query:\n{}", type, countingObjects, distinct, query);
InterpretationContext context = new InterpretationContext(this, type, prismContext, session);
interpretQueryFilter(context, query);
String rootAlias = context.getHibernateQuery().getPrimaryEntityAlias();
ResultStyle resultStyle = getResultStyle(context);
if (countingObjects) {
interpretPagingAndSorting(context, query, true);
RootHibernateQuery hibernateQuery = context.getHibernateQuery();
hibernateQuery.addProjectionElement(new CountProjectionElement(resultStyle.getIdentifiers(rootAlias), distinct));
return hibernateQuery;
}
/*
Some databases don't support DISTINCT on BLOBs. In these cases we have to create query like:
select
u.oid, u.fullObject, u.stringsCount, ..., u.booleansCount
from
RUser u
where
u.oid in (select distinct u.oid from RUser u where ...)
*/
boolean distinctBlobCapable = !repoConfiguration.isUsingOracle() && !repoConfiguration.isUsingSQLServer();
RootHibernateQuery hibernateQuery = context.getHibernateQuery();
hibernateQuery.setDistinct(distinct);
hibernateQuery.addProjectionElementsFor(resultStyle.getIdentifiers(rootAlias));
if (distinct && !distinctBlobCapable) {
String subqueryText = "\n" + hibernateQuery.getAsHqlText(2, true);
InterpretationContext wrapperContext = new InterpretationContext(this, type, prismContext, session);
interpretPagingAndSorting(wrapperContext, query, false);
RootHibernateQuery wrapperQuery = wrapperContext.getHibernateQuery();
String wrappedRootAlias = wrapperQuery.getPrimaryEntityAlias();
wrapperQuery.setResultTransformer(resultStyle.getResultTransformer());
wrapperQuery.addProjectionElementsFor(resultStyle.getIdentifiers(wrappedRootAlias));
wrapperQuery.addProjectionElementsFor(resultStyle.getContentAttributes(wrappedRootAlias));
wrapperQuery.getConditions().add(
wrapperQuery.createIn(wrapperQuery.getPrimaryEntityAlias() + ".oid", subqueryText));
wrapperQuery.addParametersFrom(hibernateQuery.getParameters());
return wrapperQuery;
} else {
interpretPagingAndSorting(context, query, false);
hibernateQuery.setResultTransformer(resultStyle.getResultTransformer());
hibernateQuery.addProjectionElementsFor(resultStyle.getContentAttributes(rootAlias));
if (distinct) {
hibernateQuery.addProjectionElementsFor(getOrderingAttributes(context)); // SQL requires this
}
return hibernateQuery;
}
}
private List<String> getOrderingAttributes(InterpretationContext context) {
return context.getHibernateQuery().getOrderingList().stream().map(o -> o.getByProperty()).collect(Collectors.toList());
}
private ResultStyle getResultStyle(InterpretationContext context) throws QueryException {
if (context.isObject()) {
return GetObjectResult.RESULT_STYLE;
} else if (AccessCertificationCaseType.class.equals(context.getType())) {
return GetContainerableResult.RESULT_STYLE;
} else if (AccessCertificationWorkItemType.class.equals(context.getType())) {
return GetCertificationWorkItemResult.RESULT_STYLE;
} else {
throw new QueryException("Unsupported type: " + context.getType());
}
}
private void interpretQueryFilter(InterpretationContext context, ObjectQuery query) throws QueryException {
if (query != null && query.getFilter() != null) {
Condition c = interpretFilter(context, query.getFilter(), null);
context.getHibernateQuery().addCondition(c);
}
}
public Condition interpretFilter(InterpretationContext context, ObjectFilter filter, Restriction parent) throws QueryException {
Restriction restriction = findAndCreateRestriction(filter, context, parent);
return restriction.interpret();
}
private <T extends ObjectFilter> Restriction findAndCreateRestriction(@NotNull T filter,
@NotNull InterpretationContext context, Restriction parent) throws QueryException {
LOGGER.trace("Determining restriction for filter {}", filter);
ItemPathResolver helper = context.getItemPathResolver();
JpaEntityDefinition baseEntityDefinition;
if (parent != null) {
baseEntityDefinition = parent.getBaseHqlEntityForChildren().getJpaDefinition();
} else {
baseEntityDefinition = context.getRootEntityDefinition();
}
Restriction restriction = findAndCreateRestrictionInternal(filter, context, parent, helper, baseEntityDefinition);
LOGGER.trace("Restriction for {} is {}", filter.getClass().getSimpleName(), restriction);
return restriction;
}
private <T extends ObjectFilter>
Restriction findAndCreateRestrictionInternal(T filter, InterpretationContext context, Restriction parent,
ItemPathResolver resolver, JpaEntityDefinition baseEntityDefinition) throws QueryException {
// the order of processing restrictions can be important, so we do the selection via handwritten code
if (filter instanceof AndFilter) {
return new AndRestriction(context, (AndFilter) filter, baseEntityDefinition, parent);
} else if (filter instanceof OrFilter) {
return new OrRestriction(context, (OrFilter) filter, baseEntityDefinition, parent);
} else if (filter instanceof NotFilter) {
return new NotRestriction(context, (NotFilter) filter, baseEntityDefinition, parent);
} else if (filter instanceof FullTextFilter) {
return new FullTextRestriction(context, (FullTextFilter) filter, baseEntityDefinition, parent);
} else if (filter instanceof InOidFilter) {
return new InOidRestriction(context, (InOidFilter) filter, baseEntityDefinition, parent);
} else if (filter instanceof OrgFilter) {
return new OrgRestriction(context, (OrgFilter) filter, baseEntityDefinition, parent);
} else if (filter instanceof TypeFilter) {
TypeFilter typeFilter = (TypeFilter) filter;
JpaEntityDefinition refinedEntityDefinition = resolver.findRestrictedEntityDefinition(baseEntityDefinition, typeFilter.getType());
return new TypeRestriction(context, typeFilter, refinedEntityDefinition, parent);
} else if (filter instanceof ExistsFilter) {
ExistsFilter existsFilter = (ExistsFilter) filter;
ItemPath path = existsFilter.getFullPath();
ItemDefinition definition = existsFilter.getDefinition();
ProperDataSearchResult<JpaDataNodeDefinition> searchResult = resolver.findProperDataDefinition(
baseEntityDefinition, path, definition, JpaDataNodeDefinition.class, context.getPrismContext());
if (searchResult == null) {
throw new QueryException("Path for ExistsFilter (" + path + ") doesn't point to a hibernate entity or property within " + baseEntityDefinition);
}
return new ExistsRestriction(context, existsFilter, searchResult.getEntityDefinition(), parent);
} else if (filter instanceof RefFilter) {
RefFilter refFilter = (RefFilter) filter;
ItemPath path = refFilter.getFullPath();
ItemDefinition definition = refFilter.getDefinition();
ProperDataSearchResult<JpaReferenceDefinition> searchResult = resolver.findProperDataDefinition(
baseEntityDefinition, path, definition, JpaReferenceDefinition.class, context.getPrismContext());
if (searchResult == null) {
throw new QueryException("Path for RefFilter (" + path + ") doesn't point to a reference item within " + baseEntityDefinition);
}
return new ReferenceRestriction(context, refFilter, searchResult.getEntityDefinition(),
parent, searchResult.getLinkDefinition());
} else if (filter instanceof PropertyValueFilter) {
PropertyValueFilter valFilter = (PropertyValueFilter) filter;
ItemPath path = valFilter.getFullPath();
ItemDefinition definition = valFilter.getDefinition();
ProperDataSearchResult<JpaPropertyDefinition> propDefRes = resolver.findProperDataDefinition(baseEntityDefinition, path, definition, JpaPropertyDefinition.class,
context.getPrismContext());
if (propDefRes == null) {
throw new QueryException("Couldn't find a proper data item to query, given base entity " + baseEntityDefinition + " and this filter: " + valFilter.debugDump());
}
// TODO can't be unified?
if (propDefRes.getTargetDefinition() instanceof JpaAnyPropertyDefinition) {
return new AnyPropertyRestriction(context, valFilter, propDefRes.getEntityDefinition(), parent, propDefRes.getLinkDefinition());
} else {
return new PropertyRestriction(context, valFilter, propDefRes.getEntityDefinition(), parent, propDefRes.getLinkDefinition());
}
} else if (filter instanceof NoneFilter || filter instanceof AllFilter || filter instanceof UndefinedFilter) {
// these should be filtered out by the client
throw new IllegalStateException("Trivial filters are not supported by QueryInterpreter: " + filter.debugDump());
} else {
throw new IllegalStateException("Unknown filter: " + filter.debugDump());
}
}
private void interpretPagingAndSorting(InterpretationContext context, ObjectQuery query, boolean countingObjects) throws QueryException {
RootHibernateQuery hibernateQuery = context.getHibernateQuery();
String rootAlias = hibernateQuery.getPrimaryEntityAlias();
if (query != null && query.getPaging() instanceof ObjectPagingAfterOid) {
ObjectPagingAfterOid paging = (ObjectPagingAfterOid) query.getPaging();
if (paging.getOidGreaterThan() != null) {
Condition c = hibernateQuery.createSimpleComparisonCondition(rootAlias + ".oid", paging.getOidGreaterThan(), ">");
hibernateQuery.addCondition(c);
}
}
if (!countingObjects && query != null && query.getPaging() != null) {
if (query.getPaging() instanceof ObjectPagingAfterOid) {
updatePagingAndSortingByOid(hibernateQuery, (ObjectPagingAfterOid) query.getPaging()); // very special case - ascending ordering by OID (nothing more)
} else {
updatePagingAndSorting(context, query.getPaging());
}
}
}
private void updatePagingAndSortingByOid(RootHibernateQuery hibernateQuery, ObjectPagingAfterOid paging) {
String rootAlias = hibernateQuery.getPrimaryEntityAlias();
if (paging.getOrderBy() != null || paging.getDirection() != null || paging.getOffset() != null) {
throw new IllegalArgumentException("orderBy, direction nor offset is allowed on ObjectPagingAfterOid");
}
hibernateQuery.addOrdering(rootAlias + ".oid", OrderDirection.ASCENDING);
if (paging.getMaxSize() != null) {
hibernateQuery.setMaxResults(paging.getMaxSize());
}
}
private void updatePagingAndSorting(InterpretationContext context, ObjectPaging paging) throws QueryException {
if (paging == null) {
return;
}
RootHibernateQuery hibernateQuery = context.getHibernateQuery();
if (paging.getOffset() != null) {
hibernateQuery.setFirstResult(paging.getOffset());
}
if (paging.getMaxSize() != null) {
hibernateQuery.setMaxResults(paging.getMaxSize());
}
if (!paging.hasOrdering()) {
return;
}
for (ObjectOrdering ordering : paging.getOrderingInstructions()) {
addOrdering(context, ordering);
}
}
private void addOrdering(InterpretationContext context, ObjectOrdering ordering) throws QueryException {
ItemPath orderByPath = ordering.getOrderBy();
// TODO if we'd like to have order-by extension properties, we'd need to provide itemDefinition for them
ProperDataSearchResult<JpaDataNodeDefinition> result = context.getItemPathResolver().findProperDataDefinition(
context.getRootEntityDefinition(), orderByPath, null, JpaDataNodeDefinition.class, context.getPrismContext());
if (result == null) {
LOGGER.error("Unknown path '" + orderByPath + "', couldn't find definition for it, "
+ "list will not be ordered by it.");
return;
}
JpaDataNodeDefinition targetDefinition = result.getLinkDefinition().getTargetDefinition();
if (targetDefinition instanceof JpaAnyContainerDefinition) {
throw new QueryException("Sorting based on extension item or attribute is not supported yet: " + orderByPath);
} else if (targetDefinition instanceof JpaReferenceDefinition) {
throw new QueryException("Sorting based on reference is not supported: " + orderByPath);
} else if (result.getLinkDefinition().isMultivalued()) {
throw new QueryException("Sorting based on multi-valued item is not supported: " + orderByPath);
} else if (targetDefinition instanceof JpaEntityDefinition) {
throw new QueryException("Sorting based on entity is not supported: " + orderByPath);
} else if (!(targetDefinition instanceof JpaPropertyDefinition)) {
throw new IllegalStateException("Unknown item definition type: " + result.getClass());
}
JpaEntityDefinition baseEntityDefinition = result.getEntityDefinition();
JpaPropertyDefinition orderByDefinition = (JpaPropertyDefinition) targetDefinition;
String hqlPropertyPath = context.getItemPathResolver()
.resolveItemPath(orderByPath, null, context.getPrimaryEntityAlias(), baseEntityDefinition, true)
.getHqlPath();
if (RPolyString.class.equals(orderByDefinition.getJpaClass())) {
hqlPropertyPath += ".orig";
}
RootHibernateQuery hibernateQuery = context.getHibernateQuery();
if (ordering.getDirection() != null) {
switch (ordering.getDirection()) {
case ASCENDING:
hibernateQuery.addOrdering(hqlPropertyPath, OrderDirection.ASCENDING);
break;
case DESCENDING:
hibernateQuery.addOrdering(hqlPropertyPath, OrderDirection.DESCENDING);
break;
}
} else {
hibernateQuery.addOrdering(hqlPropertyPath, OrderDirection.ASCENDING);
}
}
public <T> Matcher<T> findMatcher(T value) {
return findMatcher(value != null ? (Class<T>) value.getClass() : null);
}
public <T> Matcher<T> findMatcher(Class<T> type) {
Matcher<T> matcher = AVAILABLE_MATCHERS.get(type);
if (matcher == null) {
//we return default matcher
matcher = AVAILABLE_MATCHERS.get(null);
}
return matcher;
}
}