com.reignite.parser.QueryParser.java Source code

Java tutorial

Introduction

Here is the source code for com.reignite.parser.QueryParser.java

Source

package com.reignite.parser;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;

import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.reignite.parser.exception.ParserException;
import com.reignite.query.StructuredQuery;

/*
Copyright (c) 2014 Surrey Hughes
    
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
    
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
    
The Software shall be used for Good, not Evil.
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
 * Initially accepts the query json string and uses it to create a structured
 * query.
 * 
 * <pre>
 * query {
 *  [load|count|list|sum|avg]:<object>,
 *   [fields:[<field1>,<field2>,...]]
 *   where:{},
 *    startIndex:<index>,
 *    maxResults:<maxResults>
 *       }
 * 
 * where {
 *        <field>:{
 *               [eq|not|lt|gt|in|nin|lk]:<value>
 *              }
 *  }
 * 
 * eq : == not : != lt : < gt : > in : in nin : not in lk : like
 * </pre>
 * 
 * @author Surrey
 * 
 */
public class QueryParser {

    private EntityResolver resolver;
    private StructuredQuery query;
    private QuerySecurityManager securityManager;

    /**
     * Construct a new QueryParser with the given entity resolver. The resolver
     * is used to convert query entity names to Hibernate mapped entity/class
     * names.
     * 
     * @param resolver
     */
    public QueryParser(EntityResolver resolver) {
        this(resolver, null);
    }

    /**
     * Construct a new QueryParser with the given entity resolver. The resolver
     * is used to convert query entity names to Hibernate mapped entity/class
     * names.
     * 
     * @param resolver
     */
    public QueryParser(EntityResolver resolver, QuerySecurityManager securityManager) {
        this.resolver = resolver;
        this.securityManager = securityManager;
    }

    /**
     * Converts a json string into a structured query.
     * 
     * @param queryString
     * @return
     * @throws JSONException
     */
    public StructuredQuery parse(Session session, String queryString) throws ParserException {
        JSONObject jobj;
        try {
            jobj = new JSONObject(queryString);
        } catch (JSONException e) {
            throw new ParserException("Parse failed: " + e, e);
        }

        List<Join> joins = getJoins(jobj);
        query = createQuery(session, jobj, joins);
        processJoins(jobj, joins);
        addFields(jobj);
        addWhereClause(jobj);
        addOrder(jobj);
        addGroup(jobj);
        return query;
    }

    private void processJoins(JSONObject jobj, List<Join> joins) throws ParserException {
        try {
            if (joins != null && !joins.isEmpty()) {
                if (jobj.has("joinType")) {
                    String joinType = jobj.getString("joinType");
                    if (joinType.toLowerCase().equals("perrow")) {
                        query.setJoinType(JoinType.PER_ROW);
                    }
                }
                JSONObject masterJoin = jobj.getJSONObject("joins");
                JSONArray items = masterJoin.getJSONArray("items");
                for (int i = 0; i < items.length(); i++) {
                    Join joinObj = joins.get(i);
                    JSONObject item = items.getJSONObject(i);
                    if (item.has("where")) {
                        JSONObject where = item.getJSONObject("where");
                        joinObj.setWhere(createCriterion(where));
                    }
                    if (item.has("fields")) {
                        JSONArray fields = item.getJSONArray("fields");
                        for (int y = 0; y < fields.length(); y++) {
                            // field needs to go in as <join name>.field
                            String field = fields.getString(y);
                            if (!field.startsWith(joinObj.getName() + ".")) {
                                field = joinObj.getName() + "." + field;
                            }
                            joinObj.addField(field);
                        }
                    }
                    if (item.has("startIndex")) {
                        int startIndex = 0;
                        try {
                            startIndex = item.getInt("startIndex");
                        } catch (JSONException e) {
                            throw new ParserException("startIndex must be an integer");
                        }
                        joinObj.setStartIndex(startIndex);
                    }
                    if (item.has("maxResults")) {
                        int maxResults = 0;
                        try {
                            maxResults = item.getInt("maxResults");
                        } catch (JSONException e) {
                            throw new ParserException("maxResults must be an integer");
                        }
                        joinObj.setMaxResults(maxResults);
                    }
                    processJoinOrder(item, joinObj);
                }
            }
        } catch (JSONException e) {
            throw new ParserException("Failed to create join: " + e, e);
        }
    }

