de.azapps.mirakel.model.query_builder.MirakelQueryBuilder.java Source code

Java tutorial

Introduction

Here is the source code for de.azapps.mirakel.model.query_builder.MirakelQueryBuilder.java

Source

/*******************************************************************************
 * Mirakel is an Android App for managing your ToDo-Lists
 *
 * Copyright (c) 2013-2014 Anatolij Zelenin, Georg Semmler.
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     any later version.
 *
 *     This program 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.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package de.azapps.mirakel.model.query_builder;

import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.text.TextUtils;

import com.google.common.base.Optional;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import de.azapps.mirakel.model.MirakelInternalContentProvider;
import de.azapps.mirakel.model.ModelBase;
import de.azapps.tools.Log;

import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.of;

/**
 * Created by az on 15.07.14.
 */
public class MirakelQueryBuilder {
    private static final String TAG = "MirakelQueryBuilder";
    private final Context context;
    private List<String> projection = new ArrayList<>(5);
    private final StringBuilder selection = new StringBuilder();
    private final List<String> selectionArgs = new ArrayList<>(2);
    private final StringBuilder sortOrder = new StringBuilder();
    private boolean distinct = false;

    public android.support.v4.content.CursorLoader toSupportCursorLoader(final Uri uri) {
        return new android.support.v4.content.CursorLoader(this.context, uri,
                this.projection.toArray(new String[this.projection.size()]), this.selection.toString(),
                this.selectionArgs.toArray(new String[this.selectionArgs.size()]), this.sortOrder.toString());
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public android.content.CursorLoader toCursorLoader(final Uri uri) {
        return new android.content.CursorLoader(this.context, uri,
                this.projection.toArray(new String[this.projection.size()]), this.selection.toString(),
                this.selectionArgs.toArray(new String[this.selectionArgs.size()]), this.sortOrder.toString());
    }

    public MirakelQueryBuilder(final Context context) {
        this.context = context;
    }

    public MirakelQueryBuilder distinct() {
        this.distinct = true;
        return this;
    }

    private void appendConjunction(final Conjunction conjunction) {
        if (this.selection.length() != 0) {
            this.selection.append(' ').append(conjunction.toString()).append(' ');
        }
    }

    public MirakelQueryBuilder select(final String... projection) {
        this.projection = Arrays.asList(projection);
        return this;
    }

    public MirakelQueryBuilder select(final List<String> projection) {
        this.projection = projection;
        return this;
    }

    public long count(final Uri uri) {
        select("count(*)");
        final Cursor c = query(uri);
        long count = 0L;
        if (c.moveToFirst()) {
            count = c.getLong(0);
        }
        c.close();
        return count;
    }

    /**
     * Appends a selection to the current WHERE part
     *
     * @param conjunction
     *            How to connect the old query with the new one
     * @param selection
     *            The selection to add
     * @return
     */
    private MirakelQueryBuilder appendCondition(final Conjunction conjunction, final String selection) {
        if (selection.trim().isEmpty()) {
            return this;
        }
        appendConjunction(conjunction);
        this.selection.append(selection);
        return this;
    }

    private MirakelQueryBuilder appendCondition(final Conjunction conjunction, final String selection,
            final List<String> selectionArguments) {
        appendCondition(conjunction, selection);
        this.selectionArgs.addAll(selectionArguments);
        return this;
    }

    /**
     * Appends a selection to the current WHERE part
     * <p/>
     * The subQuery must be suitable for the getQuery() function
     *
     * @param conjunction
     * @param selection
     * @param subQuery
     * @return
     */
    private MirakelQueryBuilder appendCondition(final Conjunction conjunction, final String selection,
            final MirakelQueryBuilder subQuery, final Uri u) {
        appendCondition(conjunction, selection + " (" + subQuery.getQuery(u) + ')',
                subQuery.getSelectionArguments());
        return this;
    }

    @SuppressWarnings("unchecked")
    private <T> MirakelQueryBuilder appendCondition(final Conjunction conjunction, final String field,
            final Operation op, final List<T> filterInput, final List<String> selectionArgs) {
        if (filterInput.isEmpty()) {
            //is useless to call this without
            return this;
        }
        final boolean isNull = filterInput.get(0) == null;
        final Class clazz = isNull ? null : filterInput.get(0).getClass();
        final boolean isModel = !isNull && (filterInput.get(0) instanceof ModelBase);
        final boolean isBoolean = !isNull && ((clazz.equals(boolean.class)) || (clazz.equals(Boolean.class)));
        Method getId = null;
        if (isModel) {
            try {
                getId = clazz.getMethod("getId");
            } catch (final NoSuchMethodException e) {
                Log.wtf(TAG, "go and implement getId in " + clazz.getCanonicalName());
                throw new IllegalArgumentException("go and implement getId in " + clazz.getCanonicalName(), e);
            }
        }
        final List<String> filter = new ArrayList<>(filterInput.size());
        for (final T el : filterInput) {
            if (isModel) {
                try {
                    filter.add(String.valueOf(getId.invoke(el)));
                } catch (IllegalAccessException | InvocationTargetException e) {
                    Log.wtf(TAG, "go and make getId in " + clazz.getCanonicalName() + " accessible");
                    throw new IllegalArgumentException("go and implement getId in " + clazz.getCanonicalName(), e);
                }
            } else if (isBoolean) {
                filter.add((Boolean) el ? "1" : "0");
            } else if (!isNull) {
                filter.add(el.toString());
            } else {
                filter.add(null);
                break;
            }
        }
        String not = "";
        if (NOT.contains(op)) {
            not = "NOT ";
        }
        if (op == Operation.IN || op == Operation.NOT_IN) {
            appendConjunction(conjunction);
            this.selection.append(not).append(field).append(' ').append(op.toString()).append('(');
            for (final String f : filter) {
                this.selectionArgs.add(f);
                this.selection.append("?,");
            }
            this.selection.deleteCharAt(this.selection.length() - 1);
            this.selection.append(')');
        } else {
            for (final String a : filter) {
                if (a != null) {
                    appendCondition(conjunction, not + field + ' ' + op + ' ' + a);
                } else {
                    appendCondition(conjunction, field + " IS " + not + "NULL ");
                }
            }
            if (!selectionArgs.isEmpty()) {
                this.selectionArgs.addAll(selectionArgs);
            }
        }
        return this;
    }

    /**
     * Builds the query and returns it
     * <p/>
     * This is currently just a very primitive function and is not suitable for
     * production use
     *
     * @return
     */
    public String getQuery(final Uri uri) {
        final StringBuilder query = new StringBuilder(
                this.selection.length() + (this.projection.size() * 15) + (this.selectionArgs.size() * 15) + 100);
        query.append("SELECT ");
        if (this.distinct) {
            query.append("DISTINCT ");
        }
        query.append(TextUtils.join(", ", this.projection));
        query.append(" FROM ");
        query.append(MirakelInternalContentProvider.getTableName(uri));
        if (this.selection.length() > 0) {
            final String where = this.selection.toString();
            query.append(" WHERE ").append(where);
        }
        if (this.sortOrder.length() != 0) {
            query.append(" ORDER BY ").append(this.sortOrder);
        }
        return query.toString();
    }

    public String getSelection() {
        return this.selection.toString();
    }

    public List<String> getSelectionArguments() {
        return this.selectionArgs;
    }

    public Cursor query(final Uri uri) {
        final ContentResolver contentResolver = this.context.getContentResolver();
        return contentResolver.query(uri, this.projection.toArray(new String[this.projection.size()]),
                this.selection.toString(), this.selectionArgs.toArray(new String[this.selectionArgs.size()]),
                this.sortOrder.toString());
    }

    // and
    public <T extends Number> MirakelQueryBuilder and(final String field, final Operation op, final T filter) {
        return and(field, op, filter.toString());
    }

    public <T extends ModelBase> MirakelQueryBuilder and(final String field, final Operation op, final T filter) {
        return and(field, op, String.valueOf(filter.getId()));
    }

    public MirakelQueryBuilder and(final String field, final Operation op, final boolean filter) {
        return and(field, op, filter ? "1" : "0");
    }

    public MirakelQueryBuilder and(final String field, final Operation op, final String filter) {
        if (filter == null) {
            return and(field, op, Arrays.asList(new String[] { null }), new ArrayList<String>(0));
        } else if (op == Operation.IN || op == Operation.NOT_IN) {
            return appendCondition(Conjunction.AND, field, op, Collections.singletonList(filter),
                    new ArrayList<String>(0));
        }
        return and(field, op, Arrays.asList(new String[] { "?" }), Collections.singletonList(filter));
    }

    /*
     * Do not call this with something other then T extends Number, T extends
     * ModelBase or T=String java does not allow to define functions in this way
     */
    public <T> MirakelQueryBuilder and(final String field, final Operation op, final List<T> filter) {
        return and(field, op, filter, new ArrayList<String>(0));
    }

    /*
     * Do not call this with something other then T extends Number, T extends
     * ModelBase or T=String java does not allow to define functions in this way
     */
    private <T> MirakelQueryBuilder and(final String field, final Operation op, final List<T> filter,
            final List<String> selectionArgs) {
        if ((op == Operation.IN) && !selectionArgs.isEmpty()) {
            throw new IllegalArgumentException("Call condition with in is only without selectionags supported");
        }
        return appendCondition(Conjunction.AND, field, op, filter, selectionArgs);
    }

    public MirakelQueryBuilder and(final MirakelQueryBuilder other) {
        if (other.isNotEmpty()) {
            return appendCondition(Conjunction.AND, '(' + other.getSelection() + ')', other.selectionArgs);
        }
        return this;
    }

    public MirakelQueryBuilder and(final String field, final Operation op, final MirakelQueryBuilder subQuery,
            final Uri subqueryUri) {
        String not = "";
        if (NOT.contains(op)) {
            not = " NOT ";
        }
        return appendCondition(Conjunction.AND, not + field + ' ' + op, subQuery, subqueryUri);
    }

    public MirakelQueryBuilder and(final String condition) {
        return appendCondition(Conjunction.AND, condition);
    }

    // or
    public <T extends Number> MirakelQueryBuilder or(final String field, final Operation op, final T filter) {
        return or(field, op, filter.toString());
    }

    public <T extends ModelBase> MirakelQueryBuilder or(final String field, final Operation op, final T filter) {
        return or(field, op, String.valueOf(filter.getId()));
    }

    public <T extends Number> MirakelQueryBuilder or(final String field, final Operation op, final boolean filter) {
        return or(field, op, filter ? "1" : "0");
    }

    /*
     * Do not call this with something other then T extends Number, T extends
     * ModelBase or T=String
     *
     * java does not allow to define functions in this way
     */
    public <T> MirakelQueryBuilder or(final String field, final Operation op, final List<T> filter) {
        return or(field, op, filter, new ArrayList<String>(0));
    }

    public MirakelQueryBuilder or(final String field, final Operation op, final String filter) {
        if (filter == null) {
            return or(field, op, Arrays.asList(new String[] { null }), new ArrayList<String>(0));
        } else if (op == Operation.IN) {
            return appendCondition(Conjunction.OR, field, Operation.IN, Collections.singletonList(filter),
                    new ArrayList<String>(0));
        }
        return or(field, op, Arrays.asList(new String[] { "?" }), Collections.singletonList(filter));
    }

    /*
     * Do not call this with something other then T extends Number, T extends
     * ModelBase or T=String
     *
     * java does not allow to define functions in this way
     */
    private <T> MirakelQueryBuilder or(final String field, final Operation op, final List<T> filter,
            final List<String> selectionArgs) {
        if ((op == Operation.IN) && (!selectionArgs.isEmpty())) {
            throw new IllegalArgumentException("Call condition with in is only without selectionargs supported");
        }
        return appendCondition(Conjunction.OR, field, op, filter, selectionArgs);
    }

    public MirakelQueryBuilder or(final MirakelQueryBuilder other) {
        return appendCondition(Conjunction.OR, '(' + other.getSelection() + ')', other.selectionArgs);
    }

    public MirakelQueryBuilder or(final String field, final Operation op, final MirakelQueryBuilder subQuery,
            final Uri subqueryUri) {
        String not = "";
        if (NOT.contains(op)) {
            not = " NOT ";
        }
        return appendCondition(Conjunction.OR, not + field + ' ' + op, subQuery, subqueryUri);
    }

    public MirakelQueryBuilder or(final String condition) {
        return appendCondition(Conjunction.OR, condition);
    }

    public MirakelQueryBuilder not(final MirakelQueryBuilder other) {
        if (other.isNotEmpty()) {
            this.selection.append(" NOT (").append(other.getSelection()).append(')');
            this.selectionArgs.addAll(other.selectionArgs);
        }
        return this;
    }

    public static <T> T cursorToObject(final Cursor c, final Class<T> clazz) {
        try {
            final Constructor<T> constructor = clazz.getConstructor(Cursor.class);
            return constructor.newInstance(c);
        } catch (final NoSuchMethodException e) {
            Log.wtf(TAG, "go and implement a the constructor " + clazz.getCanonicalName() + "(Cursor)");
            throw new IllegalArgumentException(
                    "go and implement a the constructor " + clazz.getCanonicalName() + "(Cursor)", e);
        } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
            Log.wtf(TAG, "go and make the constructor " + clazz.getCanonicalName() + "(Cursor) accessible");
            throw new IllegalArgumentException(
                    "go and make the constructor " + clazz.getCanonicalName() + "(Cursor) accessible", e);
        }
    }

