org.intermine.pathquery.PathQueryHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.intermine.pathquery.PathQueryHandler.java

Source

package org.intermine.pathquery;

/*
 * Copyright (C) 2002-2013 FlyMine
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  See the LICENSE file for more
 * information or http://www.gnu.org/copyleft/lesser.html.
 *
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.intermine.metadata.Model;
import org.intermine.objectstore.query.ConstraintOp;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Extension of DefaultHandler to handle parsing PathQuery objects
 *
 * @author Mark Woodbridge
 * @author Kim Rutherford
 * @author Thomas Riley
 * @author Matthew Wakeling
 */
public class PathQueryHandler extends DefaultHandler {
    private final Map<String, PathQuery> queries;
    private String queryName;
    protected PathQuery query;
    protected String constraintLogic = null;
    protected String currentNodePath = null;
    private Model model = null;
    protected int version;
    private List<PathConstraintSubclass> questionableSubclasses;
    /** This is a list of String type descriptions that are attribute types */
    public static final Set<String> ATTRIBUTE_TYPES = new HashSet<String>(
            Arrays.asList("boolean", "float", "double", "short", "int", "long", "Boolean", "Float", "Double",
                    "Short", "Integer", "Long", "BigDecimal", "Date", "String"));
    private StringBuilder valueBuffer = null;
    protected String constraintPath = null;
    protected Map<String, String> constraintAttributes = null;
    protected Collection<String> constraintValues = null;
    protected String constraintCode = null;
    private static final Logger LOG = Logger.getLogger(PathQueryHandler.class);

    /**
     * Collection of models used for lookup before resorting to Model.getInstanceByName(). This
     * enables multiple models to be used with the same name.
     */
    private final Map<String, Model> models = new HashMap<String, Model>();

    /**
     * Constructor
     * @param queries Map from query name to PathQuery
     * @param version the version of the xml, an attribute on the profile manager
     */
    public PathQueryHandler(Map<String, PathQuery> queries, int version) {
        this.queries = queries;
        this.version = version;
    }