    private void processJoinOrder(JSONObject jobj, Join joinObj) throws ParserException {
        if (jobj.has("order")) {
            JSONArray orderArr;
            try {
                orderArr = jobj.getJSONArray("order");
            } catch (JSONException e) {
                throw new ParserException("order must be an array");
            }
            if (orderArr.length() > 0) {
                for (int i = 0; i < orderArr.length(); i++) {
                    JSONObject orderObj;
                    try {
                        orderObj = orderArr.getJSONObject(i);
                    } catch (JSONException e) {
                        throw new ParserException("order array elements must be of the form: {asc|desc:'field'}");
                    }
                    if (orderObj.has("asc")) {
                        try {
                            joinObj.addOrder(orderObj.getString("asc"), true);
                        } catch (JSONException e) {
                            throw new ParserException("order field values must be strings.");
                        }
                    } else if (orderObj.has("desc")) {
                        try {
                            joinObj.addOrder(orderObj.getString("desc"), false);
                        } catch (JSONException e) {
                            throw new ParserException("order field values must be strings.");
                        }
                    }
                }
            }
        }
    }

    /**
     * Joins are added as additional queries and merged into the original query.
     * This is to preserve the original record count so paging works.
     * 
     * <pre>
     *     joins : {
     *             id : "id", // the field used to aggregate join rows
     *             items : [ // join added as a field
     *                {
     *                   name : "roles",
     *                   where : {// where added as an example
     *                      "roles.id" : {
     *                         gt : 0
     *                      }
     *                   },
     *                   fields : [
     *                      "roles.role"
     *                   ]
     *                // will return an array of string with the role name
     *                }
     *             ]
     *          }
     * </pre>
     * 
     * @throws ParserException
     */
    private List<Join> getJoins(JSONObject jobj) throws ParserException {
        try {
            List<Join> joinsList = new ArrayList<Join>();
            if (jobj.has("joins")) {
                JSONObject masterJoin = jobj.getJSONObject("joins");
                String id = null;
                if (masterJoin.has("id")) {
                    id = masterJoin.getString("id");
                } else {
                    throw new ParserException("Joins must have a join id.");
                }
                JSONArray joins = masterJoin.getJSONArray("items");
                for (int i = 0; i < joins.length(); i++) {
                    Join joinObj = new Join();
                    joinObj.setJoinId(id);
                    JSONObject join = joins.getJSONObject(i);
                    if (join.has("name")) {
                        String name = join.getString("name");
                        joinObj.setName(name);
                    } else {
                        throw new ParserException("Joins must have a name.");
                    }
                    joinsList.add(joinObj);
                }
            }
            return joinsList;
        } catch (JSONException e) {
            throw new ParserException("Joins must have a name and optionally a where and fields clauses");
        }
    }

    private void addGroup(JSONObject jobj) throws ParserException {
        if (jobj.has("group")) {
            JSONArray arr = null;
            try {
                arr = jobj.getJSONArray("group");
            } catch (JSONException e) {
                throw new ParserException("group must be an array of strings (field names)");
            }
            if (arr.length() > 0) {
                for (int i = 0; i < arr.length(); i++) {
                    try {
                        query.addGroup(arr.getString(i));
                    } catch (JSONException e) {
                        throw new ParserException("group elements must all be strings (field names)");
                    }
                }
            }
        }
    }

    private void addOrder(JSONObject jobj) throws ParserException {
        if (jobj.has("order")) {
            JSONArray orderArr;
            try {
                orderArr = jobj.getJSONArray("order");
            } catch (JSONException e) {
                throw new ParserException("order must be an array");
            }
            if (orderArr.length() > 0) {
                for (int i = 0; i < orderArr.length(); i++) {
                    JSONObject orderObj;
                    try {
                        orderObj = orderArr.getJSONObject(i);
                    } catch (JSONException e) {
                        throw new ParserException("order array elements must be of the form: {asc|desc:'field'}");
                    }
                    if (orderObj.has("asc")) {
                        try {
                            query.addOrder(orderObj.getString("asc"), true);
                        } catch (JSONException e) {
                            throw new ParserException("order field values must be strings.");
                        }
                    } else if (orderObj.has("desc")) {
                        try {
                            query.addOrder(orderObj.getString("desc"), false);
                        } catch (JSONException e) {
                            throw new ParserException("order field values must be strings.");
                        }
                    }
                }
            }
        }
    }

    /**
     * Where has either and / or or a field eg: where:{and:[...,...]} or
     * where:{or:[...,...]} or where:{id:{eq:23}}
     * 
     * @param query
     * @param jobj
     * @throws JSONException
     */
    private void addWhereClause(JSONObject jobj) throws ParserException {
        if (jobj.has("where")) {
            JSONObject where;
            try {
                where = jobj.getJSONObject("where");
            } catch (JSONException e) {
                throw new ParserException("the where clause must be a JSON Object: " + query.toString());
            }
            if (where.length() == 0) {
                return;
            }
            query.addWhere(createCriterion(where));
        }
    }