    public <T extends ModelBase> List<T> getList(final Class<T> clazz) {
        final Cursor c = query(setupQueryBuilder(clazz));
        final List<T> l = new ArrayList<>(c.getCount());
        if (c.moveToFirst()) {
            do {
                final T obj = cursorToObject(c, clazz);
                l.add(obj);
            } while (c.moveToNext());
        }
        c.close();
        return l;
    }

    @NonNull
    public <T extends ModelBase> Optional<T> get(final Class<T> clazz, final long id) {
        and(ModelBase.ID, Operation.EQ, id);
        return get(clazz);
    }

    @NonNull
    public <T extends ModelBase> Optional<T> get(final Class<T> clazz) {
        Optional<T> a = absent();
        final Cursor c = query(setupQueryBuilder(clazz));
        if (c.moveToFirst()) {
            a = of(cursorToObject(c, clazz));
        }
        c.close();
        return a;
    }

    private <T> Uri setupQueryBuilder(final Class<T> clazz) {
        final Uri uri;
        try {
            uri = (Uri) clazz.getField("URI").get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            Log.wtf(TAG, "go and implement a URI  for" + clazz.getCanonicalName());
            throw new IllegalArgumentException("go and implement a URI for " + clazz.getCanonicalName(), e);
        }
        if (this.projection.isEmpty()) {
            try {
                // can be null, because field should be static
                this.projection = Arrays.asList((String[]) clazz.getField("allColumns").get(null));
            } catch (NoSuchFieldException | IllegalAccessException e) {
                Log.wtf(TAG, "go and implement allColumns for " + clazz.getCanonicalName());
                throw new IllegalArgumentException("go and implement allColumns for " + clazz.getCanonicalName(),
                        e);
            }
        }
        return uri;
    }

