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