org.intermine.web.logic.template.TemplateHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.intermine.web.logic.template.TemplateHelper.java

Source

package org.intermine.web.logic.template;

/*
 * 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.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.intermine.api.profile.InterMineBag;
import org.intermine.api.template.ApiTemplate;
import org.intermine.objectstore.query.ConstraintOp;
import org.intermine.pathquery.OrderElement;
import org.intermine.pathquery.Path;
import org.intermine.pathquery.PathConstraint;
import org.intermine.pathquery.PathConstraintBag;
import org.intermine.pathquery.PathConstraintLookup;
import org.intermine.pathquery.PathConstraintMultiValue;
import org.intermine.pathquery.PathConstraintNull;
import org.intermine.pathquery.PathException;
import org.intermine.pathquery.PathQuery;
import org.intermine.template.SwitchOffAbility;
import org.intermine.template.TemplateQuery;
import org.intermine.template.TemplateValue;
import org.intermine.template.xml.TemplateQueryBinding;
import org.intermine.webservice.server.CodeTranslator;

/**
 * Static helper routines related to templates.
 *
 * @author Thomas Riley
 * @author Richard Smith
 */
public final class TemplateHelper {
    private static final String OPERATION_PARAMETER = "op";
    private static final String EXTRA_PARAMETER = "extra";
    private static final String VALUE_PARAMETER = "value";
    private static final String ID_PARAMETER = "constraint";
    private static final String CODE_PARAMETER = "code";

    /**
     * A logger.
     */
    private static final Logger LOG = Logger.getLogger(TemplateHelper.class);

    private TemplateHelper() {
        // don't
    }

    /**
     * Given a Map of TemplateQueries (mapping from template name to TemplateQuery)
     * return a string containing each template seriaised as XML. The root element
     * will be a <code>template-queries</code> element.
     *
     * @param templates  map from template name to TemplateQuery
     * @param version the version number of the XML format
     * @return  all template queries serialised as XML
     * @see  TemplateQuery
     */
    public static String templateMapToXml(Map<String, TemplateQuery> templates, int version) {
        StringWriter sw = new StringWriter();
        XMLOutputFactory factory = XMLOutputFactory.newInstance();

        try {
            XMLStreamWriter writer = factory.createXMLStreamWriter(sw);
            writer.writeStartElement("template-queries");
            for (TemplateQuery template : templates.values()) {
                TemplateQueryBinding.marshal(template, writer, version);
            }
            writer.writeEndElement();
        } catch (XMLStreamException e) {
            throw new RuntimeException(e);
        }
        return sw.toString();
    }

    /**
     * Removed from the view all the direct attributes that aren't editable constraints
     * @param tq The template to remove attributes from.
     * @return altered template query
     */
    public static TemplateQuery removeDirectAttributesFromView(TemplateQuery tq) {
        TemplateQuery templateQuery = tq.clone();
        List<String> viewPaths = templateQuery.getView();
        String rootClass = null;
        try {
            rootClass = templateQuery.getRootClass();
            for (String viewPath : viewPaths) {
                Path path = templateQuery.makePath(viewPath);
                if (path.getElementClassDescriptors().size() == 1
                        && path.getLastClassDescriptor().getUnqualifiedName().equals(rootClass)) {
                    if (templateQuery.getEditableConstraints(viewPath).isEmpty()) {
                        templateQuery.removeView(viewPath);
                        for (OrderElement oe : templateQuery.getOrderBy()) {
                            if (oe.getOrderPath().equals(viewPath)) {
                                templateQuery.removeOrderBy(viewPath);
                                break;
                            }
                        }
                    }
                }
            }
        } catch (PathException pe) {
            LOG.error("Error updating the template's view", pe);
        }
        return templateQuery;
    }

    /**
     * Serialse a map of templates to XML.
     * @param templates The templates to serialise.
     * @param version The UserProfile version to use.
     * @return An XML serialise.
     */
    public static String apiTemplateMapToXml(Map<String, ApiTemplate> templates, int version) {
        return templateMapToXml(downCast(templates), version);
    }

