All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.cq.social.ugc.api.UgcFilterQueryWriter Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2012 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.cq.social.ugc.api;

import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Responsible for writing a UgcFilter as a JCR SQL2 query.
 */
public class UgcFilterQueryWriter {

    private static final String SELECT = "SELECT";
    private static final char SELECT_ALL = '*';
    private static final String FROM = "FROM";
    private static final String WHERE = "WHERE";
    private static final String ORDER_BY = "ORDER BY";
    private static final String CONTAINS = "CONTAINS";
    private static final String LIKE = "LIKE";
    private static final String IS = "IS";
    private static final String NULL = "NULL";
    private static final String IS_DESCENDANT_NODE = "ISDESCENDANTNODE";
    private static final String IS_SAME_NODE = "ISSAMENODE";
    private static final String IS_CHILD_NODE = "ISCHILDNODE";
    private static final String ASCENDING = "ASC";
    private static final String DESCENDING = "DESC";
    private static final String AND = "AND";
    private static final String OR = "OR";
    private static final String NOT = "NOT";
    private static final String AS = "AS";
    private static final char OPEN_FROM = '[';
    private static final char CLOSE_FROM = ']';
    private static final char SPACING = ' ';
    private static final char OPEN_GROUP = '(';
    private static final char CLOSE_GROUP = ')';
    private static final char OPEN_ORDER_NAME = '[';
    private static final char CLOSE_ORDER_NAME = ']';
    private static final char ORDERING_SEPARATOR = ',';

    private static final char VARIABLE_IDENTIFIER = '$';

    private static final char OPEN_SINGLE_QUOTE = '\'';
    private static final char CLOSE_SINGLE_QUOTE = '\'';
    private static final char OPEN_FUNCTION = '(';
    private static final char CLOSE_FUNCTION = ')';
    private static final char FUNCTION_PARAM_SEPARATOR = ',';
    private static final char SELECTOR_NAME = 'c';
    private static final char SELECTOR_SEPARATOR = '.';
    private static final char OPEN_FIELD_NAME = '[';
    private static final char CLOSE_FIELD_NAME = ']';

    private static final char EQUAL = '=';
    private static final char LESS_THAN = '<';
    private static final char GREATER_THAN = '>';
    private static final String LESS_THAN_OR_EQUAL = "<=";
    private static final String GREATER_THAN_OR_EQUAL = ">=";
    private static final String NOT_EQUAL = "<>";

    /**
     * Writes a JCR SQL2 query from the given UgcFilter.
     * @param ugcFilter UgcFilter to write as a JCR SQL2 query.
     * @return String containing the JCR SQL2 query.
     */
    public String write(final UgcFilter ugcFilter) {
        final StringBuffer query = new StringBuffer();

        query.append(SELECT);
        query.append(SPACING);
        query.append(SELECT_ALL);
        query.append(SPACING);
        query.append(FROM);
        query.append(SPACING);
        query.append(OPEN_FROM);
        query.append(ugcFilter.getContentType());
        query.append(CLOSE_FROM);
        query.append(SPACING);
        query.append(AS);
        query.append(SPACING);
        query.append(SELECTOR_NAME);
        query.append(SPACING);
        query.append(WHERE);

        if (ugcFilter.hasConstraints()) {
            query.append(SPACING);
            ConstraintVisitor constraintVisitor;
            if (ugcFilter.hasVariables()) {
                constraintVisitor = new QueryWriterConstraintVisitor(query, ugcFilter.getVariables());
            } else {
                constraintVisitor = new QueryWriterConstraintVisitor(query);
            }
            final Collection constraints = ugcFilter.getConstraints();
            for (final Constraint constraint : constraints) {
                constraint.accept(constraintVisitor);
            }
        }

        handleSort(query, ugcFilter);
        return query.toString();
    }

