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

tools.dynamia.domain.util.QueryBuilder Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2023 Dynamia Soluciones IT S.A.S - NIT 900302344-1
 * Colombia / South America
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package tools.dynamia.domain.util;

import tools.dynamia.commons.BeanMap;
import tools.dynamia.commons.BeanSorter;
import tools.dynamia.domain.ValidationError;
import tools.dynamia.domain.query.AbstractQueryCondition;
import tools.dynamia.domain.query.BooleanOp;
import tools.dynamia.domain.query.Group;
import tools.dynamia.domain.query.QueryCondition;
import tools.dynamia.domain.query.QueryConditionGroup;
import tools.dynamia.domain.query.QueryConditions;
import tools.dynamia.domain.query.QueryParameters;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * This is a simple JPA query text builder.
 *
 * @author Mario A. Serrano Leones
 */
@SuppressWarnings({"rawtypes"})
public class QueryBuilder implements Cloneable {


    enum QueryType {
        SELECT, UPDATE, DELETE
    }

    private QueryType queryType = QueryType.SELECT;

    private static final String FROM_WORD = "from";


    private QueryParameters queryParameters;
    private Class type;
    private String var = "t";
    private String select;
    private String from;
    private List wheres = new ArrayList<>();
    private List orders = new ArrayList<>();
    private List groups = new ArrayList<>();
    private List joins = new ArrayList<>();
    private Class resultType;
    private String[] fields;
    private String customSelect;
    private String customFrom;
    private boolean builded;
    private Map mapFields;


    public boolean isAppendVarNameToFields() {
        return appendVarNameToFields;
    }

    public void setAppendVarNameToFields(boolean appendVarNameToFields) {
        this.appendVarNameToFields = appendVarNameToFields;
    }

    private boolean appendVarNameToFields = true;

    /**
     * The Constant SPACE.
     */
    private static final String SPACE = " ";

    /**
     * Instantiates a new query builder.
     */
    private QueryBuilder() {
    }

    /**
     * Build a new QueryBuilder for the type, example: String queryText =
     * QueryBuilder.select(Person.class,"p").toString(); is the same that:
     * String queryText = "select p from Person p"; but QueryBuilder allow you
     * use or, ands, create projections
     *
     * @param type   the type
     * @param var    the var
     * @param fields the fields
     * @return the query builder
     */
    public static QueryBuilder select(Class type, String var, String... fields) {
        return select(type, null, var, fields);
    }

    /**
     * Build a new QueryBuilder for the type, example: String queryText =
     * QueryBuilder.select(Person.class,"p").toString(); is the same that:
     * String queryText = "select p from Person p"; but QueryBuilder allow you
     * use or, ands, create projections using a result type with appropiate
     * constructor
     *
     * @param entityType the type
     * @param var        the var
     * @param fields     the fields
     * @return the query builder
     */
    public static QueryBuilder select(Class entityType, Class resultType, String var, String... fields) {
        QueryBuilder qb = new QueryBuilder();
        qb.type = entityType;
        qb.var = var;
        qb.resultType = resultType;
        qb.fields = fields;
        qb.queryType = QueryType.SELECT;
        return qb;
    }

    /**
     * If fields is empty means all fields
     *
     */
    public static QueryBuilder select(String... fields) {
        QueryBuilder qb = new QueryBuilder();
        qb.fields = fields;
        qb.queryType = QueryType.SELECT;
        return qb;
    }

    /**
     * Query type and query var
     *
     */
    public QueryBuilder from(Class entityType, String var) {
        this.type = entityType;
        this.var = var;
        this.customFrom = null;
        return this;
    }

    /**
     * List result type can be different from query type, use this to create DTO
     * like collections
     *
     */
    public QueryBuilder resultType(Class resultType) {
        this.resultType = resultType;
        return this;
    }

    public QueryBuilder build() {
        if (!builded) {
            toString();
            builded = true;
        }
        return this;
    }

    /**
     * Crearte a QueryBuilder object using the QueryParameters in the where
     * clause example: QueryParameters qp =
     * QueryParameters.with("name","mario"); String queryText =
     * QueryBuilder.fromParameters(Person.class,"p",qp).toString(); produce:
     * "select p from Person p where p.name like :name";
     *
     * @param type       the type
     * @param var        the var
     * @param parameters the parameters
     * @return the query builder
     */
    public static QueryBuilder fromParameters(Class type, String var, QueryParameters parameters) {

        return QueryBuilder.select().from(type, var).where(parameters);
    }