    private static Map<String, TemplateQuery> downCast(Map<String, ApiTemplate> templates) {
        Map<String, TemplateQuery> ret = new HashMap<String, TemplateQuery>();
        for (Entry<String, ApiTemplate> pair : templates.entrySet()) {
            ret.put(pair.getKey(), pair.getValue());
        }
        return ret;
    }

    /**
     * Transform a map of templates into a map of API templates.
     * @param templates The original, non-api templates.
     * @return templates brought into the realm of the API.
     */
    public static Map<String, ApiTemplate> upcast(Map<String, TemplateQuery> templates) {
        Map<String, ApiTemplate> ret = new HashMap<String, ApiTemplate>();
        for (Entry<String, TemplateQuery> pair : templates.entrySet()) {
            ret.put(pair.getKey(), new ApiTemplate(pair.getValue()));
        }
        return ret;
    }

    /**
     * Routine for serialising map of templates to JSON.
     * @param templates The templates to serialise.
     * @return A JSON string.
     */
    public static String templateMapToJson(Map<String, TemplateQuery> templates) {
        StringBuilder sb = new StringBuilder("{");
        Iterator<String> keys = templates.keySet().iterator();
        while (keys.hasNext()) {
            String name = keys.next();
            sb.append("\"" + name + "\":" + templates.get(name).toJSON());
            if (keys.hasNext()) {
                sb.append(",");
            }
        }
        sb.append("}");
        String result = sb.toString();
        return result;
    }

    /**
     * Helper routine for serialising a map of templates to JSON.
     * @param templates The map of templates to serialise.
     * @return A JSON string.
     */
    public static String apiTemplateMapToJson(Map<String, ApiTemplate> templates) {
        return templateMapToJson(downCast(templates));
    }

    /**
     * Parse templates in XML format and return a map from template name to
     * TemplateQuery.
     *
     * @param xml         the template queries in xml format
     * @param savedBags   Map from bag name to bag
     * @param version the version of the xml format, an attribute on ProfileManager
     * @return            Map from template name to TemplateQuery
     * @throws Exception  when a parse exception occurs (wrapped in a RuntimeException)
     */
    public static Map<String, TemplateQuery> xmlToTemplateMap(String xml, Map<String, InterMineBag> savedBags,
            int version) throws Exception {
        Reader templateQueriesReader = new StringReader(xml);
        return TemplateQueryBinding.unmarshalTemplates(templateQueriesReader, version);
    }