    /**
     * Adds sort information to the query if present.
     * @param query StringBuffer containing the query.
     * @param ugcFilter UgcFilter containing sort information.
     */
    private void handleSort(final StringBuffer query, final UgcFilter ugcFilter) {
        if (ugcFilter.isSorted()) {
            query.append(SPACING);
            query.append(ORDER_BY);

            final List sortOrder = ugcFilter.getSortOrder();
            boolean first = true;
            for (final UgcSort sort : sortOrder) {
                if (first) {
                    first = false;
                } else {
                    query.append(ORDERING_SEPARATOR);
                }
                query.append(SPACING);

                final String propertyName = sort.getPropertyName();
                query.append(OPEN_ORDER_NAME);
                query.append(propertyName);
                query.append(CLOSE_ORDER_NAME);

                query.append(SPACING);
                if (sort.isAscending()) {
                    query.append(ASCENDING);
                } else {
                    query.append(DESCENDING);
                }
            }
        }
    }

    /**
     * QueryWriterConstraintVisitor is a ConstraintVisitor implementation which traverses the constraint hierarchy in
     * order to construct a JCR SQL2 where clause from the constraints that have been added to a UgcFilter.
     */
    private static class QueryWriterConstraintVisitor extends DefaultConstraintVisitor implements ConstraintVisitor {

        private boolean first = true;
        private final StringBuffer query;
        private Map variableMap;

        /**
         * Creates a new QueryWriterConstraintVisitor with a StringBufer containing a partially constructed query.
         * @param query StringBuffer to write the where clause to.
         */
        public QueryWriterConstraintVisitor(final StringBuffer query) {
            super();
            this.query = query;
        }

        /**
         * Creates a new QueryWriterConstraintVisitor with a StringBufer containing a partially constructed query and
         * a variable map so that variables can be replaced with bound values as needed.
         * @param query StringBuffer to write the where clause to.
         */
        public QueryWriterConstraintVisitor(final StringBuffer query, final Map variables) {
            super();
            this.query = query;
            this.variableMap = variables;
        }

        /**
         * Write a SetConstraint in JCR SQL2 format.
         * @param setConstraint SetConstraint to write
         */
        @Override
        public void visitSetConstraint(final SetConstraint setConstraint) {
            writeOperator(setConstraint);
            writeNegation(setConstraint);

            if (setConstraint.size() > 1) {
                query.append(OPEN_GROUP);
            }
            for (int i = 0; i < setConstraint.size(); i++) {
                if (i > 0) {
                    query.append(SPACING);
                    query.append(OR);
                    query.append(SPACING);
                }
                appendSelector(query, setConstraint.getPropertyName());

                query.append(SPACING);
                query.append(EQUAL);
                query.append(SPACING);

                writeValue(query, setConstraint.get(i), "");
            }
            if (setConstraint.size() > 1) {
                query.append(CLOSE_GROUP);
            }
        }

        /**
         * Write a PathConstraint in JCR SQL2 format.
         * @param pathConstraint PathConstraint to write.
         */
        @Override
        public void visitPathConstraint(final PathConstraint pathConstraint) {
            writeOperator(pathConstraint);
            // Enclose the NOT constraint within () - otherwise the query gets messed up when converting to lucene syntax
            // A AND NOT B AND C effectively becomes A AND NOT (B AND C) if we don't enclose NOT in ()
            // But If I do A AND (NOT B) AND C - things work fine .
            // Primarily the impact was that scheduled post search was browsing through whole massive UGC instead of just fetching the scheduled posts (Refer CQ-4250463)
            if(pathConstraint.isNegated()) {
                query.append(OPEN_FUNCTION);
            }
            writeNegation(pathConstraint);

            final String path = pathConstraint.getPath();
            switch (pathConstraint.getPathConstraintType()) {
                case IsSameNode:
                    query.append(IS_SAME_NODE);
                    break;
                case IsDescendantNode:
                    query.append(IS_DESCENDANT_NODE);
                    break;
                case IsChildNode:
                    query.append(IS_CHILD_NODE);
                    break;
                default:
                    throw new RuntimeException(pathConstraint.getPathConstraintType().name() + " is not supported");
            }
            query.append(OPEN_FUNCTION);
            query.append(SELECTOR_NAME);
            query.append(FUNCTION_PARAM_SEPARATOR);
            query.append(SPACING);
            query.append(OPEN_SINGLE_QUOTE);
            /*
             * As of 3/2015 rryan's research indicates SQL2 only needs the single quote quoted.
             * http://stackoverflow.com/
             * questions/27239837/how-to-escape-dynamically-generated-string-values-in-a-jcr-sql2-query
             */
            query.append(path.replace("'", "''"));
            query.append(CLOSE_SINGLE_QUOTE);
            query.append(CLOSE_FUNCTION);
            if(pathConstraint.isNegated()) {
                query.append(CLOSE_FUNCTION);
            }
        }

