1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.dialer.common.database;
18 
19 import android.support.annotation.NonNull;
20 import android.support.annotation.Nullable;
21 import android.text.TextUtils;
22 import com.android.dialer.common.Assert;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.List;
28 
29 /**
30  * Utility to build SQL selections. Handles string concatenation, nested statements, empty
31  * statements, and tracks the selection arguments.
32  *
33  * <p>A selection can be build from a string, factory methods like {@link #column(String)}, or use
34  * {@link Builder} to build complex nested selection with multiple operators. The Selection manages
35  * the {@code selection} and {@code selectionArgs} passed into {@link
36  * android.content.ContentResolver#query(android.net.Uri, String[], String, String[], String)}.
37  *
38  * <p>Example:
39  *
40  * <pre><code>
41  *   fromString("foo = 1")
42  * </code></pre>
43  *
44  * expands into "(foo = 1)", {}
45  *
46  * <p>
47  *
48  * <pre><code>
49  *   column("foo").is("LIKE", "bar")
50  * </code></pre>
51  *
52  * expands into "(foo LIKE ?)", {"bar"}
53  *
54  * <p>
55  *
56  * <pre><code>
57  *   builder()
58  *     .and(
59  *       fromString("foo = ?", "1").buildUpon()
60  *       .or(column("bar").is("<", 2))
61  *       .build())
62  *     .and(not(column("baz").is("!= 3")))
63  *     .build();
64  * </code></pre>
65  *
66  * expands into "(((foo = ?) OR (bar < ?)) AND (NOT (baz != 3)))", {"1", "2"}
67  */
68 public final class Selection {
69 
70   private final String selection;
71   private final String[] selectionArgs;
72 
Selection(@onNull String selection, @NonNull String[] selectionArgs)73   private Selection(@NonNull String selection, @NonNull String[] selectionArgs) {
74     this.selection = selection;
75     this.selectionArgs = selectionArgs;
76   }
77 
78   @NonNull
getSelection()79   public String getSelection() {
80     return selection;
81   }
82 
83   @NonNull
getSelectionArgs()84   public String[] getSelectionArgs() {
85     return selectionArgs;
86   }
87 
isEmpty()88   public boolean isEmpty() {
89     return selection.isEmpty();
90   }
91 
92   /**
93    * @return a mutable builder that appends to the selection. The selection will be parenthesized
94    *     before anything is appended to it.
95    */
96   @NonNull
buildUpon()97   public Builder buildUpon() {
98     return new Builder(this);
99   }
100 
101   /** @return a builder that is empty. */
102   @NonNull
builder()103   public static Builder builder() {
104     return new Builder();
105   }
106 
107   /**
108    * @return a Selection built from regular selection string/args pair. The result selection will be
109    *     enclosed in a parenthesis.
110    */
111   @NonNull
112   @SuppressWarnings("rawtypes")
fromString(@ullable String selection, @Nullable String... args)113   public static Selection fromString(@Nullable String selection, @Nullable String... args) {
114     return new Builder(selection, args == null ? Collections.emptyList() : Arrays.asList(args))
115         .build();
116   }
117 
118   @NonNull
fromString(@ullable String selection, Collection<String> args)119   public static Selection fromString(@Nullable String selection, Collection<String> args) {
120     return new Builder(selection, args).build();
121   }
122 
123   /** @return a selection that is negated */
124   @NonNull
not(@onNull Selection selection)125   public static Selection not(@NonNull Selection selection) {
126     Assert.checkArgument(!selection.isEmpty());
127     return fromString("NOT " + selection.getSelection(), selection.getSelectionArgs());
128   }
129 
130   /**
131    * Build a selection based on condition upon a column. is() should be called to complete the
132    * selection.
133    */
134   @NonNull
column(@onNull String column)135   public static Column column(@NonNull String column) {
136     return new Column(column);
137   }
138 
139   /** Helper class to build a selection based on condition upon a column. */
140   public static class Column {
141 
142     @NonNull private final String column;
143 
Column(@onNull String column)144     private Column(@NonNull String column) {
145       this.column = Assert.isNotNull(column);
146     }
147 
148     /** Expands to "<column> <operator> ?" and add {@code value} to the arguments. */
149     @NonNull
is(@onNull String operator, @NonNull Object value)150     public Selection is(@NonNull String operator, @NonNull Object value) {
151       return fromString(column + " " + Assert.isNotNull(operator) + " ?", value.toString());
152     }
153 
154     /**
155      * Expands to "<column> <operator>". {@link #is(String, Object)} should be used if the condition
156      * is comparing to a string or a user input value, which must be sanitized.
157      */
158     @NonNull
is(@onNull String condition)159     public Selection is(@NonNull String condition) {
160       return fromString(column + " " + Assert.isNotNull(condition));
161     }
162 
in(String... values)163     public Selection in(String... values) {
164       return in(values == null ? Collections.emptyList() : Arrays.asList(values));
165     }
166 
in(Collection<String> values)167     public Selection in(Collection<String> values) {
168       return fromString(
169           column + " IN (" + TextUtils.join(",", Collections.nCopies(values.size(), "?")) + ")",
170           values);
171     }
172   }
173 
174   /** Builder for {@link Selection} */
175   public static final class Builder {
176 
177     private final StringBuilder selection = new StringBuilder();
178     private final List<String> selectionArgs = new ArrayList<>();
179 
Builder()180     private Builder() {}
181 
Builder(@ullable String selection, Collection<String> args)182     private Builder(@Nullable String selection, Collection<String> args) {
183       if (selection == null) {
184         return;
185       }
186       checkArgsCount(selection, args);
187       this.selection.append(parenthesized(selection));
188       if (args != null) {
189         selectionArgs.addAll(args);
190       }
191     }
192 
Builder(@onNull Selection selection)193     private Builder(@NonNull Selection selection) {
194       this.selection.append(selection.getSelection());
195       Collections.addAll(selectionArgs, selection.selectionArgs);
196     }
197 
198     @NonNull
build()199     public Selection build() {
200       if (selection.length() == 0) {
201         return new Selection("", new String[] {});
202       }
203       return new Selection(
204           parenthesized(selection.toString()),
205           selectionArgs.toArray(new String[selectionArgs.size()]));
206     }
207 
208     @NonNull
and(@onNull Selection selection)209     public Builder and(@NonNull Selection selection) {
210       if (selection.isEmpty()) {
211         return this;
212       }
213 
214       if (this.selection.length() > 0) {
215         this.selection.append(" AND ");
216       }
217       this.selection.append(selection.getSelection());
218       Collections.addAll(selectionArgs, selection.getSelectionArgs());
219       return this;
220     }
221 
222     @NonNull
or(@onNull Selection selection)223     public Builder or(@NonNull Selection selection) {
224       if (selection.isEmpty()) {
225         return this;
226       }
227 
228       if (this.selection.length() > 0) {
229         this.selection.append(" OR ");
230       }
231       this.selection.append(selection.getSelection());
232       Collections.addAll(selectionArgs, selection.getSelectionArgs());
233       return this;
234     }
235 
checkArgsCount(@onNull String selection, Collection<String> args)236     private static void checkArgsCount(@NonNull String selection, Collection<String> args) {
237       int argsInSelection = 0;
238       for (int i = 0; i < selection.length(); i++) {
239         if (selection.charAt(i) == '?') {
240           argsInSelection++;
241         }
242       }
243       Assert.checkArgument(argsInSelection == (args == null ? 0 : args.size()));
244     }
245   }
246 
247   /**
248    * Parenthesized the {@code string}. Will not parenthesized if {@code string} is empty or is
249    * already parenthesized (top level parenthesis encloses the whole string).
250    */
251   @NonNull
parenthesized(@onNull String string)252   private static String parenthesized(@NonNull String string) {
253     if (string.isEmpty()) {
254       return "";
255     }
256     if (!string.startsWith("(")) {
257       return "(" + string + ")";
258     }
259     int depth = 1;
260     for (int i = 1; i < string.length() - 1; i++) {
261       switch (string.charAt(i)) {
262         case '(':
263           depth++;
264           break;
265         case ')':
266           depth--;
267           if (depth == 0) {
268             // First '(' closed before the string has ended,need an additional level of nesting.
269             // For example "(A) AND (B)" should become "((A) AND (B))"
270             return "(" + string + ")";
271           }
272           break;
273         default:
274           continue;
275       }
276     }
277     Assert.checkArgument(depth == 1);
278     return string;
279   }
280 }
281