Skip to content

Commit

Permalink
Added a new SQL parser. This parser supports both SQL style line comm…
Browse files Browse the repository at this point in the history
…ents and comment blocks. SQL statements can span multiple lines but must be delimited with a semicolon. This allows for improved readability of SQL scripts.

The old parser expects one statement per line which may or may not end with a semicolon. This means the new parser can also process scripts written for the old parser if those scripts end each line with a semicolon.

For backwards compatibility reasons this parser isn't used unless the manifest contains a meta-data entry "AA_SQL_PARSER" with the value "delimited". If this value isn't specified or is set to "legacy" the old parser implementation is used.
  • Loading branch information
mpfeiffermway committed Apr 16, 2014
1 parent bd98740 commit 55208f6
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 21 deletions.
44 changes: 36 additions & 8 deletions src/com/activeandroid/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,18 @@
import com.activeandroid.util.ReflectionUtils;

public class Configuration {

public final static String SQL_PARSER_LEGACY = "legacy";
public final static String SQL_PARSER_DELIMITED = "delimited";

//////////////////////////////////////////////////////////////////////////////////////
// PRIVATE MEMBERS
//////////////////////////////////////////////////////////////////////////////////////

private Context mContext;
private String mDatabaseName;
private int mDatabaseVersion;
private String mSqlParser;
private List<Class<? extends Model>> mModelClasses;
private List<Class<? extends TypeSerializer>> mTypeSerializers;
private int mCacheSize;
Expand Down Expand Up @@ -61,6 +66,10 @@ public String getDatabaseName() {
public int getDatabaseVersion() {
return mDatabaseVersion;
}

public String getSqlParser() {
return mSqlParser;
}

public List<Class<? extends Model>> getModelClasses() {
return mModelClasses;
Expand Down Expand Up @@ -91,9 +100,11 @@ public static class Builder {
private static final String AA_DB_VERSION = "AA_DB_VERSION";
private final static String AA_MODELS = "AA_MODELS";
private final static String AA_SERIALIZERS = "AA_SERIALIZERS";
private final static String AA_SQL_PARSER = "AA_SQL_PARSER";

private static final int DEFAULT_CACHE_SIZE = 1024;
private static final String DEFAULT_DB_NAME = "Application.db";
private static final String DEFAULT_SQL_PARSER = SQL_PARSER_LEGACY;

//////////////////////////////////////////////////////////////////////////////////////
// PRIVATE MEMBERS
Expand All @@ -104,6 +115,7 @@ public static class Builder {
private Integer mCacheSize;
private String mDatabaseName;
private Integer mDatabaseVersion;
private String mSqlParser;
private List<Class<? extends Model>> mModelClasses;
private List<Class<? extends TypeSerializer>> mTypeSerializers;

Expand Down Expand Up @@ -134,6 +146,11 @@ public Builder setDatabaseVersion(int databaseVersion) {
mDatabaseVersion = databaseVersion;
return this;
}

public Builder setSqlParser(String sqlParser) {
mSqlParser = sqlParser;
return this;
}

public Builder addModelClass(Class<? extends Model> modelClass) {
if (mModelClasses == null) {
Expand Down Expand Up @@ -188,24 +205,28 @@ public Configuration create() {
// Get database name from meta-data
if (mDatabaseName != null) {
configuration.mDatabaseName = mDatabaseName;
}
else {
} else {
configuration.mDatabaseName = getMetaDataDatabaseNameOrDefault();
}

// Get database version from meta-data
if (mDatabaseVersion != null) {
configuration.mDatabaseVersion = mDatabaseVersion;
}
else {
} else {
configuration.mDatabaseVersion = getMetaDataDatabaseVersionOrDefault();
}

// Get SQL parser from meta-data
if (mSqlParser != null) {
configuration.mSqlParser = mSqlParser;
} else {
configuration.mSqlParser = getMetaDataSqlParserOrDefault();
}

// Get model classes from meta-data
if (mModelClasses != null) {
configuration.mModelClasses = mModelClasses;
}
else {
} else {
final String modelList = ReflectionUtils.getMetaData(mContext, AA_MODELS);
if (modelList != null) {
configuration.mModelClasses = loadModelList(modelList.split(","));
Expand All @@ -215,8 +236,7 @@ public Configuration create() {
// Get type serializer classes from meta-data
if (mTypeSerializers != null) {
configuration.mTypeSerializers = mTypeSerializers;
}
else {
} else {
final String serializerList = ReflectionUtils.getMetaData(mContext, AA_SERIALIZERS);
if (serializerList != null) {
configuration.mTypeSerializers = loadSerializerList(serializerList.split(","));
Expand Down Expand Up @@ -250,6 +270,14 @@ private int getMetaDataDatabaseVersionOrDefault() {
return aaVersion;
}

private String getMetaDataSqlParserOrDefault() {
final String mode = ReflectionUtils.getMetaData(mContext, AA_SQL_PARSER);
if (mode == null) {
return DEFAULT_SQL_PARSER;
}
return mode;
}

private List<Class<? extends Model>> loadModelList(String[] models) {
final List<Class<? extends Model>> modelClasses = new ArrayList<Class<? extends Model>>();
final ClassLoader classLoader = mContext.getClass().getClassLoader();
Expand Down
74 changes: 61 additions & 13 deletions src/com/activeandroid/DatabaseHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import com.activeandroid.util.IOUtils;
import com.activeandroid.util.Log;
import com.activeandroid.util.NaturalOrderComparator;
import com.activeandroid.util.SQLiteUtils;
import com.activeandroid.util.SqlParser;

public final class DatabaseHelper extends SQLiteOpenHelper {
//////////////////////////////////////////////////////////////////////////////////////
Expand All @@ -42,13 +44,20 @@ public final class DatabaseHelper extends SQLiteOpenHelper {

public final static String MIGRATION_PATH = "migrations";

//////////////////////////////////////////////////////////////////////////////////////
// PRIVATE FIELDS
//////////////////////////////////////////////////////////////////////////////////////

private final String mSqlParser;

//////////////////////////////////////////////////////////////////////////////////////
// CONSTRUCTORS
//////////////////////////////////////////////////////////////////////////////////////

public DatabaseHelper(Configuration configuration) {
super(configuration.getContext(), configuration.getDatabaseName(), null, configuration.getDatabaseVersion());
copyAttachedDatabase(configuration.getContext(), configuration.getDatabaseName());
mSqlParser = configuration.getSqlParser();
}

//////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -187,22 +196,61 @@ private boolean executeMigrations(SQLiteDatabase db, int oldVersion, int newVers

return migrationExecuted;
}

private void executeSqlScript(SQLiteDatabase db, String file) {

InputStream stream = null;

try {
final InputStream input = Cache.getContext().getAssets().open(MIGRATION_PATH + "/" + file);
final BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String line = null;

while ((line = reader.readLine()) != null) {
line = line.replace(";", "").trim();
if (!line.isEmpty()) {
db.execSQL(line);
}
}
}
catch (IOException e) {
stream = Cache.getContext().getAssets().open(MIGRATION_PATH + "/" + file);

if (Configuration.SQL_PARSER_DELIMITED.equalsIgnoreCase(mSqlParser)) {
executeDelimitedSqlScript(db, stream);

} else {
executeLegacySqlScript(db, stream);

}

} catch (IOException e) {
Log.e("Failed to execute " + file, e);

} finally {
IOUtils.closeQuietly(stream);

}
}

private void executeDelimitedSqlScript(SQLiteDatabase db, InputStream stream) throws IOException {

List<String> commands = SqlParser.parse(stream);

for(String command : commands) {
db.execSQL(command);
}
}

private void executeLegacySqlScript(SQLiteDatabase db, InputStream stream) throws IOException {

InputStreamReader reader = null;
BufferedReader buffer = null;

try {
reader = new InputStreamReader(stream);
buffer = new BufferedReader(reader);
String line = null;

while ((line = buffer.readLine()) != null) {
line = line.replace(";", "").trim();
if (!line.isEmpty()) {
db.execSQL(line);
}
}

} finally {
IOUtils.closeQuietly(buffer);
IOUtils.closeQuietly(reader);

}
}
}
53 changes: 53 additions & 0 deletions src/com/activeandroid/util/IOUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

package com.activeandroid.util;

import android.database.Cursor;

import java.io.Closeable;
import java.io.IOException;

import com.activeandroid.util.Log;


public class IOUtils {

/**
* Unconditionally close a {@link Closeable}.
* <p/>
* Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. This is
* typically used in finally blocks.
* @param closeable A {@link Closeable} to close.
*/
public static void closeQuietly(final Closeable closeable) {

if (closeable == null) {
return;
}

try {
closeable.close();
} catch (final IOException e) {
Log.e("Couldn't close closeable.", e);
}
}

/**
* Unconditionally close a {@link Cursor}.
* <p/>
* Equivalent to {@link Cursor#close()}, except any exceptions will be ignored. This is
* typically used in finally blocks.
* @param cursor A {@link Cursor} to close.
*/
public static void closeQuietly(final Cursor cursor) {

if (cursor == null) {
return;
}

try {
cursor.close();
} catch (final Exception e) {
Log.e("Couldn't close cursor.", e);
}
}
}
94 changes: 94 additions & 0 deletions src/com/activeandroid/util/SqlParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@

package com.activeandroid.util;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;


public class SqlParser {

public final static int STATE_NONE = 0;
public final static int STATE_STRING = 1;
public final static int STATE_COMMENT = 2;
public final static int STATE_COMMENT_BLOCK = 3;

public static List<String> parse(final InputStream stream) throws IOException {

final BufferedInputStream buffer = new BufferedInputStream(stream);
final List<String> commands = new ArrayList<String>();
final StringBuffer sb = new StringBuffer();

try {
final Tokenizer tokenizer = new Tokenizer(buffer);
int state = STATE_NONE;

while (tokenizer.hasNext()) {
final char c = (char) tokenizer.next();

if (state == STATE_COMMENT_BLOCK) {
if (tokenizer.skip("*/")) {
state = STATE_NONE;
}
continue;

} else if (state == STATE_COMMENT) {
if (isNewLine(c)) {
state = STATE_NONE;
}
continue;

} else if (state == STATE_NONE && tokenizer.skip("/*")) {
state = STATE_COMMENT_BLOCK;
continue;

} else if (state == STATE_NONE && tokenizer.skip("--")) {
state = STATE_COMMENT;
continue;

} else if (state == STATE_NONE && c == ';') {
final String command = sb.toString();
commands.add(command);
sb.setLength(0);
continue;

} else if (state == STATE_NONE && c == '\'') {
state = STATE_STRING;

} else if (state == STATE_STRING && c == '\'') {
state = STATE_NONE;

}

if (state == STATE_NONE || state == STATE_STRING) {
if (state == STATE_NONE && isWhitespace(c)) {
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
sb.append(' ');
}
} else {
sb.append(c);
}
}
}

} finally {
IOUtils.closeQuietly(buffer);
}

if (sb.length() > 0) {
commands.add(sb.toString());
}

return commands;
}

private static boolean isNewLine(final char c) {
return c == '\r' || c == '\n';
}

private static boolean isWhitespace(final char c) {
return c == '\r' || c == '\n' || c == '\t' || c == ' ';
}
}
Loading

0 comments on commit 55208f6

Please sign in to comment.