From 25470938dc7d1ffd05a5b5f529760ad7c294aa63 Mon Sep 17 00:00:00 2001 From: twyen Date: Tue, 8 Aug 2017 11:16:46 -0700 Subject: Implement Selection Selection can be used to make complex SQL queries more readable, and pass around incomplete selections for appending. See cl/162013087 for usage Test: SelectionTest PiperOrigin-RevId: 164618236 Change-Id: Ice035211f6b02858255a9e7d18215c9282bf28e9 --- .../android/dialer/common/database/Selection.java | 260 +++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 java/com/android/dialer/common/database/Selection.java (limited to 'java/com/android/dialer') diff --git a/java/com/android/dialer/common/database/Selection.java b/java/com/android/dialer/common/database/Selection.java new file mode 100644 index 000000000..b61472d2f --- /dev/null +++ b/java/com/android/dialer/common/database/Selection.java @@ -0,0 +1,260 @@ +/* + * 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 com.android.dialer.common.Assert; +import java.util.ArrayList; +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 + public static Selection fromString(@Nullable String selection, @Nullable String... 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)); + } + } + + /** 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, @Nullable String... args) { + if (selection == null) { + return; + } + checkArgsCount(selection, args); + this.selection.append(parenthesized(selection)); + if (args != null) { + Collections.addAll(selectionArgs, 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, @Nullable String... args) { + int argsInSelection = 0; + for (int i = 0; i < selection.length(); i++) { + if (selection.charAt(i) == '?') { + argsInSelection++; + } + } + Assert.checkArgument(argsInSelection == (args == null ? 0 : args.length)); + } + } + + /** + * 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; + } +} -- cgit v1.2.3