Support for appending "standalone" WHERE chunks.
The existing appendWhere() methods aren't very friendly for developers, since they require manual tracking of state to decide if subsequent standalone chunks should be prefixed with "AND". While it's tempting to offer direct argument binding on the builder class, we can't really deliver on that API in a secure way, so instead add separate bindSelection() method which explicitly burns arguments into a standalone selection string, which can then be appended to the builder. This was the last piece of new functionality being used by SQLiteStatementBuilder, so we can delete that class and migrate users back to SQLiteQueryBuilder. Bug: 111268862 Test: atest frameworks/base/core/tests/coretests/src/android/database/DatabaseUtilsTest.java Test: atest frameworks/base/core/tests/utiltests/src/com/android/internal/util/ArrayUtilsTest.java Test: atest cts/tests/tests/provider/src/android/provider/cts/MediaStore* Test: atest cts/tests/tests/database/src/android/database/sqlite/cts/SQLiteQueryBuilderTest.java Merged-In: I418f24338c90bae8a9dad473fa76329cea00a8c5 Change-Id: I418f24338c90bae8a9dad473fa76329cea00a8c5
This commit is contained in:
committed by
Jeff Sharkey
parent
8a634372b3
commit
0da04839b7
@@ -12669,6 +12669,7 @@ package android.database.sqlite {
|
||||
method public static void appendColumns(java.lang.StringBuilder, java.lang.String[]);
|
||||
method public void appendWhere(java.lang.CharSequence);
|
||||
method public void appendWhereEscapeString(java.lang.String);
|
||||
method public void appendWhereStandalone(java.lang.CharSequence);
|
||||
method public java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
|
||||
method public deprecated java.lang.String buildQuery(java.lang.String[], java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String);
|
||||
method public static java.lang.String buildQueryString(boolean, java.lang.String, java.lang.String[], java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package android.database;
|
||||
|
||||
import android.annotation.UnsupportedAppUsage;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
@@ -35,6 +36,8 @@ import android.os.ParcelFileDescriptor;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.PrintStream;
|
||||
import java.text.Collator;
|
||||
@@ -216,6 +219,92 @@ public class DatabaseUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the given selection with the given selection arguments.
|
||||
* <p>
|
||||
* Internally assumes that '?' is only ever used for arguments, and doesn't
|
||||
* appear as a literal or escaped value.
|
||||
* <p>
|
||||
* This method is typically useful for trusted code that needs to cook up a
|
||||
* fully-bound selection.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public static @Nullable String bindSelection(@Nullable String selection,
|
||||
@Nullable Object... selectionArgs) {
|
||||
if (selection == null) return null;
|
||||
// If no arguments provided, so we can't bind anything
|
||||
if (ArrayUtils.isEmpty(selectionArgs)) return selection;
|
||||
// If no bindings requested, so we can shortcut
|
||||
if (selection.indexOf('?') == -1) return selection;
|
||||
|
||||
// Track the chars immediately before and after each bind request, to
|
||||
// decide if it needs additional whitespace added
|
||||
char before = ' ';
|
||||
char after = ' ';
|
||||
|
||||
int argIndex = 0;
|
||||
final int len = selection.length();
|
||||
final StringBuilder res = new StringBuilder(len);
|
||||
for (int i = 0; i < len; ) {
|
||||
char c = selection.charAt(i++);
|
||||
if (c == '?') {
|
||||
// Assume this bind request is guarded until we find a specific
|
||||
// trailing character below
|
||||
after = ' ';
|
||||
|
||||
// Sniff forward to see if the selection is requesting a
|
||||
// specific argument index
|
||||
int start = i;
|
||||
for (; i < len; i++) {
|
||||
c = selection.charAt(i);
|
||||
if (c < '0' || c > '9') {
|
||||
after = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (start != i) {
|
||||
argIndex = Integer.parseInt(selection.substring(start, i)) - 1;
|
||||
}
|
||||
|
||||
// Manually bind the argument into the selection, adding
|
||||
// whitespace when needed for clarity
|
||||
final Object arg = selectionArgs[argIndex++];
|
||||
if (before != ' ' && before != '=') res.append(' ');
|
||||
switch (DatabaseUtils.getTypeOfObject(arg)) {
|
||||
case Cursor.FIELD_TYPE_NULL:
|
||||
res.append("NULL");
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_INTEGER:
|
||||
res.append(((Number) arg).longValue());
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_FLOAT:
|
||||
res.append(((Number) arg).doubleValue());
|
||||
break;
|
||||
case Cursor.FIELD_TYPE_BLOB:
|
||||
throw new IllegalArgumentException("Blobs not supported");
|
||||
case Cursor.FIELD_TYPE_STRING:
|
||||
default:
|
||||
if (arg instanceof Boolean) {
|
||||
// Provide compatibility with legacy applications which may pass
|
||||
// Boolean values in bind args.
|
||||
res.append(((Boolean) arg).booleanValue() ? 1 : 0);
|
||||
} else {
|
||||
res.append('\'');
|
||||
res.append(arg.toString());
|
||||
res.append('\'');
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (after != ' ') res.append(' ');
|
||||
} else {
|
||||
res.append(c);
|
||||
before = c;
|
||||
}
|
||||
}
|
||||
return res.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data type of the given object's value.
|
||||
*<p>
|
||||
|
||||
@@ -44,8 +44,7 @@ import java.util.regex.Pattern;
|
||||
* This is a convenience class that helps build SQL queries to be sent to
|
||||
* {@link SQLiteDatabase} objects.
|
||||
*/
|
||||
public class SQLiteQueryBuilder
|
||||
{
|
||||
public class SQLiteQueryBuilder {
|
||||
private static final String TAG = "SQLiteQueryBuilder";
|
||||
private static final Pattern sLimitPattern =
|
||||
Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
|
||||
@@ -104,7 +103,7 @@ public class SQLiteQueryBuilder
|
||||
*
|
||||
* @param inWhere the chunk of text to append to the WHERE clause.
|
||||
*/
|
||||
public void appendWhere(CharSequence inWhere) {
|
||||
public void appendWhere(@NonNull CharSequence inWhere) {
|
||||
if (mWhereClause == null) {
|
||||
mWhereClause = new StringBuilder(inWhere.length() + 16);
|
||||
}
|
||||
@@ -121,13 +120,34 @@ public class SQLiteQueryBuilder
|
||||
* @param inWhere the chunk of text to append to the WHERE clause. it will be escaped
|
||||
* to avoid SQL injection attacks
|
||||
*/
|
||||
public void appendWhereEscapeString(String inWhere) {
|
||||
public void appendWhereEscapeString(@NonNull String inWhere) {
|
||||
if (mWhereClause == null) {
|
||||
mWhereClause = new StringBuilder(inWhere.length() + 16);
|
||||
}
|
||||
DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a standalone chunk to the {@code WHERE} clause of this query.
|
||||
* <p>
|
||||
* This method differs from {@link #appendWhere(CharSequence)} in that it
|
||||
* automatically appends {@code AND} to any existing {@code WHERE} clause
|
||||
* already under construction before appending the given standalone
|
||||
* expression wrapped in parentheses.
|
||||
*
|
||||
* @param inWhere the standalone expression to append to the {@code WHERE}
|
||||
* clause. It will be wrapped in parentheses when it's appended.
|
||||
*/
|
||||
public void appendWhereStandalone(@NonNull CharSequence inWhere) {
|
||||
if (mWhereClause == null) {
|
||||
mWhereClause = new StringBuilder(inWhere.length() + 16);
|
||||
}
|
||||
if (mWhereClause.length() > 0) {
|
||||
mWhereClause.append(" AND ");
|
||||
}
|
||||
mWhereClause.append('(').append(inWhere).append(')');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the projection map for the query. The projection map maps
|
||||
* from column names that the caller passes into query to database
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -309,7 +309,7 @@ public class ArrayUtils {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static @NonNull <T> T[] concat(Class<T> kind, @Nullable T[] a, @Nullable T[] b) {
|
||||
public static @NonNull <T> T[] concatElements(Class<T> kind, @Nullable T[] a, @Nullable T[] b) {
|
||||
final int an = (a != null) ? a.length : 0;
|
||||
final int bn = (b != null) ? b.length : 0;
|
||||
if (an == 0 && bn == 0) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.database;
|
||||
|
||||
import static android.database.DatabaseUtils.bindSelection;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DatabaseUtilsTest {
|
||||
private static final Object[] ARGS = { "baz", 4, null };
|
||||
|
||||
@Test
|
||||
public void testBindSelection_none() throws Exception {
|
||||
assertEquals(null,
|
||||
bindSelection(null, ARGS));
|
||||
assertEquals("",
|
||||
bindSelection("", ARGS));
|
||||
assertEquals("foo=bar",
|
||||
bindSelection("foo=bar", ARGS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindSelection_normal() throws Exception {
|
||||
assertEquals("foo='baz'",
|
||||
bindSelection("foo=?", ARGS));
|
||||
assertEquals("foo='baz' AND bar=4",
|
||||
bindSelection("foo=? AND bar=?", ARGS));
|
||||
assertEquals("foo='baz' AND bar=4 AND meow=NULL",
|
||||
bindSelection("foo=? AND bar=? AND meow=?", ARGS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindSelection_whitespace() throws Exception {
|
||||
assertEquals("BETWEEN 5 AND 10",
|
||||
bindSelection("BETWEEN? AND ?", 5, 10));
|
||||
assertEquals("IN 'foo'",
|
||||
bindSelection("IN?", "foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBindSelection_indexed() throws Exception {
|
||||
assertEquals("foo=10 AND bar=11 AND meow=1",
|
||||
bindSelection("foo=?10 AND bar=? AND meow=?1",
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package com.android.internal.util;
|
||||
|
||||
import static com.android.internal.util.ArrayUtils.concatElements;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
@@ -156,23 +158,23 @@ public class ArrayUtilsTest extends TestCase {
|
||||
|
||||
public void testConcatEmpty() throws Exception {
|
||||
assertArrayEquals(new Long[] {},
|
||||
ArrayUtils.concat(Long.class, null, null));
|
||||
concatElements(Long.class, null, null));
|
||||
assertArrayEquals(new Long[] {},
|
||||
ArrayUtils.concat(Long.class, new Long[] {}, null));
|
||||
concatElements(Long.class, new Long[] {}, null));
|
||||
assertArrayEquals(new Long[] {},
|
||||
ArrayUtils.concat(Long.class, null, new Long[] {}));
|
||||
concatElements(Long.class, null, new Long[] {}));
|
||||
assertArrayEquals(new Long[] {},
|
||||
ArrayUtils.concat(Long.class, new Long[] {}, new Long[] {}));
|
||||
concatElements(Long.class, new Long[] {}, new Long[] {}));
|
||||
}
|
||||
|
||||
public void testConcat() throws Exception {
|
||||
public void testconcatElements() throws Exception {
|
||||
assertArrayEquals(new Long[] { 1L },
|
||||
ArrayUtils.concat(Long.class, new Long[] { 1L }, new Long[] {}));
|
||||
concatElements(Long.class, new Long[] { 1L }, new Long[] {}));
|
||||
assertArrayEquals(new Long[] { 1L },
|
||||
ArrayUtils.concat(Long.class, new Long[] {}, new Long[] { 1L }));
|
||||
concatElements(Long.class, new Long[] {}, new Long[] { 1L }));
|
||||
assertArrayEquals(new Long[] { 1L, 2L },
|
||||
ArrayUtils.concat(Long.class, new Long[] { 1L }, new Long[] { 2L }));
|
||||
concatElements(Long.class, new Long[] { 1L }, new Long[] { 2L }));
|
||||
assertArrayEquals(new Long[] { 1L, 2L, 3L, 4L },
|
||||
ArrayUtils.concat(Long.class, new Long[] { 1L, 2L }, new Long[] { 3L, 4L }));
|
||||
concatElements(Long.class, new Long[] { 1L, 2L }, new Long[] { 3L, 4L }));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user