    private void configureParameters() {
        if (queryParameters == null || builded) {
            return;
        }

        if (!queryParameters.isEmpty()) {
            for (String property : queryParameters.getSortedKeys()) {
                Object value = queryParameters.get(property);
                if (value != null) {
                    if (value instanceof QueryCondition qc) {
                        if (qc instanceof QueryConditionGroup qcGroup) {
                            for (QueryCondition nestedQC : qcGroup.getValue()) {
                                addCondition(property, nestedQC, nestedQC.getBooleanOperator());
                            }
                        } else {
                            addCondition(property, qc, qc.getBooleanOperator());
                        }
                    } else if (value instanceof String && queryParameters.isAutocreateSearcheableStrings()) {
                        addCondition(property, QueryConditions.like(value), BooleanOp.AND);
                    } else {
                        String condition = QueryConditions.eq(value).render(property);
                        condition = appendVarNameToFields ? var + "." + condition : condition;
                        and(condition);
                    }
                }
            }
        }
        if (queryParameters.getGroups() != null) {
            for (Group group : queryParameters.getGroups()) {
                if (group.params() != null && !group.params().isEmpty()) {
                    QueryBuilder subquery = QueryBuilder.fromParameters(type, var, group.params()).build();
                    String subqueryWhere = "(" + subquery.parse(subquery.wheres, " ") + ")";

                    if (group.booleanOp() == BooleanOp.AND) {
                        and(subqueryWhere);
                    } else {
                        or(subqueryWhere);
                    }
                }
            }
        }

        if (queryParameters.isSorted()) {
            BeanSorter sorter = queryParameters.getSorter();
            String direction = sorter.isAscending() ? "ASC" : "DESC";
            if (sorter.getColumnName().toLowerCase().endsWith(" desc")
                    || sorter.getColumnName().toLowerCase().endsWith(" asc")) {
                direction = " ";
            }

            String orderField = sorter.getColumnName() + " " + direction;
            orderBy(orderField);

        }
    }

    /**
     * Adds the condition.
     *
     * @param property  the property
     * @param qc        the qc
     */
    private void addCondition(String property, QueryCondition qc, BooleanOp booleanOp) {
        String condition = renderCondition(property, qc);
        if (condition != null) {


            if (booleanOp == BooleanOp.AND) {
                and(condition);
            } else if (booleanOp == BooleanOp.OR) {
                or(condition);
            }


            if (qc instanceof AbstractQueryCondition) {
                ((AbstractQueryCondition) qc).setBooleanOperator(booleanOp);
            }
        }
    }

    private String renderCondition(String property, QueryCondition qc) {
        String render = qc.render(property);
        return checkFieldVar(render);
    }

    /**
     * Where.
     *
     * @param conditions the conditions
     * @return the query builder
     */
    public QueryBuilder where(String conditions) {
        if (!wheres.contains(conditions)) {
            wheres.add(conditions);
        }
        return this;
    }

    /**
     * Where.
     *
     * @param params the params
     * @return the query builder
     */
    public QueryBuilder where(QueryParameters params) {
        this.queryParameters = params;
        return this;
    }

    public QueryBuilder where(String field, QueryCondition condition) {
        where(QueryParameters.with(field, condition));
        where(renderCondition(field, condition));
        return this;

    }

    /**
     * And.
     *
     * @param condition the condition
     * @return the query builder
     */
    public QueryBuilder and(String condition) {
        return and(condition, true);
    }

    /**
     * And.
     *
     * @param condition the condition
     * @param append    the append
     * @return the query builder
     */
    public QueryBuilder and(String condition, boolean append) {
        if (!wheres.contains(condition)) {
            if (!wheres.isEmpty()) {
                condition = "and " + condition;
            }
            if (append) {
                where(condition);
            }
        }
        return this;
    }

    public QueryBuilder and(String field, QueryCondition condition) {
        addCondition(field, condition, BooleanOp.AND);
        getQueryParameters().add(field, condition);
        return this;
    }

