/* * Copyright (C) 2010 The Android Open Source Project * * 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 com.boardgamegeek.util; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.provider.BaseColumns; import android.support.v4.util.ArrayMap; import android.text.TextUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import timber.log.Timber; /** * Helper for building selection clauses for {@link SQLiteDatabase}. Each appended clause is combined using {@code AND}. * This class is <em>not</em> thread safe. Borrowed, then expanded, from com.google.android.apps.iosched.util. */ public class SelectionBuilder { private String tableName = null; private final Map<String, String> projectionMap = new ArrayMap<>(); private final StringBuilder selection = new StringBuilder(); private final List<String> selectionArgs = new ArrayList<>(); private final List<String> groupBy = new ArrayList<>(); private String having = null; private String limit = null; /** * Reset any internal state, allowing this builder to be recycled. */ public SelectionBuilder reset() { tableName = null; projectionMap.clear(); selection.setLength(0); selectionArgs.clear(); groupBy.clear(); return this; } public SelectionBuilder whereEquals(String column, String selectionArg) { return where(column + "=?", selectionArg); } public SelectionBuilder whereEquals(String column, int selectionArg) { return where(column + "=?", String.valueOf(selectionArg)); } public SelectionBuilder whereEquals(String column, long selectionArg) { return where(column + "=?", String.valueOf(selectionArg)); } public SelectionBuilder whereEqualsOrNull(String column, String selectionArg) { return where(column + "=? OR " + column + " IS NULL", selectionArg); } /** * Append the given selection clause to the internal state. Each clause is surrounded with parenthesis and combined * using {@code AND}. */ public SelectionBuilder where(String selection, String... selectionArgs) { if (TextUtils.isEmpty(selection)) { if (selectionArgs != null && selectionArgs.length > 0) { throw new IllegalArgumentException("Valid selection required when including arguments="); } // Shortcut when clause is empty return this; } // TODO: map selection similar to projection if (this.selection.length() > 0) { this.selection.append(" AND "); } this.selection.append("(").append(selection).append(")"); if (selectionArgs != null) { Collections.addAll(this.selectionArgs, selectionArgs); } return this; } public SelectionBuilder table(String table) { tableName = table; return this; } public SelectionBuilder limit(String rowCount) { limit = null; if (rowCount != null) { int count = StringUtils.parseInt(rowCount, 0); if (count > 0) { limit = rowCount; } } return this; } private void assertTable() { if (tableName == null) { throw new IllegalStateException("Table not specified"); } } private void assertHaving() { if (!TextUtils.isEmpty(having) && (groupBy.size() == 0)) { throw new IllegalStateException("Group by must be specified for Having clause"); } } public SelectionBuilder mapToTable(String column, String table) { if (column.equals(BaseColumns._ID)) { return mapToTable(column, table, column); } else { projectionMap.put(column, String.format("%s.%s", table, column)); } return this; } public SelectionBuilder mapToTable(String column, String table, String alias) { projectionMap.put(column, String.format("%s.%s AS %s", table, column, alias)); return this; } public SelectionBuilder mapIfNull(String column, String nullDefault) { projectionMap.put(column, String.format("IFNULL(%s,%s) AS %s", column, nullDefault, column)); return this; } public SelectionBuilder mapIfNullToTable(String column, String table, String nullDefault) { projectionMap.put(column, String.format("IFNULL(%s.%s,%s) AS %s", table, column, nullDefault, column)); return this; } public SelectionBuilder mapAsSum(String aliasColumn, String sumColumn) { return map(aliasColumn, String.format("SUM(%s)", sumColumn)); } public SelectionBuilder mapAsMax(String aliasColumn, String maxColumn) { return map(aliasColumn, String.format("MAX(%s)", maxColumn)); } public SelectionBuilder map(String fromColumn, String toClause) { projectionMap.put(fromColumn, String.format("%s AS %s", toClause, fromColumn)); return this; } public SelectionBuilder groupBy(String... groupArgs) { groupBy.clear(); if (groupArgs != null) { Collections.addAll(groupBy, groupArgs); } return this; } public SelectionBuilder having(String having) { this.having = having; return this; } /** * Return selection string for current internal state. * * @see #getSelectionArgs() */ public String getSelection() { return selection.toString(); } /** * Return selection arguments for current internal state. * * @see #getSelection() */ public String[] getSelectionArgs() { return selectionArgs.toArray(new String[selectionArgs.size()]); } public String getGroupByClause() { if (groupBy.size() == 0) { return ""; } StringBuilder clause = new StringBuilder(); for (String arg : groupBy) { if (clause.length() > 0) { clause.append(", "); } final String target = projectionMap.get(arg); if (target != null) { if (!target.contains(" AS ")) { arg = target; } } clause.append(arg); } return clause.toString(); } private void mapColumns(String[] columns) { for (int i = 0; i < columns.length; i++) { final String target = projectionMap.get(columns[i]); if (target != null) { columns[i] = target; } } } @Override public String toString() { return "table=[" + tableName + "], selection=[" + getSelection() + "], selectionArgs=" + Arrays.toString(getSelectionArgs()) + ", groupBy=[" + getGroupByClause() + "], having=[" + having + "]"; } /** * Execute query using the current internal state as {@code WHERE} clause. */ public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) { assertHaving(); return query(db, columns, getGroupByClause(), having, orderBy, limit); } /** * Execute query using the current internal state as {@code WHERE} clause. */ public Cursor query(SQLiteDatabase db, String[] columns, String groupBy, String having, String orderBy, String limit) { assertTable(); if (columns != null) { mapColumns(columns); } Timber.v("QUERY: columns=%s, %s", Arrays.toString(columns), this); Cursor c = db.query(tableName, columns, getSelection(), getSelectionArgs(), groupBy, having, orderBy, limit); Timber.v("queried %,d rows", c.getCount()); return c; } /** * Execute update using the current internal state as {@code WHERE} clause. */ public int update(SQLiteDatabase db, ContentValues values) { assertTable(); Timber.v("UPDATE: %s", this); int count = db.update(tableName, values, getSelection(), getSelectionArgs()); Timber.v("updated %,d rows", count); return count; } /** * Execute delete using the current internal state as {@code WHERE} clause. */ public int delete(SQLiteDatabase db) { assertTable(); Timber.v("DELETE: %s", this); String selection = getSelection(); if (TextUtils.isEmpty(selection)) { // this forces delete to return the count selection = "1"; } int count = db.delete(tableName, selection, getSelectionArgs()); Timber.v("deleted %,d rows", count); return count; } public static String whereNullOrEmpty(String columnName) { return String.format("(%1$S IS NULL OR %1$S='')", columnName); } public static String whereZeroOrNull(String columnName) { return String.format("(%1$s=0 OR %1$s IS NULL)", columnName); } }