    private Criterion createCriterion(JSONObject where) throws ParserException {
        if (where.has(CompoundType.AND.getName())) {
            JSONArray ands;
            try {
                ands = where.getJSONArray(CompoundType.AND.getName());
            } catch (JSONException e) {
                throw new ParserException("and clause must be an array");
            }
            return createAnd(ands, 0);
        } else if (where.has(CompoundType.OR.getName())) {
            JSONArray ors;
            try {
                ors = where.getJSONArray(CompoundType.OR.getName());
            } catch (JSONException e) {
                throw new ParserException("or clause must be an array");
            }
            return createOr(ors, 0);
        } else {
            return processField(where);
        }
    }

    // [a,b,c,d]
    // and(a,and(b,and(c,d)))
    private Criterion createAnd(JSONArray ands, int index) throws ParserException {
        if (ands.length() == (index + 1)) {
            try {
                return createCriterion(ands.getJSONObject(index));
            } catch (JSONException e) {
                throw new ParserException(
                        "and clauses must be arrays with at least two elements: " + ands.toString());
            }
        }
        JSONObject andObj = null;
        try {
            andObj = ands.getJSONObject(index);
        } catch (JSONException e) {
            throw new ParserException("each element of an and clause must be a JSON Object. " + query.toString());
        }
        return Restrictions.and(createCriterion(andObj), createAnd(ands, index + 1));
    }

    // [a,b,c,d]
    // or(a,or(b,or(c,d)))
    private Criterion createOr(JSONArray ors, int index) throws ParserException {
        if (ors.length() == (index + 1)) {
            try {
                return createCriterion(ors.getJSONObject(index));
            } catch (JSONException e) {
                throw new ParserException(
                        "or clauses must be arrays with at least two elements: " + ors.toString());
            }
        }
        JSONObject orObj = null;
        try {
            orObj = ors.getJSONObject(index);
        } catch (JSONException e) {
            throw new ParserException("each element of an or clause must be a JSON Object. " + query.toString());
        }
        return Restrictions.or(createCriterion(orObj), createOr(ors, index + 1));
    }

    private Criterion processField(JSONObject where) throws ParserException {
        String field = JSONObject.getNames(where)[0];
        JSONObject exp;
        try {
            exp = where.getJSONObject(field);
        } catch (JSONException e) {
            throw new ParserException("field expressions must be JSON Object: " + field);
        }
        ExpressionType type = ExpressionType.get(JSONObject.getNames(exp)[0]);
        Object value = null;
        // if the field is a join
        if (field.indexOf(".") > -1) {
            String join = field.substring(0, field.indexOf("."));
            query.createJoin(join);
        }
        switch (type) {
        case IN:
        case NOT_IN:
            try {
                value = createArray(exp.getJSONArray(type.getName()));
            } catch (JSONException e) {
                throw new ParserException("in and not in expressions must be arrays: " + exp);
            }
            break;
        default:
            try {
                value = createValue(exp.get(type.getName()));
            } catch (JSONException e) {
                throw new ParserException("expressions must have a value: " + exp);
            }
        }

        switch (type) {
        case GREATER_THAN:
            return Restrictions.gt(field, value);
        case IN:
            return Restrictions.in(field, (Object[]) value);
        case LESS_THAN:
            return Restrictions.lt(field, value);
        case LIKE:
            MatchMode match = MatchMode.EXACT;
            String toMatch = value.toString();
            if (value.toString().startsWith("%")) {
                match = MatchMode.END;
                toMatch = toMatch.substring(1);
            }
            if (value.toString().endsWith("%")) {
                toMatch = toMatch.substring(0, toMatch.length() - 1);
                if (match == MatchMode.END) {
                    match = MatchMode.ANYWHERE;
                } else {
                    match = MatchMode.START;
                }
            }
            return Restrictions.ilike(field, toMatch, match);
        case NOT_EQUAL:
            if (value == null) {
                return Restrictions.isNotNull(field);
            }
            return Restrictions.ne(field, value);
        case NOT_IN:
            return Restrictions.not(Restrictions.in(field, (Object[]) value));
        case EQUAL:
        default:
            if (value == null) {
                return Restrictions.isNull(field);
            }
            return Restrictions.eq(field, value);
        }

    }