    /**
     * Or.
     *
     * @param condition the condition
     * @return the query builder
     */
    public QueryBuilder or(String condition) {
        return or(condition, true);
    }

    /**
     * Or.
     *
     * @param condition the condition
     * @param append    the append
     * @return the query builder
     */
    public QueryBuilder or(String condition, boolean append) {
        if (!wheres.contains(condition)) {
            if (!wheres.isEmpty()) {
                condition = "or " + condition;
            }
            if (append) {
                where(condition);
            }
        }
        return this;
    }

    public QueryBuilder or(String field, QueryCondition condition) {
        addCondition(field, condition, BooleanOp.OR);
        getQueryParameters().add(field, condition);
        return this;
    }

    /**
     * Order by.
     *
     * @param field         the field
     * @param anotherFields the another fields
     * @return the query builder
     */
    public QueryBuilder orderBy(String field, String... anotherFields) {
        addField(orders, field);
        addFields(orders, anotherFields);
        return this;
    }

    /**
     * Group by.
     *
     * @param field         the field
     * @param anotherFields the another fields
     * @return the query builder
     */
    public QueryBuilder groupBy(String field, String... anotherFields) {
        addField(groups, field);
        addFields(groups, anotherFields);
        return this;
    }

    private void addFields(List list, String[] fields) {
        if (fields != null) {
            for (String field : fields) {
                addField(list, field);
            }
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {

        return switch (queryType) {
            case SELECT -> buildSelect();
            case UPDATE -> buildUpdate();
            case DELETE -> buildDelete();
            default -> buildSelect();
        };
    }

    private String buildSelect() {
        configureParameters();

        if (customSelect != null && !customSelect.isEmpty()) {
            select = customSelect;
        } else {
            String parseFields = var;
            if (fields != null && fields.length > 0) {
                List fieldsList = new ArrayList<>();
                for (String field : fields) {
                    addField(fieldsList, field);
                }
                parseFields = parse(fieldsList, ", ");
            }

            String fieldList = parseFields;
            if (resultType != null && resultType != BeanMap.class) {
                fieldList = " new " + resultType.getName() + "(" + parseFields + ") ";
            }
            select = "select " + fieldList;
        }

        if (customFrom != null) {
            from = customFrom + SPACE;
        } else {
            from = FROM_WORD + SPACE + type.getName() + SPACE + var;
        }

        String whereWord = getWhereWord();
        String parseGroups = groups.isEmpty() ? "" : " group by " + parse(groups, ", ");
        String parseOrders = orders.isEmpty() ? "" : " order by " + parse(orders, ", ");
        String parseJoins = joins.isEmpty() ? "" : parse(joins, " ") + SPACE;

        return select + SPACE + from + SPACE + parseJoins + whereWord + SPACE + parse(wheres, " ") + parseGroups + parseOrders;
    }

    private String buildUpdate() {
        configureParameters();

        if (mapFields == null || mapFields.isEmpty()) {
            throw new ValidationError("No fields to update provided");
        }


        String parseFields = var;

        List fieldsList = new ArrayList<>();
        mapFields.forEach((field, value) -> {

            String parsedValue;
            if (value instanceof String) {
                parsedValue = (String) value;
            } else {
                String key = "newValue" + field.replace(".", "");
                getQueryParameters().add(key, value);
                parsedValue = ":" + key;
            }

            var exp = field + "=" + parsedValue;
            addField(fieldsList, exp);
        });

        parseFields = parse(fieldsList, ", ");


        String fieldList = parseFields;

        String update = "update";


        if (customFrom != null) {
            from = customFrom + SPACE;
        } else {
            from = SPACE + type.getName() + SPACE + var;
        }

        String whereWord = getWhereWord();
        String parseJoins = joins.isEmpty() ? "" : parse(joins, " ") + SPACE;

        return update + from + SPACE + parseJoins + "set" + SPACE + fieldList + SPACE + whereWord + SPACE + parse(wheres, " ");
    }

    private String buildDelete() {
        configureParameters();

        String delete = "delete";


        if (customFrom != null) {
            from = customFrom + SPACE;
        } else {
            from = SPACE + type.getName() + SPACE + var;
        }

        String whereWord = getWhereWord();
        String parseJoins = joins.isEmpty() ? "" : parse(joins, " ") + SPACE;

        return delete + SPACE + FROM_WORD + from + SPACE + parseJoins + whereWord + SPACE + parse(wheres, " ");
    }


    private String parse(List list, String delimiter) {
        String parsed = "";
        if (!list.isEmpty()) {
            parsed = String.join(delimiter, list);
        }
        return parsed;
    }

    private void addField(List list, String field) {
        String toAdd = checkFieldVar(field);

        if (!list.contains(toAdd)) {
            list.add(toAdd);
        }
    }

    private String checkFieldVar(String field) {
        String toAdd = field;
        if (!field.startsWith("(") && !field.contains(var + ".") && appendVarNameToFields) {
            toAdd = var + "." + field;
        }
        return toAdd;
    }

    /**
     * Gets the where word.
     *
     * @return the where word
     */
    private String getWhereWord() {
        String whereWord = "";
        if (wheres != null && !wheres.isEmpty()) {
            whereWord = "where";
        }
        return whereWord;
    }

    /**
     * create a projection using a jpql function name (count, sum,avg...)
     *
     * @param function the function
     * @param field    the field
     * @return the string
     */
    public String createProjection(String function, String field) {
        build();
        String whereWord = getWhereWord();
        String varName = getVarName();

        String projection = "select " + function + "(" + varName + field + ") ";
        String parseJoins = joins.isEmpty() ? "" : parse(joins, " ") + SPACE;
        return projection + from + SPACE + parseJoins + whereWord + SPACE + parse(wheres, " ");
    }

    public String getVarName() {
        return appendVarNameToFields ? var + "." : "";
    }

    /**
     * Custom select.
     *
     * @param select the select
     * @return the query builder
     */
    public QueryBuilder customSelect(String select) {
        this.customSelect = select;
        return this;
    }

    /**
     * Custom from.
     *
     * @param from the from
     * @return the query builder
     */
    public QueryBuilder customFrom(String from) {
        this.customFrom = from;
        return this;
    }

    public QueryBuilder innerJoin(String join) {
        joins.add("inner join " + join);
        return this;
    }

    public QueryBuilder leftJoin(String join) {
        joins.add("left join " + join);
        return this;
    }

    public QueryBuilder rightJoin(String join) {
        joins.add("right join " + join);
        return this;
    }

    /**
     * Gets the type.
     *
     * @return the type
     */
    public Class getType() {
        return type;
    }

    /**
     * Gets the query parameters.
     *
     * @return the query parameters
     */
    public QueryParameters getQueryParameters() {
        if (queryParameters == null) {
            queryParameters = new QueryParameters();
            queryParameters.setType(getType());
        }
        return queryParameters;
    }

    public Class getResultType() {
        return resultType;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#clone()
     */
    @Override
    public QueryBuilder clone() {
        QueryBuilder qb = new QueryBuilder();
        qb.var = var;
        qb.from = from;
        qb.customFrom = customFrom;
        qb.select = select;
        qb.type = type;
        qb.resultType = resultType;
        qb.fields = fields;
        qb.wheres = new ArrayList<>(wheres);
        qb.joins = new ArrayList<>(joins);
        qb.orders = new ArrayList<>(orders);
        qb.groups = new ArrayList<>(groups);
        qb.queryParameters = queryParameters;
        qb.appendVarNameToFields = appendVarNameToFields;


        return qb;
    }

    public String[] getFields() {
        return fields;
    }

    /**
     * Update query
     *
     */
    public static QueryBuilder update(Class entityType, String var) {
        var builder = new QueryBuilder();
        builder.queryType = QueryType.UPDATE;
        builder.type = entityType;
        builder.var = var;
        builder.customSelect = null;

        return builder;
    }

    public QueryBuilder set(Map fields) {
        if (queryType != QueryType.UPDATE) {
            throw new ValidationError("Query builder type should be " + QueryType.UPDATE);
        }

        this.mapFields = fields;
        return this;
    }

    public static QueryBuilder delete(Class entityType, String var) {
        var builder = new QueryBuilder();
        builder.queryType = QueryType.DELETE;
        builder.type = entityType;
        builder.var = var;
        builder.customSelect = null;

        return builder;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy