com.flexive.core.search.cmis.parser.StatementBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.flexive.core.search.cmis.parser.StatementBuilder.java

Source

/***************************************************************
 *  This file is part of the [fleXive](R) framework.
 *
 *  Copyright (c) 1999-2014
 *  UCS - unique computing solutions gmbh (http://www.ucs.at)
 *  All rights reserved
 *
 *  The [fleXive](R) project is free software; you can redistribute
 *  it and/or modify it under the terms of the GNU Lesser General Public
 *  License version 2.1 or higher as published by the Free Software Foundation.
 *
 *  The GNU Lesser General Public License can be found at
 *  http://www.gnu.org/licenses/lgpl.html.
 *  A copy is found in the textfile LGPL.txt and important notices to the
 *  license from the author are found in LICENSE.txt distributed with
 *  these libraries.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  For further information about UCS - unique computing solutions gmbh,
 *  please see the company website: http://www.ucs.at
 *
 *  For further information about [fleXive](R), please see the
 *  project website: http://www.flexive.org
 *
 *
 *  This copyright notice MUST APPEAR in all copies of the file!
 ***************************************************************/
package com.flexive.core.search.cmis.parser;

import com.flexive.core.search.cmis.model.*;
import com.flexive.core.storage.ContentStorage;
import com.flexive.core.storage.TreeStorage;
import com.flexive.shared.CacheAdmin;
import com.flexive.shared.cmis.CmisVirtualProperty;
import com.flexive.shared.exceptions.FxApplicationException;
import com.flexive.shared.exceptions.FxCmisSqlParseException;
import com.flexive.shared.exceptions.FxRuntimeException;
import com.flexive.shared.search.SortDirection;
import com.flexive.shared.search.query.PropertyValueComparator;
import com.flexive.shared.structure.FxDataType;
import com.flexive.shared.structure.FxEnvironment;
import com.flexive.shared.structure.FxPropertyAssignment;
import com.flexive.shared.structure.FxType;
import com.flexive.shared.tree.FxTreeMode;
import com.flexive.shared.tree.FxTreeNode;
import org.antlr.runtime.tree.Tree;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import static com.flexive.shared.exceptions.FxCmisSqlParseException.ErrorCause;

/**
 * Translates a CMIS SQL AST to a {@link Statement}.
 *
 * @author Daniel Lichtenberger (daniel.lichtenberger@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at)
 * @version $Rev$
 * @since 3.1
 */
class StatementBuilder {
    private static final Log LOG = LogFactory.getLog(StatementBuilder.class);

    private final Tree root;
    private final Statement stmt = new Statement();
    private final Connection con;
    private final ContentStorage storage;
    private final TreeStorage treeStorage;
    private final FxEnvironment environment;

    StatementBuilder(Connection con, ContentStorage storage, TreeStorage treeStorage, Tree root) {
        this.root = root;
        this.con = con;
        this.storage = storage;
        this.treeStorage = treeStorage;
        this.environment = CacheAdmin.getEnvironment();
    }

    public Statement getStatement() {
        return stmt;
    }

    public Statement build() throws FxCmisSqlParseException {
        if (LOG.isTraceEnabled()) {
            LOG.trace("Evaluating tree: " + root.toStringTree());
        }
        assert root.getType() == CmisSqlLexer.STATEMENT;

        try {
            processFrom(getFirstChildWithType(root, CmisSqlLexer.FROM));
            processSelect(getFirstChildWithType(root, CmisSqlLexer.SELECT));
            processWhere(getFirstChildWithType(root, CmisSqlLexer.WHERE));
            processOrderBy(getFirstChildWithType(root, CmisSqlLexer.ORDER));
        } catch (FxRuntimeException e) {
            throw new FxCmisSqlParseException(LOG, e.getConverted());
        }

        return stmt;
    }