    private Object createArray(JSONArray jarr) throws JSONException, ParserException {
        Object[] arr = new Object[jarr.length()];
        for (int i = 0; i < jarr.length(); i++) {
            arr[i] = createValue(jarr.get(i));
        }
        return arr;
    }

    /**
     * Handles the case where a value for a criterion needs to be converted into
     * a native format like a date. To create native object use format like:
     * !Date(string,pattern) eg: Date(12/31/2013,dd/mm/yyyy)
     * 
     * @param object
     * @return
     */
    public Object createValue(Object object) throws ParserException {
        Object value = object;
        if (object instanceof String) {
            if (object == null || object.toString().toLowerCase().equals("null")) {
                return null;
            }
            String string = (String) object;
            if (string != null) {
                if (string.startsWith("!")) {
                    string = string.substring(1);
                    String typeString = string.substring(0, string.indexOf("("));
                    if (typeString.toLowerCase().equals("date")) {
                        String dateString = string.substring(string.indexOf("(") + 1, string.indexOf(","));
                        String pattern = string.substring(string.indexOf(",") + 1, string.indexOf(")"));
                        DateFormat df = new SimpleDateFormat(pattern);
                        try {
                            value = df.parse(dateString);
                        } catch (ParseException e) {
                            throw new ParserException("Failed to parse date: " + dateString + " of pattern: "
                                    + pattern + " Cause: " + e, e);
                        }
                    }
                } else if (string.startsWith("\\")) {
                    value = string.substring(1);
                }
            }
        } else if (object != null && JSONObject.NULL.equals(object)) {
            value = null;
        }
        return value;
    }

    public static void main(String[] args) throws Exception {
        QueryParser q = new QueryParser(null);
        String j = "null";
        System.out.println(q.createValue(j) == null);
    }

    private StructuredQuery createQuery(Session session, JSONObject jobj, List<Join> joins) throws ParserException {
        QueryType queryType = null;
        String queryObj = null;

        for (QueryType type : QueryType.values()) {
            if (jobj.has(type.getName())) {
                try {
                    queryObj = jobj.getString(type.getName());
                } catch (JSONException e) {
                    throw new ParserException("Query type must be of the form: {list|load|count|sum|avg:'entity'}");
                }
                if (queryObj != null) {
                    queryType = type;
                    break;
                }
            }
        }
        ClassMetadata data = resolver.resolveMetadata(session, queryObj);
        queryObj = data.getEntityName();
        checkAuthority(queryType, queryObj);
        StructuredQuery query = new StructuredQuery(queryType, data, session, joins);

        if (jobj.has("startIndex")) {
            int startIndex = 0;
            try {
                startIndex = jobj.getInt("startIndex");
            } catch (JSONException e) {
                throw new ParserException("startIndex must be an integer");
            }
            query.setStartIndex(startIndex);
        }
        if (jobj.has("maxResults")) {
            int maxResults = 0;
            try {
                maxResults = jobj.getInt("maxResults");
            } catch (JSONException e) {
                throw new ParserException("maxResults must be an integer");
            }
            query.setMaxResults(maxResults);
        }
        return query;
    }

    private void checkAuthority(QueryType queryType, String hibernateEntity) throws ParserException {
        if (securityManager != null && !securityManager.canRead(queryType, hibernateEntity)) {
            throw new ParserException("Not authorised to access objects of type: " + hibernateEntity
                    + " using query type: " + queryType.getName());
        }
    }

    private void addFields(JSONObject jobj) throws ParserException {
        if (jobj.has("fields")) {
            JSONArray fields;
            try {
                fields = jobj.getJSONArray("fields");
            } catch (JSONException e) {
                throw new ParserException("fields must be a JSONArray containing either strings or JSONObjects");
            }

            for (int i = 0; i < fields.length(); i++) {
                Object field;
                try {
                    field = fields.get(i);
                } catch (JSONException e) {
                    throw new ParserException("fields must not be an empty JSONArray if it is included.");
                }
                if (field instanceof String) {
                    query.addField(field.toString());
                } else if (field instanceof JSONObject) {
                    JSONObject jField = (JSONObject) field;
                    String[] names = JSONObject.getNames(jField);
                    for (String aggType : names) {
                        QueryType type = QueryType.valueOf(aggType.toUpperCase());
                        try {
                            // query.addField(jField.getString(aggType));
                            query.addAggregate(type, jField.getString(aggType));
                        } catch (JSONException e) {
                            throw new ParserException(
                                    "aggregate fields must be of the form {sum|avg|count:'field'}");
                        }
                    }
                }
            }
        }
    }

}