/*
* Copyright 2012 JBoss Inc
*
* 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 org.artificer.repository.hibernate.query;
import java.math.BigInteger;
import java.net.URI;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.MapJoin;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Subquery;
import javax.xml.namespace.QName;
import org.apache.commons.lang.StringUtils;
import org.artificer.common.ArtifactType;
import org.artificer.common.ArtificerConstants;
import org.artificer.common.ArtificerException;
import org.artificer.common.query.ArtifactSummary;
import org.artificer.common.query.xpath.ast.AndExpr;
import org.artificer.common.query.xpath.ast.Argument;
import org.artificer.common.query.xpath.ast.EqualityExpr;
import org.artificer.common.query.xpath.ast.ForwardPropertyStep;
import org.artificer.common.query.xpath.ast.FunctionCall;
import org.artificer.common.query.xpath.ast.LocationPath;
import org.artificer.common.query.xpath.ast.OrExpr;
import org.artificer.common.query.xpath.ast.PrimaryExpr;
import org.artificer.common.query.xpath.ast.Query;
import org.artificer.common.query.xpath.ast.RelationshipPath;
import org.artificer.common.query.xpath.ast.SubartifactSet;
import org.artificer.repository.ClassificationHelper;
import org.artificer.repository.error.QueryExecutionException;
import org.artificer.repository.hibernate.data.HibernateEntityCreator;
import org.artificer.repository.hibernate.entity.ArtificerArtifact;
import org.artificer.repository.hibernate.entity.ArtificerRelationship;
import org.artificer.repository.hibernate.entity.ArtificerTarget;
import org.artificer.repository.hibernate.i18n.Messages;
import org.artificer.repository.query.AbstractArtificerQueryVisitor;
import org.artificer.repository.query.ArtificerQueryArgs;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.query.dsl.BooleanJunction;
import org.hibernate.search.query.dsl.QueryBuilder;
/**
* Visitor used to produce a JSQL query from an S-RAMP xpath query.
*
* @author Brett Meyer
*/
public class ArtificerToHibernateQueryVisitor extends AbstractArtificerQueryVisitor {
private final EntityManager entityManager;
private final CriteriaBuilder criteriaBuilder;
private CriteriaQuery query = null;
private From from = null;
private String artifactModel = null;
private String artifactType = null;
private From relationshipFrom = null;
private From targetFrom = null;
private Subquery customPropertySubquery = null;
private Path customPropertyValuePath = null;
private List<Predicate> customPropertyPredicates = null;
private String propertyContext = null;
private Object valueContext = null;
private List<Predicate> predicates = new ArrayList<>();
private long totalSize;
private static final Map<QName, String> corePropertyMap = new HashMap<>();
static {
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "createdBy"), "createdBy.username");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "createdTimestamp"), "createdBy.lastActionTime");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "version"), "version");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "uuid"), "uuid");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "lastModifiedTimestamp"), "modifiedBy.lastActionTime");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "lastModifiedBy"), "modifiedBy.username");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "description"), "description");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "name"), "name");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "contentType"), "contentType");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "contentSize"), "contentSize");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "contentHash"), "contentHash");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "contentEncoding"), "contentEncoding");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "extendedType"), "extendedType");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "derived"), "derived");
corePropertyMap.put(new QName(ArtificerConstants.SRAMP_NS, "expandedFromArchive"), "expandedFromArchive");
}
private static final Map<String, String> orderByMap = new HashMap<String, String>();
static {
orderByMap.put("createdBy", "createdBy.username");
orderByMap.put("version", "version");
orderByMap.put("uuid", "uuid");
orderByMap.put("createdTimestamp", "createdBy.lastActionTime");
orderByMap.put("lastModifiedTimestamp", "modifiedBy.lastActionTime");
orderByMap.put("lastModifiedBy", "modifiedBy.username");
orderByMap.put("name", "name");
}
/**
* Default constructor.
* @param entityManager
* @param classificationHelper
*/
public ArtificerToHibernateQueryVisitor(EntityManager entityManager, ClassificationHelper classificationHelper) throws ArtificerException {
super(classificationHelper);
this.entityManager = entityManager;
criteriaBuilder = entityManager.getCriteriaBuilder();
}
/**
* Execute the query and return the results.
* @param args
* @return List<ArtificerArtifact>
* @throws ArtificerException
*/
public List<ArtifactSummary> query(ArtificerQueryArgs args) throws ArtificerException {
if (this.error != null) {
throw this.error;
}
// filter out the trash (have to do this here since 'from' can be overridden at several points in the visitor)
predicates.add(criteriaBuilder.equal(from.get("trashed"), Boolean.valueOf(false)));
// build the full set of constraints and
query.where(compileAnd(predicates));
// First, select the total count, without paging
query.select(criteriaBuilder.count(from)).distinct(true);
totalSize = (Long) entityManager.createQuery(query).getSingleResult();
// Setup the select. Note that we're only grabbing the fields we need for the summary.
query.multiselect(from.get("uuid"), from.get("name"), from.get("description"), from.get("model"),
from.get("type"), from.get("derived"), from.get("expandedFromArchive"),
from.get("createdBy").get("lastActionTime"), from.get("createdBy").get("username"),
from.get("modifiedBy").get("lastActionTime"))
.distinct(true);
if (args.getOrderBy() != null) {
String propName = orderByMap.get(args.getOrderBy());
if (propName != null) {
if (args.getOrderAscending()) {
query.orderBy(criteriaBuilder.asc(path(propName)));
} else {
query.orderBy(criteriaBuilder.desc(path(propName)));
}
}
}
TypedQuery q = entityManager.createQuery(query);
args.applyPaging(q);
q.unwrap(org.hibernate.Query.class).setCacheable(true);
return q.getResultList();
}
public long getTotalSize() {
return totalSize;
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.Query)
*/
@Override
public void visit(Query node) {
this.error = null;
query = criteriaBuilder.createQuery(ArtifactSummary.class);
from = query.from(ArtificerArtifact.class);
node.getArtifactSet().accept(this);
if (node.getPredicate() != null) {
node.getPredicate().accept(this);
}
if (node.getSubartifactSet() != null) {
SubartifactSet subartifactSet = node.getSubartifactSet();
if (subartifactSet.getRelationshipPath() != null) {
if (subartifactSet.getRelationshipPath().getRelationshipType().equalsIgnoreCase("relatedDocument")) {
// derivedFrom
from = from.join("derivedFrom");
// Now add any additional predicates included.
if (subartifactSet.getPredicate() != null) {
subartifactSet.getPredicate().accept(this);
}
} else if (subartifactSet.getRelationshipPath().getRelationshipType().equalsIgnoreCase("expandedFromDocument")
|| subartifactSet.getRelationshipPath().getRelationshipType().equalsIgnoreCase("expandedFromArchive")) {
// expandedFrom
from = from.join("expandedFrom");
// Now add any additional predicates included.
if (subartifactSet.getPredicate() != null) {
subartifactSet.getPredicate().accept(this);
}
} else {
// JOIN on the relationship and targets
relationshipFrom = from.join("relationships");
targetFrom = relationshipFrom.join("targets");
from = relationshipFrom;
// process constraints on the relationship itself
subartifactSet.getRelationshipPath().accept(this);
// root context now needs to be the relationship targets (permanently)
from = targetFrom.join("target");
// since the root context is now based on a target, we can no longer make assumptions about
// the model or type
artifactModel = null;
artifactType = null;
// Now add any additional predicates included.
if (subartifactSet.getPredicate() != null) {
subartifactSet.getPredicate().accept(this);
}
}
}
if (subartifactSet.getFunctionCall() != null) {
throw new RuntimeException(Messages.i18n.format("XP_SUBARTIFACTSET_NOT_SUPPORTED"));
}
if (subartifactSet.getSubartifactSet() != null) {
throw new RuntimeException(Messages.i18n.format("XP_TOPLEVEL_SUBARTIFACTSET_ONLY"));
}
}
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.LocationPath)
*/
@Override
public void visit(LocationPath node) {
if (node.getArtifactType() != null) {
// If an explicit type is given, we need to override the root 'from' in order to give the correct entity class.
// This is so that certain fields can be restricted to their respective entities, rather than cramming
// all possible fields into ArtificerArtifact itself.
ArtificerArtifact artifact;
try {
artifact = HibernateEntityCreator.visit(ArtifactType.valueOf(node.getArtifactType()));
} catch (Exception e) {
throw new RuntimeException(e);
}
query = criteriaBuilder.createQuery(ArtifactSummary.class);
from = query.from(artifact.getClass());
eq("type", node.getArtifactType());
artifactType = node.getArtifactType();
}
if (node.getArtifactModel() != null) {
eq("model", node.getArtifactModel());
artifactModel = node.getArtifactModel();
}
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.AndExpr)
*/
@Override
public void visit(AndExpr node) {
if (node.getRight() == null) {
node.getLeft().accept(this);
} else {
node.getLeft().accept(this);
node.getRight().accept(this);
Predicate predicate1 = predicates.remove(predicates.size() - 1);
Predicate predicate2 = predicates.remove(predicates.size() - 1);
predicates.add(criteriaBuilder.and(predicate1, predicate2));
}
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.EqualityExpr)
*/
@Override
public void visit(EqualityExpr node) {
if (node.getSubartifactSet() != null) {
node.getSubartifactSet().accept(this);
} else if (node.getExpr() != null) {
node.getExpr().accept(this);
} else if (node.getOperator() == null) {
node.getLeft().accept(this);
if (customPropertySubquery != null) {
customPropertySubquery.where(compileAnd(customPropertyPredicates));
} else if (propertyContext != null) {
exists(propertyContext);
}
} else {
node.getLeft().accept(this);
node.getRight().accept(this);
if (customPropertySubquery != null) {
customPropertyPredicates.add(criteriaBuilder.equal(customPropertyValuePath, valueContext));
customPropertySubquery.where(compileAnd(customPropertyPredicates));
} else if (propertyContext != null) {
// TODO: Not guaranteed to be propertyContext -- may be function, etc.
operation(node.getOperator().symbol(), propertyContext, valueContext);
}
valueContext = null;
}
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.ForwardPropertyStep)
*/
@Override
public void visit(ForwardPropertyStep node) {
if (node.getPropertyQName() != null) {
QName property = node.getPropertyQName();
if (property.getNamespaceURI() == null || "".equals(property.getNamespaceURI()))
property = new QName(ArtificerConstants.SRAMP_NS, property.getLocalPart());
if (property.getNamespaceURI().equals(ArtificerConstants.SRAMP_NS)) {
if (corePropertyMap.containsKey(property)) {
propertyContext = corePropertyMap.get(property);
customPropertySubquery = null;
} else {
// Note: Typically, you'd expect to see a really simple MapJoin w/ key and value predicates.
// However, *negation* ("not()") is needed and is tricky when just using a join. Instead, use
// an "a1.id in (select a2.id from ArtificerArtifact a2 [map join and predicates)" -- easily negated.
customPropertySubquery = query.subquery(ArtificerArtifact.class);
From customPropertyFrom = customPropertySubquery.from(ArtificerArtifact.class);
Join customPropertyJoin = customPropertyFrom.join("properties");
customPropertySubquery.select(customPropertyFrom.get("id"));
customPropertyPredicates = new ArrayList<>();
customPropertyPredicates.add(criteriaBuilder.equal(customPropertyFrom.get("id"), from.get("id")));
customPropertyPredicates.add(criteriaBuilder.equal(customPropertyJoin.get("key"), property.getLocalPart()));
customPropertyValuePath = customPropertyJoin.get("value");
predicates.add(criteriaBuilder.exists(customPropertySubquery));
propertyContext = null;
}
} else {
throw new RuntimeException(Messages.i18n.format("XP_INVALID_PROPERTY_NS", property.getNamespaceURI()));
}
}
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.FunctionCall)
*/
@Override
public void visit(FunctionCall node) {
if (ArtificerConstants.SRAMP_NS.equals(node.getFunctionName().getNamespaceURI())) {
if (node.getFunctionName().equals(CLASSIFIED_BY_ALL_OF)) {
visitClassifications(node, false, true);
} else if (node.getFunctionName().equals(CLASSIFIED_BY_ANY_OF)) {
visitClassifications(node, true, true);
} else if (node.getFunctionName().equals(EXACTLY_CLASSIFIED_BY_ALL_OF)) {
visitClassifications(node, false, false);
} else if (node.getFunctionName().equals(EXACTLY_CLASSIFIED_BY_ANY_OF)) {
visitClassifications(node, true, false);
} else if (node.getFunctionName().equals(GET_RELATIONSHIP_ATTRIBUTE)) {
String otherAttributeKey = reduceStringLiteralArgument(node.getArguments().get(1));
// Ex. query: /s-ramp/wsdl/WsdlDocument[someRelationship[s-ramp:getRelationshipAttribute(., 'someAttribute') = 'true']]
// Note that the predicate function needs to add a condition on the relationship selector itself, *not*
// the artifact targeted by the relationship.
customPropertySubquery = query.subquery(ArtificerRelationship.class);
From customPropertyFrom = customPropertySubquery.from(ArtificerRelationship.class);
MapJoin customPropertyJoin = customPropertyFrom.joinMap("otherAttributes");
customPropertySubquery.select(customPropertyFrom.get("id"));
customPropertyPredicates = new ArrayList<>();
customPropertyPredicates.add(criteriaBuilder.equal(customPropertyFrom.get("id"), relationshipFrom.get("id")));
customPropertyPredicates.add(criteriaBuilder.equal(customPropertyJoin.key(), otherAttributeKey));
customPropertyValuePath = customPropertyJoin.value();
predicates.add(criteriaBuilder.exists(customPropertySubquery));
propertyContext = null;
} else if (node.getFunctionName().equals(GET_TARGET_ATTRIBUTE)) {
String otherAttributeKey = reduceStringLiteralArgument(node.getArguments().get(1));
// Ex. query: /s-ramp/wsdl/WsdlDocument[someRelationship[s-ramp:getTargetAttribute(., 'someAttribute') = 'true']]
// Note that the predicate function needs to add a condition on the relationship target selector itself, *not*
// the artifact targeted by the relationship.
customPropertySubquery = query.subquery(ArtificerTarget.class);
From customPropertyFrom = customPropertySubquery.from(ArtificerTarget.class);
MapJoin customPropertyJoin = customPropertyFrom.joinMap("otherAttributes");
customPropertySubquery.select(customPropertyFrom.get("id"));
customPropertyPredicates = new ArrayList<>();
customPropertyPredicates.add(criteriaBuilder.equal(customPropertyFrom.get("id"), targetFrom.get("id")));
customPropertyPredicates.add(criteriaBuilder.equal(customPropertyJoin.key(), otherAttributeKey));
customPropertyValuePath = customPropertyJoin.value();
predicates.add(criteriaBuilder.exists(customPropertySubquery));
propertyContext = null;
} else {
if (node.getFunctionName().getLocalPart().equals("matches") || node.getFunctionName().getLocalPart().equals("not")) {
throw new RuntimeException(Messages.i18n.format("XP_BAD_FUNC_NS", node.getFunctionName().getLocalPart()) );
}
throw new RuntimeException(Messages.i18n.format("XP_FUNC_NOT_SUPPORTED", node.getFunctionName().toString()));
}
} else if (MATCHES.equals(node.getFunctionName())) {
if (node.getArguments().size() != 2) {
throw new RuntimeException(Messages.i18n.format("XP_MATCHES_FUNC_NUM_ARGS_ERROR", node.getArguments().size()));
}
Argument attributeArg = node.getArguments().get(0);
Argument patternArg = node.getArguments().get(1);
String pattern = reduceStringLiteralArgument(patternArg);
if (isFullTextSearch(attributeArg)) {
fullTextSearch(pattern);
} else {
pattern = pattern.replace(".*", "%"); // the only valid wildcard
ForwardPropertyStep attribute = reducePropertyArgument(attributeArg);
attribute.accept(this);
like(propertyContext, pattern);
}
} else if (NOT.equals(node.getFunctionName())) {
if (node.getArguments().size() != 1) {
throw new RuntimeException(Messages.i18n.format("XP_NOT_FUNC_NUM_ARGS_ERROR", node.getArguments().size()));
}
Argument argument = node.getArguments().get(0);
if (argument.getExpr() != null) {
argument.getExpr().accept(this);
// Should have resulted in only 1 constraint -- negate it and re-add
Predicate predicate = predicates.remove(predicates.size() - 1);
predicates.add(criteriaBuilder.not(predicate));
} else {
// TODO: When would not() be given a literal? That's what this implies. As-is, it won't be negated...
argument.accept(this);
}
} else {
throw new RuntimeException(Messages.i18n.format("XP_FUNCTION_NOT_SUPPORTED", node.getFunctionName().toString()));
}
}
private void visitClassifications(FunctionCall node, boolean isOr, boolean allowSubtypes) {
Collection<URI> classifications = resolveArgumentsToClassifications(node.getArguments());
Path classifierPath;
if (allowSubtypes) {
classifierPath = from.get("normalizedClassifiers");
} else {
classifierPath = from.get("classifiers");
}
List<Predicate> classifierConstraints = new ArrayList<>();
for (URI classification : classifications) {
classifierConstraints.add(criteriaBuilder.isMember(classification.toString(), classifierPath));
}
if (isOr) {
predicates.add(compileOr(classifierConstraints));
} else {
predicates.add(compileAnd(classifierConstraints));
}
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.OrExpr)
*/
@Override
public void visit(OrExpr node) {
if (node.getRight() == null) {
node.getLeft().accept(this);
} else {
node.getLeft().accept(this);
node.getRight().accept(this);
Predicate predicate1 = predicates.remove(predicates.size() - 1);
Predicate predicate2 = predicates.remove(predicates.size() - 1);
predicates.add(criteriaBuilder.or(predicate1, predicate2));
}
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.PrimaryExpr)
*/
@Override
public void visit(PrimaryExpr node) {
if (node.getLiteral() != null) {
// If this is a custom property, we must assume that the value will always be a literal String. If
// it's a built-in property, correctly handle booleans and timestamps.
if (customPropertySubquery == null) {
if (propertyContext != null && propertyContext.contains("lastActionTime")) {
Date date = null;
try {
date = SDF.parse(node.getLiteral());
} catch (ParseException e) {
error = new QueryExecutionException(e);
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
valueContext = calendar;
} else if ("true".equalsIgnoreCase(node.getLiteral())) {
valueContext = Boolean.valueOf(true);
} else if ("false".equalsIgnoreCase(node.getLiteral())) {
valueContext = Boolean.valueOf(false);
} else {
valueContext = node.getLiteral();
}
} else {
valueContext = node.getLiteral();
}
} else if (node.getNumber() != null) {
if (customPropertySubquery == null) {
if (propertyContext != null && propertyContext.contains("lastActionTime")) {
Date date = null;
if (node.getNumber() instanceof BigInteger) {
date = new Date(((BigInteger)node.getNumber()).longValue());
} else {
date = new Date((Long)node.getNumber());
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
valueContext = calendar;
} else if ("true".equalsIgnoreCase(node.getLiteral())) {
valueContext = Boolean.valueOf(true);
} else if ("false".equalsIgnoreCase(node.getLiteral())) {
valueContext = Boolean.valueOf(false);
} else {
valueContext = node.getLiteral();
}
} else {
valueContext = node.getNumber().doubleValue();
}
} else if (node.getPropertyQName() != null) {
throw new RuntimeException(Messages.i18n.format("XP_PROPERTY_PRIMARY_EXPR_NOT_SUPPORTED"));
}
}
@Override
public void visit(RelationshipPath node) {
eq("name", node.getRelationshipType());
}
/**
* @see org.artificer.common.query.xpath.visitors.XPathVisitor#visit(org.artificer.common.query.xpath.ast.SubartifactSet)
*/
@Override
public void visit(SubartifactSet node) {
if (node.getFunctionCall() != null) {
node.getFunctionCall().accept(this);
} else if (node.getRelationshipPath() != null) {
From oldRootContext = from;
if (node.getRelationshipPath().getRelationshipType().equalsIgnoreCase("relatedDocument")) {
// derivedFrom
// TODO: Should this really be LEFT?
from = from.join("derivedFrom", JoinType.LEFT);
// Now add any additional predicates included.
if (node.getPredicate() != null) {
node.getPredicate().accept(this);
}
} else if (node.getRelationshipPath().getRelationshipType().equalsIgnoreCase("expandedFromDocument")
|| node.getRelationshipPath().getRelationshipType().equalsIgnoreCase("expandedFromArchive")) {
// expandedFrom
from = from.join("expandedFrom");
// Now add any additional predicates included.
if (node.getPredicate() != null) {
node.getPredicate().accept(this);
}
} else {
// Relationship within a predicate.
// Create a subquery and 'exists' conditional. The subquery is much easier to handle, later on, if this
// predicate is negated, as opposed to removing the inner join or messing with left joins.
List<Predicate> oldPredicates = predicates;
predicates = new ArrayList<>();
Subquery relationshipSubquery = query.subquery(ArtificerRelationship.class);
relationshipFrom = relationshipSubquery.from(ArtificerRelationship.class);
targetFrom = relationshipFrom.join("targets");
relationshipSubquery.select(relationshipFrom.get("id"));
Join relationshipOwnerJoin = relationshipFrom.join("owner");
predicates.add(criteriaBuilder.equal(relationshipOwnerJoin.get("id"), oldRootContext.get("id")));
from = relationshipFrom;
// process constraints on the relationship itself
node.getRelationshipPath().accept(this);
// context now needs to be the relationship targets
from = targetFrom.join("target");
// Now add any additional predicates included.
if (node.getPredicate() != null) {
node.getPredicate().accept(this);
}
// Add predicates to subquery
relationshipSubquery.where(compileAnd(predicates));
predicates = oldPredicates;
// Add 'exists' predicate (using subquery) to original list
predicates.add(criteriaBuilder.exists(relationshipSubquery));
}
// restore the original selector (since the relationship was in a predicate, not a path)
from = oldRootContext;
if (node.getSubartifactSet() != null) {
throw new RuntimeException(Messages.i18n.format("XP_MULTILEVEL_SUBARTYSETS_NOT_SUPPORTED"));
}
}
}
private void eq(String propertyName, Object value) {
if (Boolean.TRUE.equals(value)) {
predicates.add(criteriaBuilder.isTrue(path(propertyName)));
} else if (Boolean.FALSE.equals(value)) {
predicates.add(criteriaBuilder.isFalse(path(propertyName)));
} else {
predicates.add(criteriaBuilder.equal(path(propertyName), value));
}
}
private void gt(String propertyName, Object value) {
if (value instanceof Date) {
predicates.add(criteriaBuilder.greaterThan(path(propertyName), (Date) value));
} else if (value instanceof Calendar) {
predicates.add(criteriaBuilder.greaterThan(path(propertyName), (Calendar) value));
} else if (value instanceof Number) {
predicates.add(criteriaBuilder.gt(path(propertyName), (Number) value));
}
}
private void ge(String propertyName, Object value) {
if (value instanceof Date) {
predicates.add(criteriaBuilder.greaterThanOrEqualTo(path(propertyName), (Date) value));
} else if (value instanceof Calendar) {
predicates.add(criteriaBuilder.greaterThanOrEqualTo(path(propertyName), (Calendar) value));
} else if (value instanceof Number) {
predicates.add(criteriaBuilder.ge(path(propertyName), (Number) value));
}
}
private void lt(String propertyName, Object value) {
if (value instanceof Date) {
predicates.add(criteriaBuilder.lessThan(path(propertyName), (Date) value));
} else if (value instanceof Calendar) {
predicates.add(criteriaBuilder.lessThan(path(propertyName), (Calendar) value));
} else if (value instanceof Number) {
predicates.add(criteriaBuilder.lt(path(propertyName), (Number) value));
}
}
private void le(String propertyName, Object value) {
if (value instanceof Date) {
predicates.add(criteriaBuilder.lessThanOrEqualTo(path(propertyName), (Date) value));
} else if (value instanceof Calendar) {
predicates.add(criteriaBuilder.lessThanOrEqualTo(path(propertyName), (Calendar) value));
} else if (value instanceof Number) {
predicates.add(criteriaBuilder.le(path(propertyName), (Number) value));
}
}
private void like(String propertyName, Object value) {
predicates.add(criteriaBuilder.like(path(propertyName), (String) value));
}
private void ne(String propertyName, Object value) {
predicates.add(criteriaBuilder.notEqual(path(propertyName), value));
}
private void operation(String operator, String propertyName, Object value) {
if ("=".equalsIgnoreCase(operator)) eq(propertyName, value);
else if (">".equalsIgnoreCase(operator)) gt(propertyName, value);
else if (">=".equalsIgnoreCase(operator)) ge(propertyName, value);
else if ("<".equalsIgnoreCase(operator)) lt(propertyName, value);
else if ("<=".equalsIgnoreCase(operator)) le(propertyName, value);
else if ("like".equalsIgnoreCase(operator)) like(propertyName, value);
else if ("<>".equalsIgnoreCase(operator)) ne(propertyName, value);
}
private void fullTextSearch(String query) {
FullTextEntityManager fullTextEntityManager = org.hibernate.search.jpa.Search.getFullTextEntityManager(
entityManager);
QueryBuilder qb = fullTextEntityManager.getSearchFactory().buildQueryBuilder()
.forEntity(ArtificerArtifact.class).get();
BooleanJunction<BooleanJunction> junction = qb.bool();
// not trashed
junction.must(qb.keyword().onField("trashed").matching(false).createQuery());
// the main full-text query
junction.must(qb.keyword()
.onFields("description", "name", "comments.text", "properties.key", "properties.value")
.andField("content").ignoreFieldBridge()
.andField("contentPath").ignoreFieldBridge()
.matching(query)
.createQuery());
// if we have the current model and/or type, further restrict the results
if (StringUtils.isNotBlank(artifactModel)) {
junction.must(
qb.keyword().onField("model").matching(artifactModel).createQuery());
}
if (StringUtils.isNotBlank(artifactType)) {
junction.must(
qb.keyword().onField("type").matching(artifactType).createQuery());
}
FullTextQuery fullTextQuery = fullTextEntityManager.createFullTextQuery(
junction.createQuery(), ArtificerArtifact.class);
fullTextQuery.setProjection("id");
List<Object[]> results = fullTextQuery.getResultList();
// There is not currently a way to combine JPA Criteria Queries with Hibernate Search Queries. Until then,
// we need to build up a list of the full-text result IDs. That list is then used as a "artifact.id IN ([list])"
// predicate.
// Note that some databases (Oracle especially) limit the number of elements in an "in" expression. Even if
// it's restricted, they typically allow for at least 1000 elements. Just to be safe (and maintain
// portability), break the expressions up into 1000-element chunks.
List<Predicate> searchResults = new ArrayList<>();
for (int i = 0; i < results.size(); i += 1000) {
List<Object[]> subResults;
if (results.size() > i + 1000) {
subResults = results.subList(i, i + 1000 - 1);
} else {
subResults = results;
}
Long[] ids = new Long[subResults.size()];
for (int j = 0; j < subResults.size(); j++) {
Object[] result = subResults.get(j);
ids[j] = (Long) result[0];
}
searchResults.add(from.get("id").in(ids));
}
if (searchResults.size() > 0) {
predicates.add(compileOr(searchResults));
}
}
private void exists(String propertyName) {
predicates.add(criteriaBuilder.isNotNull(path(propertyName)));
}
private Predicate compileAnd(List<Predicate> constraints) {
if (constraints.size() == 0) {
return null;
} else if (constraints.size() == 1) {
return constraints.get(0);
} else {
return criteriaBuilder.and(constraints.get(0), compileAnd(constraints.subList(1, constraints.size())));
}
}
private Predicate compileOr(List<Predicate> constraints) {
if (constraints.size() == 0) {
return null;
} else if (constraints.size() == 1) {
return constraints.get(0);
} else {
return criteriaBuilder.or(constraints.get(0), compileOr(constraints.subList(1, constraints.size())));
}
}
public Path path(String propertyName) {
if (propertyName.contains(".")) {
// The propertyName is a path. Example: createdBy.username, where 'createdBy' is an @Embedded User.
// Needs to become from.get("createdBy").get("username").
String[] split = propertyName.split("\\.");
Path path = from.get(split[0]);
if (split.length > 1) {
for (int i = 1; i < split.length; i++) {
path = path.get(split[i]);
}
}
return path;
} else {
return from.get(propertyName);
}
}
}