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
17package android.arch.persistence.room;
18
19import android.arch.persistence.db.SupportSQLiteProgram;
20import android.arch.persistence.db.SupportSQLiteQuery;
21import android.support.annotation.IntDef;
22import android.support.annotation.RestrictTo;
23import android.support.annotation.VisibleForTesting;
24
25import java.lang.annotation.Retention;
26import java.lang.annotation.RetentionPolicy;
27import java.util.Arrays;
28import java.util.Iterator;
29import java.util.Map;
30import java.util.TreeMap;
31
32/**
33 * This class is used as an intermediate place to keep binding arguments so that we can run
34 * Cursor queries with correct types rather than passing everything as a string.
35 * <p>
36 * Because it is relatively a big object, they are pooled and must be released after each use.
37 *
38 * @hide
39 */
40@SuppressWarnings("unused")
41@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
42public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
43    @SuppressWarnings("WeakerAccess")
44    @VisibleForTesting
45    // Maximum number of queries we'll keep cached.
46    static final int POOL_LIMIT = 15;
47    @SuppressWarnings("WeakerAccess")
48    @VisibleForTesting
49    // Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
50    // clear the bigger queries (# of arguments).
51    static final int DESIRED_POOL_SIZE = 10;
52    private volatile String mQuery;
53    @SuppressWarnings("WeakerAccess")
54    @VisibleForTesting
55    final long[] mLongBindings;
56    @SuppressWarnings("WeakerAccess")
57    @VisibleForTesting
58    final double[] mDoubleBindings;
59    @SuppressWarnings("WeakerAccess")
60    @VisibleForTesting
61    final String[] mStringBindings;
62    @SuppressWarnings("WeakerAccess")
63    @VisibleForTesting
64    final byte[][] mBlobBindings;
65
66    @Binding
67    private final int[] mBindingTypes;
68    @SuppressWarnings("WeakerAccess")
69    @VisibleForTesting
70    final int mCapacity;
71    // number of arguments in the query
72    @SuppressWarnings("WeakerAccess")
73    @VisibleForTesting
74    int mArgCount;
75
76
77    @SuppressWarnings("WeakerAccess")
78    @VisibleForTesting
79    static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();
80
81    /**
82     * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
83     * given query.
84     *
85     * @param query         The query to prepare
86     * @param argumentCount The number of query arguments
87     * @return A RoomSQLiteQuery that holds the given query and has space for the given number of
88     * arguments.
89     */
90    @SuppressWarnings("WeakerAccess")
91    public static RoomSQLiteQuery acquire(String query, int argumentCount) {
92        synchronized (sQueryPool) {
93            final Map.Entry<Integer, RoomSQLiteQuery> entry =
94                    sQueryPool.ceilingEntry(argumentCount);
95            if (entry != null) {
96                sQueryPool.remove(entry.getKey());
97                final RoomSQLiteQuery sqliteQuery = entry.getValue();
98                sqliteQuery.init(query, argumentCount);
99                return sqliteQuery;
100            }
101        }
102        RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
103        sqLiteQuery.init(query, argumentCount);
104        return sqLiteQuery;
105    }
106
107    private RoomSQLiteQuery(int capacity) {
108        mCapacity = capacity;
109        // because, 1 based indices... we don't want to offsets everything with 1 all the time.
110        int limit = capacity + 1;
111        //noinspection WrongConstant
112        mBindingTypes = new int[limit];
113        mLongBindings = new long[limit];
114        mDoubleBindings = new double[limit];
115        mStringBindings = new String[limit];
116        mBlobBindings = new byte[limit][];
117    }
118
119    @SuppressWarnings("WeakerAccess")
120    void init(String query, int argCount) {
121        mQuery = query;
122        mArgCount = argCount;
123    }
124
125    /**
126     * Releases the query back to the pool.
127     * <p>
128     * After released, the statement might be returned when {@link #acquire(String, int)} is called
129     * so you should never re-use it after releasing.
130     */
131    @SuppressWarnings("WeakerAccess")
132    public void release() {
133        synchronized (sQueryPool) {
134            sQueryPool.put(mCapacity, this);
135            prunePoolLocked();
136        }
137    }
138
139    private static void prunePoolLocked() {
140        if (sQueryPool.size() > POOL_LIMIT) {
141            int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
142            final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator();
143            while (toBeRemoved-- > 0) {
144                iterator.next();
145                iterator.remove();
146            }
147        }
148    }
149
150    @Override
151    public String getSql() {
152        return mQuery;
153    }
154
155    public int getArgCount() {
156        return mArgCount;
157    }
158
159    @Override
160    public void bindTo(SupportSQLiteProgram program) {
161        for (int index = 1; index <= mArgCount; index++) {
162            switch (mBindingTypes[index]) {
163                case NULL:
164                    program.bindNull(index);
165                    break;
166                case LONG:
167                    program.bindLong(index, mLongBindings[index]);
168                    break;
169                case DOUBLE:
170                    program.bindDouble(index, mDoubleBindings[index]);
171                    break;
172                case STRING:
173                    program.bindString(index, mStringBindings[index]);
174                    break;
175                case BLOB:
176                    program.bindBlob(index, mBlobBindings[index]);
177                    break;
178            }
179        }
180    }
181
182    @Override
183    public void bindNull(int index) {
184        mBindingTypes[index] = NULL;
185    }
186
187    @Override
188    public void bindLong(int index, long value) {
189        mBindingTypes[index] = LONG;
190        mLongBindings[index] = value;
191    }
192
193    @Override
194    public void bindDouble(int index, double value) {
195        mBindingTypes[index] = DOUBLE;
196        mDoubleBindings[index] = value;
197    }
198
199    @Override
200    public void bindString(int index, String value) {
201        mBindingTypes[index] = STRING;
202        mStringBindings[index] = value;
203    }
204
205    @Override
206    public void bindBlob(int index, byte[] value) {
207        mBindingTypes[index] = BLOB;
208        mBlobBindings[index] = value;
209    }
210
211    @Override
212    public void close() throws Exception {
213        // no-op. not calling release because it is internal API.
214    }
215
216    /**
217     * Copies arguments from another RoomSQLiteQuery into this query.
218     *
219     * @param other The other query, which holds the arguments to be copied.
220     */
221    public void copyArgumentsFrom(RoomSQLiteQuery other) {
222        int argCount = other.getArgCount() + 1; // +1 for the binding offsets
223        System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
224        System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
225        System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
226        System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
227        System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
228    }
229
230    @Override
231    public void clearBindings() {
232        Arrays.fill(mBindingTypes, NULL);
233        Arrays.fill(mStringBindings, null);
234        Arrays.fill(mBlobBindings, null);
235        mQuery = null;
236        // no need to clear others
237    }
238
239    private static final int NULL = 1;
240    private static final int LONG = 2;
241    private static final int DOUBLE = 3;
242    private static final int STRING = 4;
243    private static final int BLOB = 5;
244
245    @Retention(RetentionPolicy.SOURCE)
246    @IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
247    @interface Binding {
248    }
249}
250