org.artificer.repository.hibernate.query.ArtificerToHibernateQueryVisitor.java Source code

Java tutorial

Introduction

Here is the source code for org.artificer.repository.hibernate.query.ArtificerToHibernateQueryVisitor.java

Source

/*
 * 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 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;

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 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;

/**
 * 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) {
            // TODO: may be an int?
            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);
        }
    }

}