        /**
         * Write a ValueConstraint in JCR SQL2 format.
         * @param valueConstraint ValueConstraint to write.
         */
        @Override
        public void visitValueConstraint(final ValueConstraint valueConstraint) {
            writeOperator(valueConstraint);
            if (ComparisonType.NotEquals == valueConstraint.getComparison()) {
                query.append(OPEN_GROUP);
                query.append(NOT);
                query.append(SPACING);
                query.append(OPEN_GROUP);
            } else {
                writeNegation(valueConstraint);
            }

            final String name = valueConstraint.getPropertyName();
            final Object value = valueConstraint.getValue();

            appendSelector(query, name);

            query.append(SPACING);
            final ComparisonType comparisonType = valueConstraint.getComparison();
            Object v = value;
            if (value instanceof String) {
                v = fetchVariable((String) value);
            }

            switch (comparisonType) {
                case Equals:
                    if (v == null) {
                        query.append(IS);
                    } else {
                        query.append(EQUAL);
                    }
                    break;
                case NotEquals:
                    if (v == null) {
                        query.append(IS);
                    } else {
                        query.append(EQUAL);
                    }
                    break;
                case GreaterThan:
                    query.append(GREATER_THAN);
                    break;
                case GreaterThanOrEqualTo:
                    query.append(GREATER_THAN_OR_EQUAL);
                    break;
                case LessThan:
                    query.append(LESS_THAN);
                    break;
                case LessThanOrEqualTo:
                    query.append(LESS_THAN_OR_EQUAL);
                    break;
                case BeginsWith:
                    query.append(LIKE);
                    break;
                default:
                    throw new RuntimeException("ComparisonType " + comparisonType.name() + " is not supported");
            }
            query.append(SPACING);

            if (v == null) {
                query.append((String) null);
            } else {
                writeValue(query, value, ComparisonType.BeginsWith == valueConstraint.getComparison() ? "%" : "");
            }

            if (ComparisonType.NotEquals == valueConstraint.getComparison()) {
                query.append(CLOSE_GROUP);
                query.append(CLOSE_GROUP);
            }
        }

        /**
         * Add the selector, separator, and square brackets to escape necessary characters such as :, with the
         * selector name inside. So the format will be like "c.[my:name]".
         * @param sb a StringBuffer to append to
         * @param name the selector to add.
         */
        private void appendSelector(final StringBuffer sb, final String name) {
            sb.append(SELECTOR_NAME);
            sb.append(SELECTOR_SEPARATOR);
            sb.append(OPEN_FIELD_NAME);
            sb.append(name);
            sb.append(CLOSE_FIELD_NAME);
        }

        /**
         * Write a ConstraintGroup in JCR SQL2 format.
         * @param constraintGroup ConstraintGroup to write.
         */
        @Override
        public void visitConstraintGroup(final ConstraintGroup constraintGroup) {
            if (constraintGroup.hasConstraints()) {
                writeOperator(constraintGroup);
                writeNegation(constraintGroup);
                final Collection constraints = constraintGroup.getConstraints();
                if (constraints.size() > 1) {
                    query.append(OPEN_GROUP);
                }
                this.first = true; // reset first so that operator is not added after the (
                for (final Constraint constraint : constraints) {
                    constraint.accept(this);
                }
                if (constraints.size() > 1) {
                    query.append(CLOSE_GROUP);
                }
            }
        }