    /**
     * Given a HTTP request, parse out the template values.
     * 
     * A template value is expected to be encoded such as:
     * <pre><code>
     *   constraintX=Gene
     *   opX=LOOKUP
     *   valueX=eve
     *   extraX=D.%20melanogaster
     * </code></pre>
     * where X is an integer from 1 - 25. The only element that may be omitted is
     * the "extra" parameter where none is expected.
     *
     * @param request HTTP request by user
     * @return map of constraints and values to be used to populate template.
     * @throws TemplateValueParseException if the request parameters are bad.
     */
    public static Map<String, List<ConstraintInput>> parseConstraints(HttpServletRequest request)
            throws TemplateValueParseException {
        // Maximum number of constraints is determined by the valid code range
        // on PathQueries.
        Map<String, List<ConstraintInput>> ret = new HashMap<String, List<ConstraintInput>>();
        Set<String> processedIds = new HashSet<String>();
        for (int i = 1; i <= PathQuery.MAX_CONSTRAINTS; i++) {

            String idParameter = ID_PARAMETER + i;
            String id = request.getParameter(idParameter);
            processedIds.add(idParameter);

            String opParameter = OPERATION_PARAMETER + i;
            String opString = request.getParameter(opParameter);
            ConstraintOp op = getConstraintOp(opParameter, opString);

            String valueParameter = VALUE_PARAMETER + i;
            String[] values = request.getParameterValues(valueParameter);
            String value = null;
            List<String> multivalues = null;
            if (values != null) {
                value = values[0];
                multivalues = Arrays.asList(values);
            }

            String extraParameter = EXTRA_PARAMETER + i;
            String extraValue = request.getParameter(extraParameter);

            String codeParameter = CODE_PARAMETER + i;
            String code = request.getParameter(codeParameter);

            if (opString != null && opString.length() > 0 && op == null) {
                throw new TemplateValueParseException("invalid parameter: '" + opParameter + "' with value '"
                        + opString + "': " + "This must be valid operation code. "
                        + "Special characters must be encoded in request. " + " See help for 'url encoding'.");
            }

            if (isPresent(op) || isPresent(value) || isPresent(id) || isPresent(extraValue) || isPresent(code)
                    || multivalues != null) {
                String problemIntro = "parameters were provided for constraint " + i;
                if (!isPresent(id)) {
                    throw new TemplateValueParseException(
                            problemIntro + " but no path was provided to identify the "
                                    + "constraint. Missing parameter: '" + idParameter + "'.");
                }
                if (!isPresent(op)) {
                    throw new TemplateValueParseException(problemIntro + " but the operation was not specified."
                            + " Missing parameter '" + opParameter + "'.");
                }
                if (!PathConstraintNull.VALID_OPS.contains(op)
                        && (request.getParameterValues(valueParameter) == null)) {
                    throw new TemplateValueParseException(problemIntro + " but no values were provided, and " + op
                            + " requires at least one value. Missing" + " parameter '" + valueParameter + "'.");
                }
                if (!PathConstraintMultiValue.VALID_OPS.contains(op) && multivalues != null
                        && multivalues.size() > 1) {
                    throw new TemplateValueParseException(" An operation was provided ('" + op + "') "
                            + " that expected at most one value, but " + multivalues.size()
                            + " values were provided using the parameter '" + valueParameter + "'.");
                }

                ConstraintInput load = new ConstraintInput(idParameter, id, code, op, value, multivalues,
                        extraValue);
                if (ret.get(id) == null) {
                    ret.put(id, new ArrayList<ConstraintInput>());
                }
                ret.get(id).add(load);
            }
        }
        // Make sure there aren't any extra parameters hanging around.
        // Use the id parameters (eg. constraint1, constraint2, ...) as a proxy
        // for the whole constraint.
        Set<String> allIdParameters = new HashSet<String>();
        for (Enumeration<String> e = request.getParameterNames(); e.hasMoreElements();) {
            String next = e.nextElement();
            if (next.startsWith("constraint")) {
                allIdParameters.add(next);
            }
        }
        allIdParameters.removeAll(processedIds);
        if (allIdParameters.size() > 0) {
            throw new TemplateValueParseException("Maximum number of template parameters ("
                    + PathQuery.MAX_CONSTRAINTS + ") exceeded. " + "The extra values were :" + allIdParameters);
        }
        return ret;
    }

    private static ConstraintOp getConstraintOp(String parName, String parValue)
            throws TemplateValueParseException {
        ConstraintOp ret = ConstraintOp.getConstraintOp(CodeTranslator.getCode(parValue));
        if (parValue != null && ret == null) {
            throw new TemplateValueParseException(
                    "Problem with parameter '" + parName + "': '" + parValue + "' is not a valid operator.");
        }
        return ret;
    }

    private static boolean isPresent(String value) {
        return (value != null && value.length() > 0);
    }

    private static boolean isPresent(ConstraintOp op) {
        return (op != null);
    }

    public static class TemplateValueParseException extends Exception {
        private static final long serialVersionUID = -6128402589193631537L;

        public TemplateValueParseException(String message) {
            super(message);
        }
    }