    protected void processSelect(Tree selectNode) throws FxCmisSqlParseException {
        assert selectNode.getType() == CmisSqlLexer.SELECT;
        assert !stmt.getTables().isEmpty() : "No tables selected, or FROM clause not processed.";

        for (Tree child : children(selectNode)) {
            switch (child.getType()) {
            case CmisSqlLexer.ANYREF:
                for (ValueExpression valueExpression : processAnyRef(child)) {
                    stmt.addSelectedColumn(valueExpression);
                }
                break;
            default:
                stmt.addSelectedColumn(processValueExpression(child));
                break;
            }

        }
    }

    protected void processFrom(Tree fromNode) throws FxCmisSqlParseException {
        assert fromNode.getType() == CmisSqlLexer.FROM;
        for (Tree child : children(fromNode)) {
            stmt.addTable(decodeFromTableReference(child));
        }
    }

    private TableReference decodeFromTableReference(Tree node) throws FxCmisSqlParseException {
        switch (node.getType()) {
        case CmisSqlLexer.TREF:
            return decodeTableReference(node);
        case CmisSqlLexer.JOIN:
            // add table references
            final Tree onNode = getFirstChildWithType(node, CmisSqlLexer.ON);
            final TableReference table1 = decodeFromTableReference(node.getChild(0));
            final TableReference table2 = decodeFromTableReference(node.getChild(1));
            // decode "ON" node, which will reference the tables just added, so we temporarily
            // add them to the statement to avoid replicating the column reference decoder
            stmt.addTable(table1);
            stmt.addTable(table2);
            final JoinedTableReference tableJoin = new JoinedTableReference(table1, table2,
                    decodeColumnReference(onNode.getChild(0), false),
                    decodeColumnReference(onNode.getChild(1), false));
            stmt.removeTable(table1);
            stmt.removeTable(table2);
            return tableJoin;
        default:
            throw new UnsupportedOperationException(
                    "Invalid node type for 'FROM' table reference: " + node.getType());
        }
    }

    private SingleTableReference decodeTableReference(Tree node) {
        assert node.getType() == CmisSqlLexer.TREF;
        final Tree aliasDef = getFirstChildWithType(node, CmisSqlLexer.TALIASDEF);
        final String tableName = decodeIdent(node.getChild(0));
        return aliasDef == null ? new SingleTableReference(environment, tableName)
                : new SingleTableReference(environment, tableName, decodeIdent(aliasDef.getChild(0)));
    }