    public void addModel(Model m) {
        String name = m.getName();
        models.put(name, m);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException {
        if (valueBuffer != null) {
            throw new SAXException("Cannot have any tags inside a value tag");
        }
        if ((constraintPath != null) && !("value".equals(qName) || "nullValue".equals(qName))) {
            throw new SAXException("Cannot have anything other than value tag inside a constraint");
        }
        if ("query-list".equals(qName)) {
            // Do nothing
        } else if ("query".equals(qName)) {
            queryName = validateName(attrs.getValue("name"));
            String modelName = attrs.getValue("model");
            if (models.containsKey(modelName)) {
                model = models.get(modelName);
            } else {
                try {
                    model = Model.getInstanceByName(attrs.getValue("model"));
                } catch (Exception e) {
                    throw new SAXException(e);
                }
            }
            query = new PathQuery(model);

            if (attrs.getValue("title") != null && !attrs.getValue("title").isEmpty()) {
                query.setTitle(attrs.getValue("title"));
            }

            if (attrs.getValue("longDescription") != null) {
                query.setDescription(attrs.getValue("longDescription"));
            }
            if (attrs.getValue("view") != null) {
                String view = attrs.getValue("view");
                if (view.contains(":")) {
                    // This is an old style query, and we need to convert the colons into outer
                    // joins
                    String[] viewPathArray = PathQuery.SPACE_SPLITTER.split(view.trim());
                    for (String viewPath : viewPathArray) {
                        setOuterJoins(query, viewPath);
                    }
                    view = view.replace(':', '.');
                }
                query.addViewSpaceSeparated(view);
            }
            String so = attrs.getValue("sortOrder");
            if (!StringUtils.isBlank(so)) {
                if (so.indexOf(' ') < 0) {
                    // be accomodating of input as possible - assume asc.
                    so += " asc";
                }
                query.addOrderBySpaceSeparated(so);
            }
            constraintLogic = attrs.getValue("constraintLogic");
            questionableSubclasses = new ArrayList<PathConstraintSubclass>();
        } else if ("node".equals(qName)) {
            // There's a node tag, so all constraints inside must inherit this path. Set it in a
            // variable, and reset the variable to null when we see the end tag.

            currentNodePath = attrs.getValue("path");
            if (currentNodePath.contains(":")) {
                setOuterJoins(query, currentNodePath);
                currentNodePath = currentNodePath.replace(':', '.');
            }
            String type = attrs.getValue("type");
            if ((type != null) && (!ATTRIBUTE_TYPES.contains(type))
                    && (currentNodePath.contains(".") || currentNodePath.contains(":"))) {
                PathConstraintSubclass subclass = new PathConstraintSubclass(currentNodePath, type);
                query.addConstraint(subclass);
                questionableSubclasses.add(subclass);
            }
        } else if ("constraint".equals(qName)) {
            String path = attrs.getValue("path");
            if (currentNodePath != null) {
                if (path != null) {
                    throw new SAXException("Cannot set path in a constraint inside a node");
                }
                path = currentNodePath;
            }
            String code = attrs.getValue("code");
            String type = attrs.getValue("type");
            if (type != null) {
                query.addConstraint(new PathConstraintSubclass(path, type));
            } else {
                path = path.replace(':', '.');
                constraintPath = path;
                constraintAttributes = new HashMap<String, String>();
                for (int i = 0; i < attrs.getLength(); i++) {
                    constraintAttributes.put(attrs.getQName(i), attrs.getValue(i));
                }
                constraintValues = new LinkedHashSet<String>();
                constraintCode = code;
            }
        } else if ("pathDescription".equals(qName)) {
            String pathString = attrs.getValue("pathString");
            String description = attrs.getValue("description");

            // Descriptions should only refer to classes that appear in the view, which we have
            // already read.  This check makes sure the description is for a class that has
            // attributes on the view list before adding it.  We ignore invalid descriptions to
            // cope with legacy bad validation of qyery XML.
            if (pathString.endsWith(".")) {
                throw new SAXException("Invalid path '" + pathString + "' for description: " + description);
            }
            String pathToCheck = pathString + ".";
            for (String viewString : query.getView()) {
                if (viewString.startsWith(pathToCheck)) {
                    query.setDescription(pathString, description);
                    break;
                }
            }
        } else if ("join".equals(qName)) {
            String pathString = attrs.getValue("path");
            String type = attrs.getValue("style");
            if ("INNER".equals(type.toUpperCase())) {
                query.setOuterJoinStatus(pathString, OuterJoinStatus.INNER);
            } else if ("OUTER".equals(type.toUpperCase())) {
                query.setOuterJoinStatus(pathString, OuterJoinStatus.OUTER);
            } else {
                throw new SAXException("Unknown join style " + type + " for path " + pathString);
            }
        } else if ("value".equals(qName)) {
            valueBuffer = new StringBuilder();
        }
    }

    /**
     * Process a constraint from the xml attributes.
     *
     * @param q the PathQuery, to enable creating Path objects
     * @param path the path of the constraint to create
     * @param attrs the XML attributes
     * @param values the enclosed values
     * @return a PathConstraint object
     * @throws SAXException if something is wrong
     */
    public PathConstraint processConstraint(PathQuery q, String path, Map<String, String> attrs,
            Collection<String> values) throws SAXException {

        if (path == null) {
            throw new SAXException("Bad constraint: Path is null. " + q.toString());
        }

        ConstraintOp constraintOp = ConstraintOp.getConstraintOp(attrs.get("op"));
        if (constraintOp == null) {
            // Handle any allowed synonyms.
            String origOp = attrs.get("op");
            if ("IS EMPTY".equals(origOp)) { // Synonym for IS NULL
                constraintOp = ConstraintOp.IS_NULL;
            } else if ("IS NOT EMPTY".equals(origOp)) { // Synonym for IS NOT NULL
                constraintOp = ConstraintOp.IS_NOT_NULL;
            }
        }
        // TODO: work out if this is pointless busy-work.
        if (ConstraintOp.CONTAINS.equals(constraintOp)) {
            constraintOp = ConstraintOp.CONTAINS;
        }
        if (ConstraintOp.DOES_NOT_CONTAIN.equals(constraintOp)) {
            constraintOp = ConstraintOp.DOES_NOT_CONTAIN;
        }

        if (PathConstraintRange.VALID_OPS.contains(constraintOp) && !values.isEmpty()) {
            Collection<String> valuesCollection = new LinkedHashSet<String>();
            for (String value : values) {
                valuesCollection.add(value.trim());
            }
            return new PathConstraintRange(path, constraintOp, valuesCollection);
        } else if (PathConstraintMultitype.VALID_OPS.contains(constraintOp) && !values.isEmpty()) {
            Collection<String> typesCollection = new LinkedHashSet<String>();
            for (String value : values) {
                typesCollection.add(value.trim());
            }
            return new PathConstraintMultitype(path, constraintOp, typesCollection);
        } else if (PathConstraintAttribute.VALID_OPS.contains(constraintOp)) {
            boolean isLoop = (attrs.get("loopPath") != null);
            if (PathConstraintLoop.VALID_OPS.contains(constraintOp)) {
                try {
                    Path constraintPath2 = q.makePath(path);
                    if (!constraintPath2.endIsAttribute()) {
                        isLoop = true;
                    }
                } catch (PathException e) {
                    if (isLoop) {
                        // A genuine error - rethrow
                        throw new SAXException("Illegal loop constraint definition", e);
                    } else {
                        // Not actually a loop constraint. Ignore.
                        LOG.error("Cannot recognise path in constraint: " + path, e);
                    }
                }
            }
            if (isLoop) {
                String loopPath = attrs.get("loopPath");
                if (loopPath == null) {
                    loopPath = attrs.get("value");
                }
                loopPath = loopPath.replace(':', '.');
                return new PathConstraintLoop(path, constraintOp, loopPath);
            } else {
                String constraintValue = attrs.get("value");
                return new PathConstraintAttribute(path, constraintOp, constraintValue);
            }
        } else if (PathConstraintNull.VALID_OPS.contains(constraintOp)) {
            return new PathConstraintNull(path, constraintOp);
        } else if (PathConstraintBag.VALID_OPS.contains(constraintOp)) {
            String bag = attrs.get("value");
            String ids = attrs.get("ids");
            if (bag != null) {
                return new PathConstraintBag(path, constraintOp, bag);
            } else if (ids != null) {
                String[] idArray = ids.split(",");
                Collection<Integer> idsCollection = new LinkedHashSet<Integer>();
                for (String id : idArray) {
                    try {
                        idsCollection.add(Integer.valueOf(id.trim()));
                    } catch (NumberFormatException e) {
                        throw new SAXException("List of IDs contains invalid integer: " + id, e);
                    }
                }
                return new PathConstraintIds(path, constraintOp, idsCollection);
            } else {
                throw new SAXException("Invalid query: operation was: " + constraintOp
                        + " but no bag or ids were provided (from text \"" + attrs.get("op") + "\", attributes: "
                        + attrs + ")");
            }

        } else if (PathConstraintMultiValue.VALID_OPS.contains(constraintOp)) {
            Collection<String> valuesCollection = new LinkedHashSet<String>();
            for (String value : values) {
                valuesCollection.add(value == null ? value : value.trim());
            }
            return new PathConstraintMultiValue(path, constraintOp, valuesCollection);
        } else if (ConstraintOp.LOOKUP.equals(constraintOp)) {
            String lookup = attrs.get("value");
            String extraValue = attrs.get("extraValue");
            return new PathConstraintLookup(path, lookup, extraValue);
        } else {
            throw new SAXException("Invalid operation type: " + constraintOp + " (from text \"" + attrs.get("op")
                    + "\", attributes: " + attrs + ")");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("query".equals(qName)) {
            if (constraintLogic != null) {
                query.setConstraintLogic(constraintLogic);
            }

            if (query.isValid()) {
                try {
                    Map<String, String> subClasses = query.getSubclasses();
                    for (PathConstraintSubclass subclass : questionableSubclasses) {
                        Map<String, String> trimmedSubclasses = new HashMap<String, String>(subClasses);
                        trimmedSubclasses.remove(subclass.getPath());
                        Path path = new Path(model, subclass.getPath(), trimmedSubclasses);
                        if (path.getEndClassDescriptor().getUnqualifiedName().equals(subclass.getType())) {
                            query.removeConstraint(subclass);
                        }
                    }
                } catch (PathException e) {
                    // Shouldn't ever happen, as the query is valid
                    throw new Error("Error", e);
                }
            }
            queries.put(queryName, query);
        } else if ("node".equals(qName)) {
            currentNodePath = null;
        } else if ("constraint".equals(qName) && (constraintPath != null)) {
            PathConstraint constraint = processConstraint(query, constraintPath, constraintAttributes,
                    constraintValues);
            if (constraintCode == null) {
                query.addConstraint(constraint);
            } else {
                query.addConstraint(constraint, constraintCode);
            }
            constraintPath = null;
        } else if ("value".equals(qName)) {
            if (valueBuffer == null || valueBuffer.length() < 1) {
                throw new NullPointerException("No value provided in value tag." + " Failed for template query: "
                        + queryName + " on constraint: " + constraintPath);
            }
            constraintValues.add(valueBuffer.toString());
            valueBuffer = null;
        } else if ("nullValue".equals(qName)) {
            constraintValues.add(null);
            valueBuffer = null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void characters(char[] ch, int start, int length) {
        if (valueBuffer != null) {
            valueBuffer.append(ch, start, length);
        }
    }

    /**
     * Convert a List of Objects to a List of Strings using toString
     * @param list the Object List
     * @return the String list
     */
    protected List<String> toStrings(List<Object> list) {
        List<String> strings = new ArrayList<String>();
        for (Object o : list) {
            strings.add(o.toString());
        }
        return strings;
    }

    /**
     * Checks that the query has a name and that there's no name duplicates
     * and appends a number to the name if there is.
     * @param name the query name
     * @return the validated query name
     */
    protected String validateName(String name) {
        String validatedName = name;
        if (name == null || name.length() == 0) {
            validatedName = "unnamed_query";
        }
        if (queries.containsKey(validatedName)) {
            int i = 1;
            while (true) {
                String testName = validatedName + "_" + i;
                if (!queries.containsKey((testName))) {
                    return testName;
                }
                i++;
            }
        }
        return validatedName;
    }

    /**
     * Given a path that may contain ':' characters to represent outer joins, find each : separated
     * segment and set the status for that join to OUTER.  NOTE this method will change the
     * query parameter handed to it.
     * @param query the query to set join styles
     * @param path a path that may contain outer joins represented by ':'
     */
    protected void setOuterJoins(PathQuery query, String path) {
        int from = 0;
        while (path.indexOf(':', from) != -1) {
            int colonPos = path.indexOf(':', from);
            int nextDot = path.replace(':', '.').indexOf('.', colonPos + 1);
            String outerJoin = nextDot == -1 ? path : path.substring(0, nextDot);
            query.setOuterJoinStatus(outerJoin.replace(':', '.'), OuterJoinStatus.OUTER);
            from = colonPos + 1;
        }
    }
}