    /**
     * Creates a map from input to be used later to populate the template.
     *
     * @param template template
     * @param input values from URL
     * @return map from constraints to values
     * @throws TemplateValueParseException if the input is bad.
     */
    public static Map<String, List<TemplateValue>> getValuesFromInput(TemplateQuery template,
            TemplateResultInput input) throws TemplateValueParseException {
        Map<String, List<TemplateValue>> values = new HashMap<String, List<TemplateValue>>();
        for (String path : template.getEditablePaths()) {
            List<PathConstraint> constraintsForPath = template.getEditableConstraints(path);
            List<ConstraintInput> inputsForPath = new ArrayList<ConstraintInput>();
            if (input.getConstraints().get(path) != null) {
                inputsForPath.addAll(input.getConstraints().get(path));
            }

            // too many inputs for path
            if (constraintsForPath.size() < inputsForPath.size()) {
                throw new TemplateValueParseException("There were more constraints specified "
                        + " in the request than there are editable constraints for path " + path + ".");
            }

            if (constraintsForPath.size() == 1) {
                // one constraint and at most one input
                PathConstraint con = constraintsForPath.get(0);
                ConstraintInput conInput = null;
                if (!inputsForPath.isEmpty()) {
                    conInput = inputsForPath.get(0);
                }
                checkAndAddValue(values, template, con, conInput, null);
            } else {
                // more than one constraint so we need to look at codes
                for (PathConstraint con : constraintsForPath) {
                    ConstraintInput foundConInput = null;
                    String code = template.getConstraints().get(con);
                    for (ConstraintInput conInput : inputsForPath) {
                        if (StringUtils.isBlank(conInput.getCode())) {
                            String err = "There are multiple editable constraints" + "for path " + path
                                    + " but codes weren't set.  If there is"
                                    + " more than one constraint on a path you need to specify the"
                                    + " corresponding constraint codes.";
                            throw new TemplateValueParseException(err);
                        }
                        if (conInput.getCode().equals(code)) {
                            if (foundConInput != null) {
                                String err = "There was more than one constraint" + " specified with code: "
                                        + conInput.getCode() + " in the request for path: " + path
                                        + "  You should only" + " provide one value per code.";
                                throw new TemplateValueParseException(err);
                            }
                            foundConInput = conInput;
                        }
                    }
                    // foundConInput may be null but that's ok if the constraint is optional
                    checkAndAddValue(values, template, con, foundConInput, code);
                }
            }
        }
        return values;
    }

    private static void checkAndAddValue(Map<String, List<TemplateValue>> values, TemplateQuery template,
            PathConstraint con, ConstraintInput conInput, String code) throws TemplateValueParseException {
        if (conInput != null) {
            if (template.isRequired(con)) {
                addToValuesMap(values, createTemplateValue(con, conInput, SwitchOffAbility.LOCKED));
            } else {
                addToValuesMap(values, createTemplateValue(con, conInput, SwitchOffAbility.ON));
            }
        } else if (template.isRequired(con)) {
            throw new TemplateValueParseException("There isn't a specified constraint value "
                    + "and operation for path " + con.getPath() + ((code != null) ? " and code " + code : "")
                    + " in the request; this constraint is required.");
        } else {
            // no value was provided but the constraint was optional so we can do nothing
        }
    }

    private static void addToValuesMap(Map<String, List<TemplateValue>> valMap, TemplateValue newValue) {
        String path = newValue.getConstraint().getPath();
        List<TemplateValue> values = valMap.get(path);
        if (values == null) {
            values = new ArrayList<TemplateValue>();
            valMap.put(path, values);
        }
        values.add(newValue);
    }

    private static TemplateValue createTemplateValue(PathConstraint con, ConstraintInput input,
            SwitchOffAbility switchOffAbility) {
        TemplateValue value;
        if (PathConstraintBag.VALID_OPS.contains(input.getConstraintOp())) {
            value = new TemplateValue(con, input.getConstraintOp(), input.getValue(),
                    TemplateValue.ValueType.BAG_VALUE, switchOffAbility);
        } else if (con instanceof PathConstraintLookup) {
            value = new TemplateValue(con, input.getConstraintOp(), input.getValue(),
                    TemplateValue.ValueType.SIMPLE_VALUE, input.getExtraValue(), switchOffAbility);
        } else if (con instanceof PathConstraintBag) {
            value = new TemplateValue(con, input.getConstraintOp(), input.getValue(),
                    TemplateValue.ValueType.BAG_VALUE, switchOffAbility);
        } else {
            if (PathConstraintMultiValue.VALID_OPS.contains(input.getConstraintOp())) {
                value = new TemplateValue(con, input.getConstraintOp(), TemplateValue.ValueType.SIMPLE_VALUE,
                        input.getMultivalues(), switchOffAbility);
            } else if (input.getValue() != null) {
                value = new TemplateValue(con, input.getConstraintOp(), input.getValue(),
                        TemplateValue.ValueType.SIMPLE_VALUE, switchOffAbility);
            } else {
                // For unary (null) constraints.
                value = new TemplateValue(con, input.getConstraintOp(), TemplateValue.ValueType.SIMPLE_VALUE,
                        switchOffAbility);
            }
        }
        return value;
    }
}