    protected void processWhere(Tree whereNode) throws FxCmisSqlParseException {
        if (whereNode == null) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Empty where node.");
            }
            return;
        }
        assert whereNode.getType() == CmisSqlLexer.WHERE;
        stmt.setRootCondition(processCondition(null, whereNode.getChild(0)));
    }

    protected Condition processCondition(ConditionList parent, Tree root) throws FxCmisSqlParseException {
        switch (root.getType()) {
        case CmisSqlLexer.AND:
        case CmisSqlLexer.OR:
            final ConditionList.Connective type = root.getType() == CmisSqlLexer.AND ? ConditionList.Connective.AND
                    : ConditionList.Connective.OR;
            // re-use parent list to avoid nesting expressions with the same operator
            final ConditionList list = parent != null && parent.getConnective().equals(type) ? parent
                    : new ConditionList(parent, type);
            // add nested conditions to the list
            for (Tree node : children(root)) {
                final Condition cond = processCondition(list, node);
                if (cond != list) {
                    list.addCondition(cond);
                }
            }
            return list;

        case CmisSqlLexer.COMPOP:
            final Tree compNode = root.getChild(0);
            final String compOp = compNode.getText();
            final ValueExpression lhs = processValueExpression(compNode.getChild(0));
            return new ComparisonCondition(parent, lhs, PropertyValueComparator.forComparison(compOp),
                    processLiteral(compNode.getChild(1),
                            lhs instanceof ColumnReference ? (ColumnReference) lhs : null));

        case CmisSqlLexer.CONTAINS:
            assert root.getChild(0).getType() == CmisSqlLexer.CHARLIT; // contains search expression
            final String qualifier = root.getChildCount() > 1 ? decodeIdent(root.getChild(1).getChild(0)) : null;
            final String expression = decodeCharLiteral(root.getChild(0));
            if (qualifier == null && stmt.getTables().size() > 1) {
                throw new FxCmisSqlParseException(LOG, ErrorCause.AMBIGUOUS_CONTAINS, expression);
            }

            return new ContainsCondition(parent,
                    qualifier != null ? stmt.getTable(qualifier) : stmt.getTables().get(0), qualifier, expression);

        case CmisSqlLexer.LIKE:
            return new LikeCondition(parent,
                    decodeColumnReference(getFirstChildWithType(root, CmisSqlLexer.CREF), false),
                    decodeCharLiteral(getFirstChildWithType(root, CmisSqlLexer.CHARLIT)),
                    getFirstChildWithType(root, CmisSqlLexer.NOT) != null);

        case CmisSqlLexer.IN:
            final Tree inCref = getFirstChildWithType(root, CmisSqlLexer.CREF);
            final ColumnReference inColumnReference = decodeColumnReference(inCref, false);
            final List<Literal> inValues = new ArrayList<Literal>(root.getChildCount() - 1);
            for (int i = inCref.getChildIndex() + 1; i < root.getChildCount(); i++) {
                inValues.add(processLiteral(root.getChild(i), inColumnReference));
            }
            return new InCondition(parent, inColumnReference, inValues,
                    getFirstChildWithType(root, CmisSqlLexer.NOT) != null);

        case CmisSqlLexer.NULL:
            return new NullCondition(parent,
                    decodeColumnReference(getFirstChildWithType(root, CmisSqlLexer.CREF), false),
                    getFirstChildWithType(root, CmisSqlLexer.NOT) != null);

        case CmisSqlLexer.IN_FOLDER:
        case CmisSqlLexer.IN_TREE:
            // optional table qualifier
            final TableReference folderQualifier = root.getChildCount() > 1
                    ? stmt.getTable(decodeIdent(root.getChild(1).getChild(0)))
                    : null;
            if (folderQualifier == null && stmt.getTableCount() > 1) {
                // table must be specified for JOINs
                throw new FxCmisSqlParseException(LOG, ErrorCause.AMBIGUOUS_TABLE_REF,
                        (root.getType() == CmisSqlLexer.IN_FOLDER ? "IN_FOLDER" : "IN_TREE"));
            }
            final TableReference sourceTable = folderQualifier == null ? stmt.getTables().get(0) : folderQualifier;

            // find folder - TODO: Edit/Live tree selection?
            final String folderIdent = decodeCharLiteral(root.getChild(0));
            final long folderId;
            try {
                if (StringUtils.isNumeric(folderIdent)) {
                    // folder ID as used by the CMIS interfaces
                    folderId = Long.parseLong(folderIdent);
                    // check if the folder is actually valid
                    treeStorage.getTreeNodeInfo(con, FxTreeMode.Edit, folderId);
                } else if (folderIdent != null && folderIdent.startsWith("/")) {
                    // lookup folder path
                    folderId = treeStorage.getIdByFQNPath(con, FxTreeMode.Edit, FxTreeNode.ROOT_NODE, folderIdent);
                    if (folderId == -1) {
                        throw new FxCmisSqlParseException(LOG, ErrorCause.INVALID_NODE_PATH, folderIdent);
                    }
                } else {
                    throw new IllegalArgumentException("No folder ID or path for tree condition");
                }

                return root.getType() == CmisSqlLexer.IN_FOLDER
                        // IN_FOLDER
                        ? new FolderCondition(parent, sourceTable, folderId)
                        // IN_TREE (needs node info for bounds checks)
                        : new TreeCondition(parent, sourceTable,
                                treeStorage.getTreeNodeInfo(con, FxTreeMode.Edit, folderId));

            } catch (FxApplicationException e) {
                throw new FxCmisSqlParseException(LOG, e);
            }
        default:
            throw new UnsupportedOperationException("Unsupported condition: " + root.toStringTree());
        }
    }

    protected ValueExpression processValueExpression(Tree node) throws FxCmisSqlParseException {
        switch (node.getType()) {
        case CmisSqlLexer.CREF:
            return decodeColumnReference(node, false);
        case CmisSqlLexer.FUNCTION:
            return processFunction(node);
        default:
            throw new UnsupportedOperationException("Unsupported value expression: " + node.toStringTree());
        }
    }

    protected List<ValueExpression> processAnyRef(Tree node) {
        assert node.getType() == CmisSqlLexer.ANYREF;

        // find out the target tables of "*"
        final List<TableReference> tables = new ArrayList<TableReference>();
        final Tree tableReferenceNode = getFirstChildWithType(node, CmisSqlLexer.TREF);
        if (tableReferenceNode != null) {
            tables.add(stmt.getTable(decodeIdent(tableReferenceNode.getChild(0))));
        } else {
            if (stmt.getTables().size() > 1) {
                throw new IllegalArgumentException("'*' without table alias, but with more than one source table");
            }
            tables.addAll(stmt.getTables());
        }

        final List<ValueExpression> result = new ArrayList<ValueExpression>();
        for (TableReference rootTable : tables) {
            for (TableReference table : rootTable.getLeafTables()) {
                final FxType type = table.getRootType();

                // add all properties assigned to the root type
                for (FxPropertyAssignment assignment : type.getAssignedProperties()) {
                    if (!assignment.isSystemInternal()
                            && !FxDataType.Binary.equals(assignment.getProperty().getDataType())) {
                        result.add(new ColumnReference(environment, storage, table,
                                assignment.getAlias().toLowerCase(), null, false));
                    }
                }

                // add the main binary property
                if (type.getMainBinaryAssignment() != null) {
                    result.add(new ColumnReference(environment, storage, table,
                            type.getMainBinaryAssignment().getAlias().toLowerCase(), null, false));
                }
            }
        }

        // add all CMIS system properties
        for (CmisVirtualProperty property : CmisVirtualProperty.values()) {
            if (property.isSupportsQuery()) {
                result.add(new ColumnReference(environment, storage, tables.get(0), property.getCmisPropertyName(),
                        null, false));
            }
        }

        return result;
    }

    protected ValueFunction processFunction(Tree node) throws FxCmisSqlParseException {
        assert node.getType() == CmisSqlLexer.FUNCTION;
        final Tree funNode = node.getChild(0);
        final String alias = getOptionalTextNode(node, CmisSqlLexer.CALIAS);
        switch (funNode.getType()) {
        case CmisSqlLexer.UPPER:
            return new StringValueFunction(decodeColumnReference(node.getChild(1), true), "upper", alias);
        case CmisSqlLexer.LOWER:
            return new StringValueFunction(decodeColumnReference(node.getChild(1), true), "lower", alias);
        case CmisSqlLexer.SCORE:
            return new NumericValueFunction("score", alias);
        default:
            throw new UnsupportedOperationException("Unsupported function: " + funNode.toStringTree());
        }
    }

    protected ColumnReference decodeColumnReference(Tree node, boolean useUpperCaseFilter)
            throws FxCmisSqlParseException {
        assert node.getType() == CmisSqlLexer.CREF;

        // a normal column (property) reference
        final Tree tableRef = getFirstChildWithType(node, CmisSqlLexer.TREF);
        final boolean multiValued = node.getChild(0).getType() == CmisSqlLexer.ANY;
        final String column = decodeIdent(node.getChild(multiValued ? 1 : 0));

        // table alias must be set unless only one table was selected
        if (tableRef == null && stmt.getTables().size() > 1) {
            throw new FxCmisSqlParseException(LOG, ErrorCause.AMBIGUOUS_COLUMN_REF, column);
        }

        final ColumnReference result = new ColumnReference(environment, storage,
                tableRef != null ? stmt.getTable(decodeIdent(tableRef.getChild(0))) : stmt.getTables().get(0),
                column, getOptionalTextNode(node, CmisSqlLexer.CALIAS), useUpperCaseFilter);
        if (multiValued && !result.isMultivalued()) {
            throw new FxCmisSqlParseException(LOG, ErrorCause.EXPECTED_MVREF, result.getAlias());
        }
        return result;
    }

    /**
     * Process a literal node. The parser separates literals in numeric literals
     * ({@link com.flexive.core.search.cmis.parser.CmisSqlLexer#CHARLIT})
     * and character literals
     * ({@link com.flexive.core.search.cmis.parser.CmisSqlLexer#NUMLIT}).
     * The optional column reference is used for determining the value range of numeric literals,
     * otherwise Double is used.
     *
     * @param node      the literal node
     * @param forColumn an optional column reference that specifies the value range for numeric literals
     * @return a literal instance
     */
    protected Literal processLiteral(Tree node, ColumnReference forColumn) {
        final String text = node.getChild(0).getText();
        switch (node.getType()) {
        case CmisSqlLexer.CHARLIT:
            // strip quotes
            return new Literal<String>(decodeCharLiteral(node));
        case CmisSqlLexer.NUMLIT:
            if (forColumn == null) {
                return new Literal<Number>(Double.parseDouble(text));
            } else {
                // determine numeric type of column
                switch (forColumn.getPropertyEntry().getProperty().getDataType()) {
                case Number:
                    return new Literal<Integer>(Integer.parseInt(text));
                case LargeNumber:
                case Reference:
                case SelectOne:
                case SelectMany:
                    return new Literal<Long>(Long.parseLong(text));
                case Double:
                    return new Literal<Double>(Double.parseDouble(text));
                case Float:
                    return new Literal<Float>(Float.parseFloat(text));
                default:
                    throw new UnsupportedOperationException("Unsupported numeric literal type: "
                            + forColumn.getPropertyEntry().getProperty().getDataType());
                }
            }

        default:
            throw new UnsupportedOperationException("Unsupported literal node: " + node.toStringTree());
        }
    }

    protected String decodeIdent(Tree node) {
        assert node.getType() == CmisSqlLexer.IDENTIFIER;
        final String text = node.getChild(0).getText();
        return text.startsWith("\"") && text.endsWith("\"") ? text.substring(1, text.length() - 1) : text;
    }

    protected String decodeCharLiteral(Tree node) {
        assert node.getType() == CmisSqlLexer.CHARLIT;
        final String text = node.getChild(0).getText();
        return text.substring(1, text.length() - 1);
    }

    protected void processOrderBy(Tree orderByNode) {
        if (orderByNode == null) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Empty order by node.");
            }
            return;
        }
        assert orderByNode.getType() == CmisSqlLexer.ORDER;
        for (Tree sortSpec : children(orderByNode)) {
            assert sortSpec.getType() == CmisSqlLexer.SORTSPEC;
            assert sortSpec.getChildCount() >= 1;
            final String columnName = decodeIdent(sortSpec.getChild(0));
            final SortDirection direction = sortSpec.getChildCount() > 1
                    && (sortSpec.getChild(1).getType() == CmisSqlLexer.DESC) ? SortDirection.DESCENDING
                            : SortDirection.ASCENDING;
            stmt.addOrderByColumn(columnName, direction);
        }
    }

    private String getOptionalTextNode(Tree node, int type) {
        final Tree child = getFirstChildWithType(node, type);
        if (child != null && child.getChildCount() == 0) {
            throw new IllegalArgumentException("Text node of type " + type + " exists, but has no children.");
        }
        return child != null
                ? (child.getChild(0).getType() == CmisSqlLexer.IDENTIFIER ? decodeIdent(child.getChild(0))
                        : child.getChild(0).getText())
                : null;
    }

    private static Iterable<Tree> children(Tree node) {
        return new ChildIterable(node);
    }

    private static Tree getFirstChildWithType(Tree node, int type) {
        for (Tree child : children(node)) {
            if (child.getType() == type) {
                return child;
            }
        }
        return null;
    }

    private static final class ChildIterable implements Iterable<Tree> {
        private final Tree tree;

        private ChildIterable(Tree tree) {
            this.tree = tree;
        }

        @Override
        public Iterator<Tree> iterator() {
            return new Iterator<Tree>() {
                int index = 0;

                @Override
                public boolean hasNext() {
                    return index < tree.getChildCount();
                }

                @Override
                public Tree next() {
                    return tree.getChild(index++);
                }

                @Override
                public void remove() {
                    throw new UnsupportedOperationException();
                }
            };
        }
    }
}