        /**
         * Write a RangeConstraint in JCR SLQ2 format.
         * @param rangeConstraint RangeConstraint to write.
         */
        @Override
        public void visitRangeConstraint(final RangeConstraint rangeConstraint) {
            writeOperator(rangeConstraint);
            writeNegation(rangeConstraint);

            final Object min = rangeConstraint.getMinValue();
            final Object max = rangeConstraint.getMaxValue();
            final String propertyName = rangeConstraint.getPropertyName();

            query.append(OPEN_GROUP);

            appendSelector(query, propertyName);

            query.append(SPACING);
            if (rangeConstraint.isInclusive()) {
                query.append(GREATER_THAN_OR_EQUAL);
            } else {
                query.append(GREATER_THAN);
            }
            query.append(SPACING);
            writeValue(query, min, "");
            query.append(SPACING);

            query.append(AND);

            query.append(SPACING);
            appendSelector(query, propertyName);
            query.append(SPACING);

            if (rangeConstraint.isInclusive()) {
                query.append(LESS_THAN_OR_EQUAL);
            } else {
                query.append(LESS_THAN);
            }
            query.append(SPACING);
            writeValue(query, max, "");

            query.append(CLOSE_GROUP);
        }

        /**
         * Write a FullTextConstraint in JCR SLQ2 format.
         * @param fullTextConstraint FullTextConstraint to write.
         */
        @Override
        public void visitFullTextConstraint(final FullTextConstraint fullTextConstraint) {
            writeOperator(fullTextConstraint);
            writeNegation(fullTextConstraint);

            query.append(CONTAINS);
            query.append(OPEN_FUNCTION);

            if (fullTextConstraint.definesPropertiesToInclude()) {
                boolean first = true;
                for (final String fieldName : fullTextConstraint.getIncludedProperties()) {
                    if (first) {
                        first = false;
                    } else {
                        query.append(FUNCTION_PARAM_SEPARATOR);
                        query.append(SPACING);
                    }
                    appendSelector(query, fieldName);
                }
            } else {
                query.append(SELECTOR_NAME);
                query.append(SELECTOR_SEPARATOR);
                query.append(SELECT_ALL);
            }
            query.append(FUNCTION_PARAM_SEPARATOR);
            query.append(SPACING);
            writeValue(query, fullTextConstraint.getExpression(), "");
            query.append(CLOSE_FUNCTION);
        }

        private Object fetchVariable(final String valueString) {
            if (VARIABLE_IDENTIFIER == valueString.charAt(0)) {
                if (this.variableMap != null) {
                    final String varName = valueString.substring(1);
                    if (this.variableMap.containsKey(varName)) {
                        return this.variableMap.get(varName);
                    }
                }
            }
            return valueString;
        }

        /**
         * Writes a value to the query. If the value is an unknown variable, long or integer it is written without
         * quotes, otherwise it is wrapped in single quotes. If the value is a known variable, it gets replaced with
         * its actual mapped value, and wrapped in single quotes unless it's a long or integer.
         * @param query StringBuffer containing the query being constructed.
         * @param value Value to write.
         * @param suffix The suffix to add to a string parameter
         */
        protected void writeValue(final StringBuffer query, final Object value, final String suffix) {
            final String valueString = value.toString();
            final Object varValue = fetchVariable(valueString);

            if (varValue != null) {
                if (varValue instanceof Integer || varValue instanceof Long) {
                    query.append(varValue);
                } else {
                    query.append(OPEN_SINGLE_QUOTE);
                    query.append((varValue + suffix).replaceAll("'", "''"));
                    query.append(CLOSE_SINGLE_QUOTE);
                }

            } else {
                if (value instanceof Integer || value instanceof Long) {
                    query.append(valueString);
                } else {
                    query.append(OPEN_SINGLE_QUOTE);
                    query.append(valueString);
                    query.append(CLOSE_SINGLE_QUOTE);
                }
            }
        }

        /**
         * Writes negation in JCR SQL2 format if present in the constraint.
         * @param constraint Constraint to check for negation.
         */
        protected void writeNegation(final Constraint constraint) {
            if (constraint.isNegated()) {
                this.query.append(NOT);
                this.query.append(SPACING);
            }
        }

        /**
         * Writes the operator in JCR SQL2 format if it is not the first constraint being handled.
         * @param constraint Constraint containing the operator.
         */
        protected void writeOperator(final Constraint constraint) {
            if (this.first) {
                this.first = false;
            } else {
                this.query.append(SPACING);
                final Operator operator = constraint.getOperator();
                switch (operator) {
                    case And:
                        this.query.append(AND);
                        break;
                    case Or:
                        this.query.append(OR);
                        break;
                    default:
                        throw new RuntimeException("Operator " + operator.name() + " is not supported");
                }
                this.query.append(SPACING);
            }
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy