Home | History | Annotate | Download | only in database
      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 
     73   private Selection(@NonNull String selection, @NonNull String[] selectionArgs) {
     74     this.selection = selection;
     75     this.selectionArgs = selectionArgs;
     76   }
     77 
     78   @NonNull
     79   public String getSelection() {
     80     return selection;
     81   }
     82 
     83   @NonNull
     84   public String[] getSelectionArgs() {
     85     return selectionArgs;
     86   }
     87 
     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
     97   public Builder buildUpon() {
     98     return new Builder(this);
     99   }
    100 
    101   /** @return a builder that is empty. */
    102   @NonNull
    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")
    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
    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
    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
    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 
    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
    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
    159     public Selection is(@NonNull String condition) {
    160       return fromString(column + " " + Assert.isNotNull(condition));
    161     }
    162 
    163     public Selection in(String... values) {
    164       return in(values == null ? Collections.emptyList() : Arrays.asList(values));
    165     }
    166 
    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 
    180     private Builder() {}
    181 
    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 
    193     private Builder(@NonNull Selection selection) {
    194       this.selection.append(selection.getSelection());
    195       Collections.addAll(selectionArgs, selection.selectionArgs);
    196     }
    197 
    198     @NonNull
    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
    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
    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 
    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
    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