| /* |
| * 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 android.arch.persistence.room; |
| |
| import android.arch.persistence.db.SupportSQLiteProgram; |
| import android.arch.persistence.db.SupportSQLiteQuery; |
| import android.support.annotation.IntDef; |
| import android.support.annotation.RestrictTo; |
| import android.support.annotation.VisibleForTesting; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.TreeMap; |
| |
| /** |
| * This class is used as an intermediate place to keep binding arguments so that we can run |
| * Cursor queries with correct types rather than passing everything as a string. |
| * <p> |
| * Because it is relatively a big object, they are pooled and must be released after each use. |
| * |
| * @hide |
| */ |
| @SuppressWarnings("unused") |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram { |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| // Maximum number of queries we'll keep cached. |
| static final int POOL_LIMIT = 15; |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| // Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always |
| // clear the bigger queries (# of arguments). |
| static final int DESIRED_POOL_SIZE = 10; |
| private volatile String mQuery; |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| final long[] mLongBindings; |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| final double[] mDoubleBindings; |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| final String[] mStringBindings; |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| final byte[][] mBlobBindings; |
| |
| @Binding |
| private final int[] mBindingTypes; |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| final int mCapacity; |
| // number of arguments in the query |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| int mArgCount; |
| |
| |
| @SuppressWarnings("WeakerAccess") |
| @VisibleForTesting |
| static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>(); |
| |
| /** |
| * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the |
| * given query. |
| * |
| * @param query The query to prepare |
| * @param argumentCount The number of query arguments |
| * @return A RoomSQLiteQuery that holds the given query and has space for the given number of |
| * arguments. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public static RoomSQLiteQuery acquire(String query, int argumentCount) { |
| synchronized (sQueryPool) { |
| final Map.Entry<Integer, RoomSQLiteQuery> entry = |
| sQueryPool.ceilingEntry(argumentCount); |
| if (entry != null) { |
| sQueryPool.remove(entry.getKey()); |
| final RoomSQLiteQuery sqliteQuery = entry.getValue(); |
| sqliteQuery.init(query, argumentCount); |
| return sqliteQuery; |
| } |
| } |
| RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount); |
| sqLiteQuery.init(query, argumentCount); |
| return sqLiteQuery; |
| } |
| |
| private RoomSQLiteQuery(int capacity) { |
| mCapacity = capacity; |
| // because, 1 based indices... we don't want to offsets everything with 1 all the time. |
| int limit = capacity + 1; |
| //noinspection WrongConstant |
| mBindingTypes = new int[limit]; |
| mLongBindings = new long[limit]; |
| mDoubleBindings = new double[limit]; |
| mStringBindings = new String[limit]; |
| mBlobBindings = new byte[limit][]; |
| } |
| |
| @SuppressWarnings("WeakerAccess") |
| void init(String query, int argCount) { |
| mQuery = query; |
| mArgCount = argCount; |
| } |
| |
| /** |
| * Releases the query back to the pool. |
| * <p> |
| * After released, the statement might be returned when {@link #acquire(String, int)} is called |
| * so you should never re-use it after releasing. |
| */ |
| @SuppressWarnings("WeakerAccess") |
| public void release() { |
| synchronized (sQueryPool) { |
| sQueryPool.put(mCapacity, this); |
| prunePoolLocked(); |
| } |
| } |
| |
| private static void prunePoolLocked() { |
| if (sQueryPool.size() > POOL_LIMIT) { |
| int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE; |
| final Iterator<Integer> iterator = sQueryPool.descendingKeySet().iterator(); |
| while (toBeRemoved-- > 0) { |
| iterator.next(); |
| iterator.remove(); |
| } |
| } |
| } |
| |
| @Override |
| public String getSql() { |
| return mQuery; |
| } |
| |
| public int getArgCount() { |
| return mArgCount; |
| } |
| |
| @Override |
| public void bindTo(SupportSQLiteProgram program) { |
| for (int index = 1; index <= mArgCount; index++) { |
| switch (mBindingTypes[index]) { |
| case NULL: |
| program.bindNull(index); |
| break; |
| case LONG: |
| program.bindLong(index, mLongBindings[index]); |
| break; |
| case DOUBLE: |
| program.bindDouble(index, mDoubleBindings[index]); |
| break; |
| case STRING: |
| program.bindString(index, mStringBindings[index]); |
| break; |
| case BLOB: |
| program.bindBlob(index, mBlobBindings[index]); |
| break; |
| } |
| } |
| } |
| |
| @Override |
| public void bindNull(int index) { |
| mBindingTypes[index] = NULL; |
| } |
| |
| @Override |
| public void bindLong(int index, long value) { |
| mBindingTypes[index] = LONG; |
| mLongBindings[index] = value; |
| } |
| |
| @Override |
| public void bindDouble(int index, double value) { |
| mBindingTypes[index] = DOUBLE; |
| mDoubleBindings[index] = value; |
| } |
| |
| @Override |
| public void bindString(int index, String value) { |
| mBindingTypes[index] = STRING; |
| mStringBindings[index] = value; |
| } |
| |
| @Override |
| public void bindBlob(int index, byte[] value) { |
| mBindingTypes[index] = BLOB; |
| mBlobBindings[index] = value; |
| } |
| |
| @Override |
| public void close() { |
| // no-op. not calling release because it is internal API. |
| } |
| |
| /** |
| * Copies arguments from another RoomSQLiteQuery into this query. |
| * |
| * @param other The other query, which holds the arguments to be copied. |
| */ |
| public void copyArgumentsFrom(RoomSQLiteQuery other) { |
| int argCount = other.getArgCount() + 1; // +1 for the binding offsets |
| System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount); |
| System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount); |
| System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount); |
| System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount); |
| System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount); |
| } |
| |
| @Override |
| public void clearBindings() { |
| Arrays.fill(mBindingTypes, NULL); |
| Arrays.fill(mStringBindings, null); |
| Arrays.fill(mBlobBindings, null); |
| mQuery = null; |
| // no need to clear others |
| } |
| |
| private static final int NULL = 1; |
| private static final int LONG = 2; |
| private static final int DOUBLE = 3; |
| private static final int STRING = 4; |
| private static final int BLOB = 5; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({NULL, LONG, DOUBLE, STRING, BLOB}) |
| @interface Binding { |
| } |
| } |