/* * Copyright (C) 2017 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.android.dialer.common.database; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import com.android.dialer.common.Assert; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; /** * Utility to build SQL selections. Handles string concatenation, nested statements, empty * statements, and tracks the selection arguments. * *

A selection can be build from a string, factory methods like {@link #column(String)}, or use * {@link Builder} to build complex nested selection with multiple operators. The Selection manages * the {@code selection} and {@code selectionArgs} passed into {@link * android.content.ContentResolver#query(android.net.Uri, String[], String, String[], String)}. * *

Example: * *


 *   fromString("foo = 1")
 * 
* * expands into "(foo = 1)", {} * *

* *


 *   column("foo").is("LIKE", "bar")
 * 
* * expands into "(foo LIKE ?)", {"bar"} * *

* *


 *   builder()
 *     .and(
 *       fromString("foo = ?", "1").buildUpon()
 *       .or(column("bar").is("<", 2))
 *       .build())
 *     .and(not(column("baz").is("!= 3")))
 *     .build();
 * 
* * expands into "(((foo = ?) OR (bar < ?)) AND (NOT (baz != 3)))", {"1", "2"} */ public final class Selection { private final String selection; private final String[] selectionArgs; private Selection(@NonNull String selection, @NonNull String[] selectionArgs) { this.selection = selection; this.selectionArgs = selectionArgs; } @NonNull public String getSelection() { return selection; } @NonNull public String[] getSelectionArgs() { return selectionArgs; } public boolean isEmpty() { return selection.isEmpty(); } /** * @return a mutable builder that appends to the selection. The selection will be parenthesized * before anything is appended to it. */ @NonNull public Builder buildUpon() { return new Builder(this); } /** @return a builder that is empty. */ @NonNull public static Builder builder() { return new Builder(); } /** * @return a Selection built from regular selection string/args pair. The result selection will be * enclosed in a parenthesis. */ @NonNull @SuppressWarnings("rawtypes") public static Selection fromString(@Nullable String selection, @Nullable String... args) { return new Builder(selection, args == null ? Collections.emptyList() : Arrays.asList(args)) .build(); } @NonNull public static Selection fromString(@Nullable String selection, Collection args) { return new Builder(selection, args).build(); } /** @return a selection that is negated */ @NonNull public static Selection not(@NonNull Selection selection) { Assert.checkArgument(!selection.isEmpty()); return fromString("NOT " + selection.getSelection(), selection.getSelectionArgs()); } /** * Build a selection based on condition upon a column. is() should be called to complete the * selection. */ @NonNull public static Column column(@NonNull String column) { return new Column(column); } /** Helper class to build a selection based on condition upon a column. */ public static class Column { @NonNull private final String column; private Column(@NonNull String column) { this.column = Assert.isNotNull(column); } /** Expands to " ?" and add {@code value} to the arguments. */ @NonNull public Selection is(@NonNull String operator, @NonNull Object value) { return fromString(column + " " + Assert.isNotNull(operator) + " ?", value.toString()); } /** * Expands to " ". {@link #is(String, Object)} should be used if the condition * is comparing to a string or a user input value, which must be sanitized. */ @NonNull public Selection is(@NonNull String condition) { return fromString(column + " " + Assert.isNotNull(condition)); } public Selection in(String... values) { return in(values == null ? Collections.emptyList() : Arrays.asList(values)); } public Selection in(Collection values) { return fromString( column + " IN (" + TextUtils.join(",", Collections.nCopies(values.size(), "?")) + ")", values); } } /** Builder for {@link Selection} */ public static final class Builder { private final StringBuilder selection = new StringBuilder(); private final List selectionArgs = new ArrayList<>(); private Builder() {} private Builder(@Nullable String selection, Collection args) { if (selection == null) { return; } checkArgsCount(selection, args); this.selection.append(parenthesized(selection)); if (args != null) { selectionArgs.addAll(args); } } private Builder(@NonNull Selection selection) { this.selection.append(selection.getSelection()); Collections.addAll(selectionArgs, selection.selectionArgs); } @NonNull public Selection build() { if (selection.length() == 0) { return new Selection("", new String[] {}); } return new Selection( parenthesized(selection.toString()), selectionArgs.toArray(new String[selectionArgs.size()])); } @NonNull public Builder and(@NonNull Selection selection) { if (selection.isEmpty()) { return this; } if (this.selection.length() > 0) { this.selection.append(" AND "); } this.selection.append(selection.getSelection()); Collections.addAll(selectionArgs, selection.getSelectionArgs()); return this; } @NonNull public Builder or(@NonNull Selection selection) { if (selection.isEmpty()) { return this; } if (this.selection.length() > 0) { this.selection.append(" OR "); } this.selection.append(selection.getSelection()); Collections.addAll(selectionArgs, selection.getSelectionArgs()); return this; } private static void checkArgsCount(@NonNull String selection, Collection args) { int argsInSelection = 0; for (int i = 0; i < selection.length(); i++) { if (selection.charAt(i) == '?') { argsInSelection++; } } Assert.checkArgument(argsInSelection == (args == null ? 0 : args.size())); } } /** * Parenthesized the {@code string}. Will not parenthesized if {@code string} is empty or is * already parenthesized (top level parenthesis encloses the whole string). */ @NonNull private static String parenthesized(@NonNull String string) { if (string.isEmpty()) { return ""; } if (!string.startsWith("(")) { return "(" + string + ")"; } int depth = 1; for (int i = 1; i < string.length() - 1; i++) { switch (string.charAt(i)) { case '(': depth++; break; case ')': depth--; if (depth == 0) { // First '(' closed before the string has ended,need an additional level of nesting. // For example "(A) AND (B)" should become "((A) AND (B))" return "(" + string + ")"; } break; default: continue; } } Assert.checkArgument(depth == 1); return string; } }