    public MirakelQueryBuilder sort(final String field, final Sorting s) {
        sort(field, s, null);
        return this;
    }

    public MirakelQueryBuilder sort(final String field, final Sorting s, final List<String> selectionArgs) {
        if (this.sortOrder.length() > 0) {
            this.sortOrder.append(", ");
        }
        this.sortOrder.append(field).append(' ').append(s);
        if (selectionArgs != null) {
            this.selectionArgs.addAll(selectionArgs);
        }
        return this;
    }

    public enum Conjunction {
        AND, OR;
    }

    public enum Sorting {
        ASC, DESC;
    }

    static final List<Operation> NOT = Arrays.asList(Operation.NOT_EQ, Operation.NOT_LIKE, Operation.NOT_GT,
            Operation.NOT_GE, Operation.NOT_LT, Operation.NOT_LE, Operation.NOT_IN);

    public enum Operation {
        EQ, LIKE, GT, GE, LT, LE, IN, NOT_EQ, NOT_LIKE, NOT_GT, NOT_GE, NOT_LT, NOT_LE, NOT_IN;

        @Override
        public String toString() {
            switch (this) {
            case EQ:
            case NOT_EQ:
                return "=";
            case LIKE:
            case NOT_LIKE:
                return "LIKE";
            case GT:
            case NOT_GT:
                return ">";
            case GE:
            case NOT_GE:
                return ">=";
            case LT:
            case NOT_LT:
                return "<";
            case LE:
            case NOT_LE:
                return "<=";
            case IN:
            case NOT_IN:
                return "IN";
            default:
                throw new IllegalArgumentException("Unknown Operation " + super.toString());
            }
        }
    }

    public boolean isEmpty() {
        return selection.toString().trim().isEmpty() && selectionArgs.isEmpty();
    }

    public boolean isNotEmpty() {
        return !isEmpty();
    }
}