diff --git a/app/src/main/java/com/activeandroid/ActiveAndroid.java b/app/src/main/java/com/activeandroid/ActiveAndroid.java new file mode 100644 index 000000000..c58c8efd8 --- /dev/null +++ b/app/src/main/java/com/activeandroid/ActiveAndroid.java @@ -0,0 +1,86 @@ +package com.activeandroid; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; + +import com.activeandroid.util.Log; + +public final class ActiveAndroid { + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public static void initialize(Context context) { + initialize(new Configuration.Builder(context).create()); + } + + public static void initialize(Configuration configuration) { + initialize(configuration, false); + } + + public static void initialize(Context context, boolean loggingEnabled) { + initialize(new Configuration.Builder(context).create(), loggingEnabled); + } + + public static void initialize(Configuration configuration, boolean loggingEnabled) { + // Set logging enabled first + setLoggingEnabled(loggingEnabled); + Cache.initialize(configuration); + } + + public static void clearCache() { + Cache.clear(); + } + + public static void dispose() { + Cache.dispose(); + } + + public static void setLoggingEnabled(boolean enabled) { + Log.setEnabled(enabled); + } + + public static SQLiteDatabase getDatabase() { + return Cache.openDatabase(); + } + + public static void beginTransaction() { + Cache.openDatabase().beginTransaction(); + } + + public static void endTransaction() { + Cache.openDatabase().endTransaction(); + } + + public static void setTransactionSuccessful() { + Cache.openDatabase().setTransactionSuccessful(); + } + + public static boolean inTransaction() { + return Cache.openDatabase().inTransaction(); + } + + public static void execSQL(String sql) { + Cache.openDatabase().execSQL(sql); + } + + public static void execSQL(String sql, Object[] bindArgs) { + Cache.openDatabase().execSQL(sql, bindArgs); + } +} diff --git a/app/src/main/java/com/activeandroid/Cache.java b/app/src/main/java/com/activeandroid/Cache.java new file mode 100644 index 000000000..6495e3790 --- /dev/null +++ b/app/src/main/java/com/activeandroid/Cache.java @@ -0,0 +1,158 @@ +package com.activeandroid; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.util.Collection; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.support.v4.util.LruCache; + +import com.activeandroid.serializer.TypeSerializer; +import com.activeandroid.util.Log; + +public final class Cache { + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC CONSTANTS + ////////////////////////////////////////////////////////////////////////////////////// + + public static final int DEFAULT_CACHE_SIZE = 1024; + + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE MEMBERS + ////////////////////////////////////////////////////////////////////////////////////// + + private static Context sContext; + + private static ModelInfo sModelInfo; + private static DatabaseHelper sDatabaseHelper; + + private static LruCache sEntities; + + private static boolean sIsInitialized = false; + + ////////////////////////////////////////////////////////////////////////////////////// + // CONSTRUCTORS + ////////////////////////////////////////////////////////////////////////////////////// + + private Cache() { + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public static synchronized void initialize(Configuration configuration) { + if (sIsInitialized) { + Log.v("ActiveAndroid already initialized."); + return; + } + + sContext = configuration.getContext(); + sModelInfo = new ModelInfo(configuration); + sDatabaseHelper = new DatabaseHelper(configuration); + + // TODO: It would be nice to override sizeOf here and calculate the memory + // actually used, however at this point it seems like the reflection + // required would be too costly to be of any benefit. We'll just set a max + // object size instead. + sEntities = new LruCache(configuration.getCacheSize()); + + openDatabase(); + + sIsInitialized = true; + + Log.v("ActiveAndroid initialized successfully."); + } + + public static synchronized void clear() { + sEntities.evictAll(); + Log.v("Cache cleared."); + } + + public static synchronized void dispose() { + closeDatabase(); + + sEntities = null; + sModelInfo = null; + sDatabaseHelper = null; + + sIsInitialized = false; + + Log.v("ActiveAndroid disposed. Call initialize to use library."); + } + + // Database access + + public static boolean isInitialized() { + return sIsInitialized; + } + + public static synchronized SQLiteDatabase openDatabase() { + return sDatabaseHelper.getWritableDatabase(); + } + + public static synchronized void closeDatabase() { + sDatabaseHelper.close(); + } + + // Context access + + public static Context getContext() { + return sContext; + } + + // Entity cache + + public static String getIdentifier(Class type, Long id) { + return getTableName(type) + "@" + id; + } + + public static String getIdentifier(Model entity) { + return getIdentifier(entity.getClass(), entity.getId()); + } + + public static synchronized void addEntity(Model entity) { + sEntities.put(getIdentifier(entity), entity); + } + + public static synchronized Model getEntity(Class type, long id) { + return sEntities.get(getIdentifier(type, id)); + } + + public static synchronized void removeEntity(Model entity) { + sEntities.remove(getIdentifier(entity)); + } + + // Model cache + + public static synchronized Collection getTableInfos() { + return sModelInfo.getTableInfos(); + } + + public static synchronized TableInfo getTableInfo(Class type) { + return sModelInfo.getTableInfo(type); + } + + public static synchronized TypeSerializer getParserForType(Class type) { + return sModelInfo.getTypeSerializer(type); + } + + public static synchronized String getTableName(Class type) { + return sModelInfo.getTableInfo(type).getTableName(); + } +} diff --git a/app/src/main/java/com/activeandroid/Configuration.java b/app/src/main/java/com/activeandroid/Configuration.java new file mode 100644 index 000000000..b197d2234 --- /dev/null +++ b/app/src/main/java/com/activeandroid/Configuration.java @@ -0,0 +1,318 @@ +package com.activeandroid; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.content.Context; + +import com.activeandroid.serializer.TypeSerializer; +import com.activeandroid.util.Log; +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> mModelClasses; + private List> mTypeSerializers; + private int mCacheSize; + + ////////////////////////////////////////////////////////////////////////////////////// + // CONSTRUCTORS + ////////////////////////////////////////////////////////////////////////////////////// + + private Configuration(Context context) { + mContext = context; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public Context getContext() { + return mContext; + } + + public String getDatabaseName() { + return mDatabaseName; + } + + public int getDatabaseVersion() { + return mDatabaseVersion; + } + + public String getSqlParser() { + return mSqlParser; + } + + public List> getModelClasses() { + return mModelClasses; + } + + public List> getTypeSerializers() { + return mTypeSerializers; + } + + public int getCacheSize() { + return mCacheSize; + } + + public boolean isValid() { + return mModelClasses != null && mModelClasses.size() > 0; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // INNER CLASSES + ////////////////////////////////////////////////////////////////////////////////////// + + public static class Builder { + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE CONSTANTS + ////////////////////////////////////////////////////////////////////////////////////// + + private static final String AA_DB_NAME = "AA_DB_NAME"; + 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 + ////////////////////////////////////////////////////////////////////////////////////// + + private Context mContext; + + private Integer mCacheSize; + private String mDatabaseName; + private Integer mDatabaseVersion; + private String mSqlParser; + private List> mModelClasses; + private List> mTypeSerializers; + + ////////////////////////////////////////////////////////////////////////////////////// + // CONSTRUCTORS + ////////////////////////////////////////////////////////////////////////////////////// + + public Builder(Context context) { + mContext = context.getApplicationContext(); + mCacheSize = DEFAULT_CACHE_SIZE; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public Builder setCacheSize(int cacheSize) { + mCacheSize = cacheSize; + return this; + } + + public Builder setDatabaseName(String databaseName) { + mDatabaseName = databaseName; + return this; + } + + public Builder setDatabaseVersion(int databaseVersion) { + mDatabaseVersion = databaseVersion; + return this; + } + + public Builder setSqlParser(String sqlParser) { + mSqlParser = sqlParser; + return this; + } + + public Builder addModelClass(Class modelClass) { + if (mModelClasses == null) { + mModelClasses = new ArrayList>(); + } + + mModelClasses.add(modelClass); + return this; + } + + public Builder addModelClasses(Class... modelClasses) { + if (mModelClasses == null) { + mModelClasses = new ArrayList>(); + } + + mModelClasses.addAll(Arrays.asList(modelClasses)); + return this; + } + + public Builder setModelClasses(Class... modelClasses) { + mModelClasses = Arrays.asList(modelClasses); + return this; + } + + public Builder addTypeSerializer(Class typeSerializer) { + if (mTypeSerializers == null) { + mTypeSerializers = new ArrayList>(); + } + + mTypeSerializers.add(typeSerializer); + return this; + } + + public Builder addTypeSerializers(Class... typeSerializers) { + if (mTypeSerializers == null) { + mTypeSerializers = new ArrayList>(); + } + + mTypeSerializers.addAll(Arrays.asList(typeSerializers)); + return this; + } + + public Builder setTypeSerializers(Class... typeSerializers) { + mTypeSerializers = Arrays.asList(typeSerializers); + return this; + } + + public Configuration create() { + Configuration configuration = new Configuration(mContext); + configuration.mCacheSize = mCacheSize; + + // Get database name from meta-data + if (mDatabaseName != null) { + configuration.mDatabaseName = mDatabaseName; + } else { + configuration.mDatabaseName = getMetaDataDatabaseNameOrDefault(); + } + + // Get database version from meta-data + if (mDatabaseVersion != null) { + configuration.mDatabaseVersion = mDatabaseVersion; + } 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 { + final String modelList = ReflectionUtils.getMetaData(mContext, AA_MODELS); + if (modelList != null) { + configuration.mModelClasses = loadModelList(modelList.split(",")); + } + } + + // Get type serializer classes from meta-data + if (mTypeSerializers != null) { + configuration.mTypeSerializers = mTypeSerializers; + } else { + final String serializerList = ReflectionUtils.getMetaData(mContext, AA_SERIALIZERS); + if (serializerList != null) { + configuration.mTypeSerializers = loadSerializerList(serializerList.split(",")); + } + } + + return configuration; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + // Meta-data methods + + private String getMetaDataDatabaseNameOrDefault() { + String aaName = ReflectionUtils.getMetaData(mContext, AA_DB_NAME); + if (aaName == null) { + aaName = DEFAULT_DB_NAME; + } + + return aaName; + } + + private int getMetaDataDatabaseVersionOrDefault() { + Integer aaVersion = ReflectionUtils.getMetaData(mContext, AA_DB_VERSION); + if (aaVersion == null || aaVersion == 0) { + aaVersion = 1; + } + + 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> loadModelList(String[] models) { + final List> modelClasses = new ArrayList>(); + final ClassLoader classLoader = mContext.getClass().getClassLoader(); + for (String model : models) { + try { + Class modelClass = Class.forName(model.trim(), false, classLoader); + if (ReflectionUtils.isModel(modelClass)) { + modelClasses.add(modelClass); + } + } + catch (ClassNotFoundException e) { + Log.e("Couldn't create class.", e); + } + } + + return modelClasses; + } + + private List> loadSerializerList(String[] serializers) { + final List> typeSerializers = new ArrayList>(); + final ClassLoader classLoader = mContext.getClass().getClassLoader(); + for (String serializer : serializers) { + try { + Class serializerClass = Class.forName(serializer.trim(), false, classLoader); + if (ReflectionUtils.isTypeSerializer(serializerClass)) { + typeSerializers.add(serializerClass); + } + } + catch (ClassNotFoundException e) { + Log.e("Couldn't create class.", e); + } + } + + return typeSerializers; + } + + } +} diff --git a/app/src/main/java/com/activeandroid/DatabaseHelper.java b/app/src/main/java/com/activeandroid/DatabaseHelper.java new file mode 100644 index 000000000..7158c5bb6 --- /dev/null +++ b/app/src/main/java/com/activeandroid/DatabaseHelper.java @@ -0,0 +1,257 @@ +package com.activeandroid; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.text.TextUtils; + +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 { + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC CONSTANTS + ////////////////////////////////////////////////////////////////////////////////////// + + 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(); + } + + ////////////////////////////////////////////////////////////////////////////////////// + // OVERRIDEN METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onOpen(SQLiteDatabase db) { + executePragmas(db); + }; + + @Override + public void onCreate(SQLiteDatabase db) { + executePragmas(db); + executeCreate(db); + executeMigrations(db, -1, db.getVersion()); + executeCreateIndex(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + executePragmas(db); + executeCreate(db); + executeMigrations(db, oldVersion, newVersion); + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public void copyAttachedDatabase(Context context, String databaseName) { + final File dbPath = context.getDatabasePath(databaseName); + + // If the database already exists, return + if (dbPath.exists()) { + return; + } + + // Make sure we have a path to the file + dbPath.getParentFile().mkdirs(); + + // Try to copy database file + try { + final InputStream inputStream = context.getAssets().open(databaseName); + final OutputStream output = new FileOutputStream(dbPath); + + byte[] buffer = new byte[8192]; + int length; + + while ((length = inputStream.read(buffer, 0, 8192)) > 0) { + output.write(buffer, 0, length); + } + + output.flush(); + output.close(); + inputStream.close(); + } + catch (IOException e) { + Log.e("Failed to open file", e); + } + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + private void executePragmas(SQLiteDatabase db) { + if (SQLiteUtils.FOREIGN_KEYS_SUPPORTED) { + db.execSQL("PRAGMA foreign_keys=ON;"); + Log.i("Foreign Keys supported. Enabling foreign key features."); + } + } + + private void executeCreateIndex(SQLiteDatabase db) { + db.beginTransaction(); + try { + for (TableInfo tableInfo : Cache.getTableInfos()) { + String[] definitions = SQLiteUtils.createIndexDefinition(tableInfo); + + for (String definition : definitions) { + db.execSQL(definition); + } + } + db.setTransactionSuccessful(); + } + finally { + db.endTransaction(); + } + } + + private void executeCreate(SQLiteDatabase db) { + db.beginTransaction(); + try { + for (TableInfo tableInfo : Cache.getTableInfos()) { + db.execSQL(SQLiteUtils.createTableDefinition(tableInfo)); + } + db.setTransactionSuccessful(); + } + finally { + db.endTransaction(); + } + } + + private boolean executeMigrations(SQLiteDatabase db, int oldVersion, int newVersion) { + boolean migrationExecuted = false; + try { + final List files = Arrays.asList(Cache.getContext().getAssets().list(MIGRATION_PATH)); + Collections.sort(files, new NaturalOrderComparator()); + + db.beginTransaction(); + try { + for (String file : files) { + try { + final int version = Integer.valueOf(file.replace(".sql", "")); + + if (version > oldVersion && version <= newVersion) { + executeSqlScript(db, file); + migrationExecuted = true; + + Log.i(file + " executed succesfully."); + } + } + catch (NumberFormatException e) { + Log.w("Skipping invalidly named file: " + file, e); + } + } + db.setTransactionSuccessful(); + } + finally { + db.endTransaction(); + } + } + catch (IOException e) { + Log.e("Failed to execute migrations.", e); + } + + return migrationExecuted; + } + + private void executeSqlScript(SQLiteDatabase db, String file) { + + InputStream stream = null; + + try { + 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 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 (!TextUtils.isEmpty(line)) { + db.execSQL(line); + } + } + + } finally { + IOUtils.closeQuietly(buffer); + IOUtils.closeQuietly(reader); + + } + } +} diff --git a/app/src/main/java/com/activeandroid/Model.java b/app/src/main/java/com/activeandroid/Model.java new file mode 100644 index 000000000..4d22d9c89 --- /dev/null +++ b/app/src/main/java/com/activeandroid/Model.java @@ -0,0 +1,314 @@ +package com.activeandroid; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; +import com.activeandroid.serializer.TypeSerializer; +import com.activeandroid.util.Log; +import com.activeandroid.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("unchecked") +public abstract class Model { + + /** Prime number used for hashcode() implementation. */ + private static final int HASH_PRIME = 739; + + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE MEMBERS + ////////////////////////////////////////////////////////////////////////////////////// + + private Long mId = null; + + private final TableInfo mTableInfo; + private final String idName; + ////////////////////////////////////////////////////////////////////////////////////// + // CONSTRUCTORS + ////////////////////////////////////////////////////////////////////////////////////// + + public Model() { + mTableInfo = Cache.getTableInfo(getClass()); + idName = mTableInfo.getIdName(); + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public final Long getId() { + return mId; + } + + public final void delete() { + Cache.openDatabase().delete(mTableInfo.getTableName(), idName+"=?", new String[] { getId().toString() }); + Cache.removeEntity(this); + } + + public final Long save() { + final SQLiteDatabase db = Cache.openDatabase(); + final ContentValues values = new ContentValues(); + + for (Field field : mTableInfo.getFields()) { + final String fieldName = mTableInfo.getColumnName(field); + Class fieldType = field.getType(); + + field.setAccessible(true); + + try { + Object value = field.get(this); + + if (value != null) { + final TypeSerializer typeSerializer = Cache.getParserForType(fieldType); + if (typeSerializer != null) { + // serialize data + value = typeSerializer.serialize(value); + // set new object type + if (value != null) { + fieldType = value.getClass(); + // check that the serializer returned what it promised + if (!fieldType.equals(typeSerializer.getSerializedType())) { + Log.w(String.format("TypeSerializer returned wrong type: expected a %s but got a %s", + typeSerializer.getSerializedType(), fieldType)); + } + } + } + } + + // TODO: Find a smarter way to do this? This if block is necessary because we + // can't know the type until runtime. + if (value == null) { + values.putNull(fieldName); + } + else if (fieldType.equals(Byte.class) || fieldType.equals(byte.class)) { + values.put(fieldName, (Byte) value); + } + else if (fieldType.equals(Short.class) || fieldType.equals(short.class)) { + values.put(fieldName, (Short) value); + } + else if (fieldType.equals(Integer.class) || fieldType.equals(int.class)) { + values.put(fieldName, (Integer) value); + } + else if (fieldType.equals(Long.class) || fieldType.equals(long.class)) { + values.put(fieldName, (Long) value); + } + else if (fieldType.equals(Float.class) || fieldType.equals(float.class)) { + values.put(fieldName, (Float) value); + } + else if (fieldType.equals(Double.class) || fieldType.equals(double.class)) { + values.put(fieldName, (Double) value); + } + else if (fieldType.equals(Boolean.class) || fieldType.equals(boolean.class)) { + values.put(fieldName, (Boolean) value); + } + else if (fieldType.equals(Character.class) || fieldType.equals(char.class)) { + values.put(fieldName, value.toString()); + } + else if (fieldType.equals(String.class)) { + values.put(fieldName, value.toString()); + } + else if (fieldType.equals(Byte[].class) || fieldType.equals(byte[].class)) { + values.put(fieldName, (byte[]) value); + } + else if (ReflectionUtils.isModel(fieldType)) { + values.put(fieldName, ((Model) value).getId()); + } + else if (ReflectionUtils.isSubclassOf(fieldType, Enum.class)) { + values.put(fieldName, ((Enum) value).name()); + } + } + catch (IllegalArgumentException e) { + Log.e(e.getClass().getName(), e); + } + catch (IllegalAccessException e) { + Log.e(e.getClass().getName(), e); + } + } + + if (mId == null) { + mId = db.insert(mTableInfo.getTableName(), null, values); + } + else { + db.update(mTableInfo.getTableName(), values, idName+"=" + mId, null); + } + + return mId; + } + + // Convenience methods + + public static void delete(Class type, long id) { + TableInfo tableInfo = Cache.getTableInfo(type); + new Delete().from(type).where(tableInfo.getIdName()+"=?", id).execute(); + } + + public static T load(Class type, long id) { + TableInfo tableInfo = Cache.getTableInfo(type); + return (T) new Select().from(type).where(tableInfo.getIdName()+"=?", id).executeSingle(); + } + + // Model population + + public final void loadFromCursor(Cursor cursor) { + /** + * Obtain the columns ordered to fix issue #106 (https://github.com/pardom/ActiveAndroid/issues/106) + * when the cursor have multiple columns with same name obtained from join tables. + */ + List columnsOrdered = new ArrayList(Arrays.asList(cursor.getColumnNames())); + for (Field field : mTableInfo.getFields()) { + final String fieldName = mTableInfo.getColumnName(field); + Class fieldType = field.getType(); + final int columnIndex = columnsOrdered.indexOf(fieldName); + + if (columnIndex < 0) { + continue; + } + + field.setAccessible(true); + + try { + boolean columnIsNull = cursor.isNull(columnIndex); + TypeSerializer typeSerializer = Cache.getParserForType(fieldType); + Object value = null; + + if (typeSerializer != null) { + fieldType = typeSerializer.getSerializedType(); + } + + // TODO: Find a smarter way to do this? This if block is necessary because we + // can't know the type until runtime. + if (columnIsNull) { + field = null; + } + else if (fieldType.equals(Byte.class) || fieldType.equals(byte.class)) { + value = cursor.getInt(columnIndex); + } + else if (fieldType.equals(Short.class) || fieldType.equals(short.class)) { + value = cursor.getInt(columnIndex); + } + else if (fieldType.equals(Integer.class) || fieldType.equals(int.class)) { + value = cursor.getInt(columnIndex); + } + else if (fieldType.equals(Long.class) || fieldType.equals(long.class)) { + value = cursor.getLong(columnIndex); + } + else if (fieldType.equals(Float.class) || fieldType.equals(float.class)) { + value = cursor.getFloat(columnIndex); + } + else if (fieldType.equals(Double.class) || fieldType.equals(double.class)) { + value = cursor.getDouble(columnIndex); + } + else if (fieldType.equals(Boolean.class) || fieldType.equals(boolean.class)) { + value = cursor.getInt(columnIndex) != 0; + } + else if (fieldType.equals(Character.class) || fieldType.equals(char.class)) { + value = cursor.getString(columnIndex).charAt(0); + } + else if (fieldType.equals(String.class)) { + value = cursor.getString(columnIndex); + } + else if (fieldType.equals(Byte[].class) || fieldType.equals(byte[].class)) { + value = cursor.getBlob(columnIndex); + } + else if (ReflectionUtils.isModel(fieldType)) { + final long entityId = cursor.getLong(columnIndex); + final Class entityType = (Class) fieldType; + + Model entity = Cache.getEntity(entityType, entityId); + if (entity == null) { + entity = new Select().from(entityType).where(idName+"=?", entityId).executeSingle(); + } + + value = entity; + } + else if (ReflectionUtils.isSubclassOf(fieldType, Enum.class)) { + @SuppressWarnings("rawtypes") + final Class enumType = (Class) fieldType; + value = Enum.valueOf(enumType, cursor.getString(columnIndex)); + } + + // Use a deserializer if one is available + if (typeSerializer != null && !columnIsNull) { + value = typeSerializer.deserialize(value); + } + + // Set the field value + if (value != null) { + field.set(this, value); + } + } + catch (IllegalArgumentException e) { + Log.e(e.getClass().getName(), e); + } + catch (IllegalAccessException e) { + Log.e(e.getClass().getName(), e); + } + catch (SecurityException e) { + Log.e(e.getClass().getName(), e); + } + } + + if (mId != null) { + Cache.addEntity(this); + } + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PROTECTED METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + protected final List getMany(Class type, String foreignKey) { + return new Select().from(type).where(Cache.getTableName(type) + "." + foreignKey + "=?", getId()).execute(); + } + + ////////////////////////////////////////////////////////////////////////////////////// + // OVERRIDEN METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String toString() { + return mTableInfo.getTableName() + "@" + getId(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Model && this.mId != null) { + final Model other = (Model) obj; + + return this.mId.equals(other.mId) + && (this.mTableInfo.getTableName().equals(other.mTableInfo.getTableName())); + } else { + return this == obj; + } + } + + @Override + public int hashCode() { + int hash = HASH_PRIME; + hash += HASH_PRIME * (mId == null ? super.hashCode() : mId.hashCode()); //if id is null, use Object.hashCode() + hash += HASH_PRIME * mTableInfo.getTableName().hashCode(); + return hash; //To change body of generated methods, choose Tools | Templates. + } +} diff --git a/app/src/main/java/com/activeandroid/ModelInfo.java b/app/src/main/java/com/activeandroid/ModelInfo.java new file mode 100644 index 000000000..09e79117c --- /dev/null +++ b/app/src/main/java/com/activeandroid/ModelInfo.java @@ -0,0 +1,209 @@ +package com.activeandroid; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import android.content.Context; + +import com.activeandroid.serializer.CalendarSerializer; +import com.activeandroid.serializer.SqlDateSerializer; +import com.activeandroid.serializer.TypeSerializer; +import com.activeandroid.serializer.UtilDateSerializer; +import com.activeandroid.serializer.FileSerializer; +import com.activeandroid.util.Log; +import com.activeandroid.util.ReflectionUtils; +import dalvik.system.DexFile; + +final class ModelInfo { + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + private Map, TableInfo> mTableInfos = new HashMap, TableInfo>(); + private Map, TypeSerializer> mTypeSerializers = new HashMap, TypeSerializer>() { + { + put(Calendar.class, new CalendarSerializer()); + put(java.sql.Date.class, new SqlDateSerializer()); + put(java.util.Date.class, new UtilDateSerializer()); + put(java.io.File.class, new FileSerializer()); + } + }; + + ////////////////////////////////////////////////////////////////////////////////////// + // CONSTRUCTORS + ////////////////////////////////////////////////////////////////////////////////////// + + public ModelInfo(Configuration configuration) { + if (!loadModelFromMetaData(configuration)) { + try { + scanForModel(configuration.getContext()); + } + catch (IOException e) { + Log.e("Couldn't open source path.", e); + } + } + + Log.i("ModelInfo loaded."); + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public Collection getTableInfos() { + return mTableInfos.values(); + } + + public TableInfo getTableInfo(Class type) { + return mTableInfos.get(type); + } + + public TypeSerializer getTypeSerializer(Class type) { + return mTypeSerializers.get(type); + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + private boolean loadModelFromMetaData(Configuration configuration) { + if (!configuration.isValid()) { + return false; + } + + final List> models = configuration.getModelClasses(); + if (models != null) { + for (Class model : models) { + mTableInfos.put(model, new TableInfo(model)); + } + } + + final List> typeSerializers = configuration.getTypeSerializers(); + if (typeSerializers != null) { + for (Class typeSerializer : typeSerializers) { + try { + TypeSerializer instance = typeSerializer.newInstance(); + mTypeSerializers.put(instance.getDeserializedType(), instance); + } + catch (InstantiationException e) { + Log.e("Couldn't instantiate TypeSerializer.", e); + } + catch (IllegalAccessException e) { + Log.e("IllegalAccessException", e); + } + } + } + + return true; + } + + private void scanForModel(Context context) throws IOException { + String packageName = context.getPackageName(); + String sourcePath = context.getApplicationInfo().sourceDir; + List paths = new ArrayList(); + + if (sourcePath != null && !(new File(sourcePath).isDirectory())) { + DexFile dexfile = new DexFile(sourcePath); + Enumeration entries = dexfile.entries(); + + while (entries.hasMoreElements()) { + paths.add(entries.nextElement()); + } + } + // Robolectric fallback + else { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration resources = classLoader.getResources(""); + + while (resources.hasMoreElements()) { + String path = resources.nextElement().getFile(); + if (path.contains("bin") || path.contains("classes")) { + paths.add(path); + } + } + } + + for (String path : paths) { + File file = new File(path); + scanForModelClasses(file, packageName, context.getClassLoader()); + } + } + + private void scanForModelClasses(File path, String packageName, ClassLoader classLoader) { + if (path.isDirectory()) { + for (File file : path.listFiles()) { + scanForModelClasses(file, packageName, classLoader); + } + } + else { + String className = path.getName(); + + // Robolectric fallback + if (!path.getPath().equals(className)) { + className = path.getPath(); + + if (className.endsWith(".class")) { + className = className.substring(0, className.length() - 6); + } + else { + return; + } + + className = className.replace(System.getProperty("file.separator"), "."); + + int packageNameIndex = className.lastIndexOf(packageName); + if (packageNameIndex < 0) { + return; + } + + className = className.substring(packageNameIndex); + } + + try { + Class discoveredClass = Class.forName(className, false, classLoader); + if (ReflectionUtils.isModel(discoveredClass)) { + @SuppressWarnings("unchecked") + Class modelClass = (Class) discoveredClass; + mTableInfos.put(modelClass, new TableInfo(modelClass)); + } + else if (ReflectionUtils.isTypeSerializer(discoveredClass)) { + TypeSerializer instance = (TypeSerializer) discoveredClass.newInstance(); + mTypeSerializers.put(instance.getDeserializedType(), instance); + } + } + catch (ClassNotFoundException e) { + Log.e("Couldn't create class.", e); + } + catch (InstantiationException e) { + Log.e("Couldn't instantiate TypeSerializer.", e); + } + catch (IllegalAccessException e) { + Log.e("IllegalAccessException", e); + } + } + } +} diff --git a/app/src/main/java/com/activeandroid/TableInfo.java b/app/src/main/java/com/activeandroid/TableInfo.java new file mode 100644 index 000000000..32d1ecb3f --- /dev/null +++ b/app/src/main/java/com/activeandroid/TableInfo.java @@ -0,0 +1,124 @@ +package com.activeandroid; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import android.text.TextUtils; +import android.util.Log; + +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.util.ReflectionUtils; + +public final class TableInfo { + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE MEMBERS + ////////////////////////////////////////////////////////////////////////////////////// + + private Class mType; + private String mTableName; + private String mIdName = Table.DEFAULT_ID_NAME; + + private Map mColumnNames = new LinkedHashMap(); + + ////////////////////////////////////////////////////////////////////////////////////// + // CONSTRUCTORS + ////////////////////////////////////////////////////////////////////////////////////// + + public TableInfo(Class type) { + mType = type; + + final Table tableAnnotation = type.getAnnotation(Table.class); + + if (tableAnnotation != null) { + mTableName = tableAnnotation.name(); + mIdName = tableAnnotation.id(); + } + else { + mTableName = type.getSimpleName(); + } + + // Manually add the id column since it is not declared like the other columns. + Field idField = getIdField(type); + mColumnNames.put(idField, mIdName); + + List fields = new LinkedList(ReflectionUtils.getDeclaredColumnFields(type)); + Collections.reverse(fields); + + for (Field field : fields) { + if (field.isAnnotationPresent(Column.class)) { + final Column columnAnnotation = field.getAnnotation(Column.class); + String columnName = columnAnnotation.name(); + if (TextUtils.isEmpty(columnName)) { + columnName = field.getName(); + } + + mColumnNames.put(field, columnName); + } + } + + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public Class getType() { + return mType; + } + + public String getTableName() { + return mTableName; + } + + public String getIdName() { + return mIdName; + } + + public Collection getFields() { + return mColumnNames.keySet(); + } + + public String getColumnName(Field field) { + return mColumnNames.get(field); + } + + + private Field getIdField(Class type) { + if (type.equals(Model.class)) { + try { + return type.getDeclaredField("mId"); + } + catch (NoSuchFieldException e) { + Log.e("Impossible!", e.toString()); + } + } + else if (type.getSuperclass() != null) { + return getIdField(type.getSuperclass()); + } + + return null; + } + +} diff --git a/app/src/main/java/com/activeandroid/annotation/Column.java b/app/src/main/java/com/activeandroid/annotation/Column.java new file mode 100644 index 000000000..56bbcde85 --- /dev/null +++ b/app/src/main/java/com/activeandroid/annotation/Column.java @@ -0,0 +1,110 @@ +package com.activeandroid.annotation; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Column { + public enum ConflictAction { + ROLLBACK, ABORT, FAIL, IGNORE, REPLACE + } + + public enum ForeignKeyAction { + SET_NULL, SET_DEFAULT, CASCADE, RESTRICT, NO_ACTION + } + + public String name() default ""; + + public int length() default -1; + + public boolean notNull() default false; + + public ConflictAction onNullConflict() default ConflictAction.FAIL; + + public ForeignKeyAction onDelete() default ForeignKeyAction.NO_ACTION; + + public ForeignKeyAction onUpdate() default ForeignKeyAction.NO_ACTION; + + public boolean unique() default false; + + public ConflictAction onUniqueConflict() default ConflictAction.FAIL; + + /* + * If set uniqueGroups = {"group_name"}, we will create a table constraint with group. + * + * Example: + * + * @Table(name = "table_name") + * public class Table extends Model { + * @Column(name = "member1", uniqueGroups = {"group1"}, onUniqueConflicts = {ConflictAction.FAIL}) + * public String member1; + * + * @Column(name = "member2", uniqueGroups = {"group1", "group2"}, onUniqueConflicts = {ConflictAction.FAIL, ConflictAction.IGNORE}) + * public String member2; + * + * @Column(name = "member3", uniqueGroups = {"group2"}, onUniqueConflicts = {ConflictAction.IGNORE}) + * public String member3; + * } + * + * CREATE TABLE table_name (..., UNIQUE (member1, member2) ON CONFLICT FAIL, UNIQUE (member2, member3) ON CONFLICT IGNORE) + */ + public String[] uniqueGroups() default {}; + + public ConflictAction[] onUniqueConflicts() default {}; + + /* + * If set index = true, we will create a index with single column. + * + * Example: + * + * @Table(name = "table_name") + * public class Table extends Model { + * @Column(name = "member", index = true) + * public String member; + * } + * + * Execute CREATE INDEX index_table_name_member on table_name(member) + */ + public boolean index() default false; + + /* + * If set indexGroups = {"group_name"}, we will create a index with group. + * + * Example: + * + * @Table(name = "table_name") + * public class Table extends Model { + * @Column(name = "member1", indexGroups = {"group1"}) + * public String member1; + * + * @Column(name = "member2", indexGroups = {"group1", "group2"}) + * public String member2; + * + * @Column(name = "member3", indexGroups = {"group2"}) + * public String member3; + * } + * + * Execute CREATE INDEX index_table_name_group1 on table_name(member1, member2) + * Execute CREATE INDEX index_table_name_group2 on table_name(member2, member3) + */ + public String[] indexGroups() default {}; +} diff --git a/app/src/main/java/com/activeandroid/annotation/Table.java b/app/src/main/java/com/activeandroid/annotation/Table.java new file mode 100644 index 000000000..541dfbe92 --- /dev/null +++ b/app/src/main/java/com/activeandroid/annotation/Table.java @@ -0,0 +1,31 @@ +package com.activeandroid.annotation; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Table { + + public static final String DEFAULT_ID_NAME = "Id"; + public String name(); + public String id() default DEFAULT_ID_NAME; +} diff --git a/app/src/main/java/com/activeandroid/query/Delete.java b/app/src/main/java/com/activeandroid/query/Delete.java new file mode 100644 index 000000000..6d19dcedc --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Delete.java @@ -0,0 +1,33 @@ +package com.activeandroid.query; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import com.activeandroid.Model; + +public final class Delete implements Sqlable { + public Delete() { + } + + public From from(Class table) { + return new From(table, this); + } + + @Override + public String toSql() { + return "DELETE "; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/query/From.java b/app/src/main/java/com/activeandroid/query/From.java new file mode 100644 index 000000000..825e36db7 --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/From.java @@ -0,0 +1,344 @@ +package com.activeandroid.query; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import android.text.TextUtils; + +import com.activeandroid.Cache; +import com.activeandroid.Model; +import com.activeandroid.query.Join.JoinType; +import com.activeandroid.util.Log; +import com.activeandroid.util.SQLiteUtils; + +import java.util.ArrayList; +import java.util.List; + +public final class From implements Sqlable { + private Sqlable mQueryBase; + + private Class mType; + private String mAlias; + private List mJoins; + private final StringBuilder mWhere = new StringBuilder(); + private String mGroupBy; + private String mHaving; + private String mOrderBy; + private String mLimit; + private String mOffset; + + private List mArguments; + + public From(Class table, Sqlable queryBase) { + mType = table; + mJoins = new ArrayList(); + mQueryBase = queryBase; + + mJoins = new ArrayList(); + mArguments = new ArrayList(); + } + + public From as(String alias) { + mAlias = alias; + return this; + } + + public Join join(Class table) { + Join join = new Join(this, table, null); + mJoins.add(join); + return join; + } + + public Join leftJoin(Class table) { + Join join = new Join(this, table, JoinType.LEFT); + mJoins.add(join); + return join; + } + + public Join outerJoin(Class table) { + Join join = new Join(this, table, JoinType.OUTER); + mJoins.add(join); + return join; + } + + public Join innerJoin(Class table) { + Join join = new Join(this, table, JoinType.INNER); + mJoins.add(join); + return join; + } + + public Join crossJoin(Class table) { + Join join = new Join(this, table, JoinType.CROSS); + mJoins.add(join); + return join; + } + + public From where(String clause) { + // Chain conditions if a previous condition exists. + if (mWhere.length() > 0) { + mWhere.append(" AND "); + } + mWhere.append(clause); + return this; + } + + public From where(String clause, Object... args) { + where(clause).addArguments(args); + return this; + } + + public From and(String clause) { + return where(clause); + } + + public From and(String clause, Object... args) { + return where(clause, args); + } + + public From or(String clause) { + if (mWhere.length() > 0) { + mWhere.append(" OR "); + } + mWhere.append(clause); + return this; + } + + public From or(String clause, Object... args) { + or(clause).addArguments(args); + return this; + } + + public From groupBy(String groupBy) { + mGroupBy = groupBy; + return this; + } + + public From having(String having) { + mHaving = having; + return this; + } + + public From orderBy(String orderBy) { + mOrderBy = orderBy; + return this; + } + + public From limit(int limit) { + return limit(String.valueOf(limit)); + } + + public From limit(String limit) { + mLimit = limit; + return this; + } + + public From offset(int offset) { + return offset(String.valueOf(offset)); + } + + public From offset(String offset) { + mOffset = offset; + return this; + } + + void addArguments(Object[] args) { + for(Object arg : args) { + if (arg.getClass() == boolean.class || arg.getClass() == Boolean.class) { + arg = (arg.equals(true) ? 1 : 0); + } + mArguments.add(arg); + } + } + + private void addFrom(final StringBuilder sql) { + sql.append("FROM "); + sql.append(Cache.getTableName(mType)).append(" "); + + if (mAlias != null) { + sql.append("AS "); + sql.append(mAlias); + sql.append(" "); + } + } + + private void addJoins(final StringBuilder sql) { + for (final Join join : mJoins) { + sql.append(join.toSql()); + } + } + + private void addWhere(final StringBuilder sql) { + if (mWhere.length() > 0) { + sql.append("WHERE "); + sql.append(mWhere); + sql.append(" "); + } + } + + private void addGroupBy(final StringBuilder sql) { + if (mGroupBy != null) { + sql.append("GROUP BY "); + sql.append(mGroupBy); + sql.append(" "); + } + } + + private void addHaving(final StringBuilder sql) { + if (mHaving != null) { + sql.append("HAVING "); + sql.append(mHaving); + sql.append(" "); + } + } + + private void addOrderBy(final StringBuilder sql) { + if (mOrderBy != null) { + sql.append("ORDER BY "); + sql.append(mOrderBy); + sql.append(" "); + } + } + + private void addLimit(final StringBuilder sql) { + if (mLimit != null) { + sql.append("LIMIT "); + sql.append(mLimit); + sql.append(" "); + } + } + + private void addOffset(final StringBuilder sql) { + if (mOffset != null) { + sql.append("OFFSET "); + sql.append(mOffset); + sql.append(" "); + } + } + + private String sqlString(final StringBuilder sql) { + + final String sqlString = sql.toString().trim(); + + // Don't waste time building the string + // unless we're going to log it. + if (Log.isEnabled()) { + Log.v(sqlString + " " + TextUtils.join(",", getArguments())); + } + + return sqlString; + } + + @Override + public String toSql() { + final StringBuilder sql = new StringBuilder(); + sql.append(mQueryBase.toSql()); + + addFrom(sql); + addJoins(sql); + addWhere(sql); + addGroupBy(sql); + addHaving(sql); + addOrderBy(sql); + addLimit(sql); + addOffset(sql); + + return sqlString(sql); + } + + public String toExistsSql() { + + final StringBuilder sql = new StringBuilder(); + sql.append("SELECT EXISTS(SELECT 1 "); + + addFrom(sql); + addJoins(sql); + addWhere(sql); + addGroupBy(sql); + addHaving(sql); + addLimit(sql); + addOffset(sql); + + sql.append(")"); + + return sqlString(sql); + } + + public String toCountSql() { + + final StringBuilder sql = new StringBuilder(); + sql.append("SELECT COUNT(*) "); + + addFrom(sql); + addJoins(sql); + addWhere(sql); + addGroupBy(sql); + addHaving(sql); + addLimit(sql); + addOffset(sql); + + return sqlString(sql); + } + + public List execute() { + if (mQueryBase instanceof Select) { + return SQLiteUtils.rawQuery(mType, toSql(), getArguments()); + + } else { + SQLiteUtils.execSql(toSql(), getArguments()); + return null; + + } + } + + public T executeSingle() { + if (mQueryBase instanceof Select) { + limit(1); + return (T) SQLiteUtils.rawQuerySingle(mType, toSql(), getArguments()); + + } else { + limit(1); + SQLiteUtils.rawQuerySingle(mType, toSql(), getArguments()).delete(); + return null; + + } + } + + /** + * Gets a value indicating whether the query returns any rows. + * @return true if the query returns at least one row; otherwise, false. + */ + public boolean exists() { + return SQLiteUtils.intQuery(toExistsSql(), getArguments()) != 0; + } + + /** + * Gets the number of rows returned by the query. + */ + public int count() { + return SQLiteUtils.intQuery(toCountSql(), getArguments()); + } + + public String[] getArguments() { + final int size = mArguments.size(); + final String[] args = new String[size]; + + for (int i = 0; i < size; i++) { + args[i] = mArguments.get(i).toString(); + } + + return args; + } +} diff --git a/app/src/main/java/com/activeandroid/query/Join.java b/app/src/main/java/com/activeandroid/query/Join.java new file mode 100644 index 000000000..13cdba3be --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Join.java @@ -0,0 +1,94 @@ +package com.activeandroid.query; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import android.text.TextUtils; + +import com.activeandroid.Cache; +import com.activeandroid.Model; + +public final class Join implements Sqlable { + static enum JoinType { + LEFT, OUTER, INNER, CROSS + } + + private From mFrom; + private Class mType; + private String mAlias; + private JoinType mJoinType; + private String mOn; + private String[] mUsing; + + Join(From from, Class table, JoinType joinType) { + mFrom = from; + mType = table; + mJoinType = joinType; + } + + public Join as(String alias) { + mAlias = alias; + return this; + } + + public From on(String on) { + mOn = on; + return mFrom; + } + + public From on(String on, Object... args) { + mOn = on; + mFrom.addArguments(args); + return mFrom; + } + + public From using(String... columns) { + mUsing = columns; + return mFrom; + } + + @Override + public String toSql() { + StringBuilder sql = new StringBuilder(); + + if (mJoinType != null) { + sql.append(mJoinType.toString()).append(" "); + } + + sql.append("JOIN "); + sql.append(Cache.getTableName(mType)); + sql.append(" "); + + if (mAlias != null) { + sql.append("AS "); + sql.append(mAlias); + sql.append(" "); + } + + if (mOn != null) { + sql.append("ON "); + sql.append(mOn); + sql.append(" "); + } + else if (mUsing != null) { + sql.append("USING ("); + sql.append(TextUtils.join(", ", mUsing)); + sql.append(") "); + } + + return sql.toString(); + } +} diff --git a/app/src/main/java/com/activeandroid/query/Select.java b/app/src/main/java/com/activeandroid/query/Select.java new file mode 100644 index 000000000..1d4c64885 --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Select.java @@ -0,0 +1,93 @@ +package com.activeandroid.query; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import android.text.TextUtils; + +import com.activeandroid.Model; + +public final class Select implements Sqlable { + private String[] mColumns; + private boolean mDistinct = false; + private boolean mAll = false; + + public Select() { + } + + public Select(String... columns) { + mColumns = columns; + } + + public Select(Column... columns) { + final int size = columns.length; + mColumns = new String[size]; + for (int i = 0; i < size; i++) { + mColumns[i] = columns[i].name + " AS " + columns[i].alias; + } + } + + public Select distinct() { + mDistinct = true; + mAll = false; + + return this; + } + + public Select all() { + mDistinct = false; + mAll = true; + + return this; + } + + public From from(Class table) { + return new From(table, this); + } + + public static class Column { + String name; + String alias; + + public Column(String name, String alias) { + this.name = name; + this.alias = alias; + } + } + + @Override + public String toSql() { + StringBuilder sql = new StringBuilder(); + + sql.append("SELECT "); + + if (mDistinct) { + sql.append("DISTINCT "); + } + else if (mAll) { + sql.append("ALL "); + } + + if (mColumns != null && mColumns.length > 0) { + sql.append(TextUtils.join(", ", mColumns) + " "); + } + else { + sql.append("* "); + } + + return sql.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/query/Set.java b/app/src/main/java/com/activeandroid/query/Set.java new file mode 100644 index 000000000..183d99f0f --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Set.java @@ -0,0 +1,103 @@ +package com.activeandroid.query; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import com.activeandroid.util.SQLiteUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public final class Set implements Sqlable { + private Update mUpdate; + + private String mSet; + private String mWhere; + + private List mSetArguments; + private List mWhereArguments; + + public Set(Update queryBase, String set) { + mUpdate = queryBase; + mSet = set; + + mSetArguments = new ArrayList(); + mWhereArguments = new ArrayList(); + } + + public Set(Update queryBase, String set, Object... args) { + mUpdate = queryBase; + mSet = set; + + mSetArguments = new ArrayList(); + mWhereArguments = new ArrayList(); + + mSetArguments.addAll(Arrays.asList(args)); + } + + public Set where(String where) { + mWhere = where; + mWhereArguments.clear(); + + return this; + } + + public Set where(String where, Object... args) { + mWhere = where; + mWhereArguments.clear(); + mWhereArguments.addAll(Arrays.asList(args)); + + return this; + } + + @Override + public String toSql() { + StringBuilder sql = new StringBuilder(); + sql.append(mUpdate.toSql()); + sql.append("SET "); + sql.append(mSet); + sql.append(" "); + + if (mWhere != null) { + sql.append("WHERE "); + sql.append(mWhere); + sql.append(" "); + } + + return sql.toString(); + } + + public void execute() { + SQLiteUtils.execSql(toSql(), getArguments()); + } + + public String[] getArguments() { + final int setSize = mSetArguments.size(); + final int whereSize = mWhereArguments.size(); + final String[] args = new String[setSize + whereSize]; + + for (int i = 0; i < setSize; i++) { + args[i] = mSetArguments.get(i).toString(); + } + + for (int i = 0; i < whereSize; i++) { + args[i + setSize] = mWhereArguments.get(i).toString(); + } + + return args; + } +} diff --git a/app/src/main/java/com/activeandroid/query/Sqlable.java b/app/src/main/java/com/activeandroid/query/Sqlable.java new file mode 100644 index 000000000..2c3f5d437 --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Sqlable.java @@ -0,0 +1,21 @@ +package com.activeandroid.query; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +public interface Sqlable { + public String toSql(); +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/query/Update.java b/app/src/main/java/com/activeandroid/query/Update.java new file mode 100644 index 000000000..a69d2d8e0 --- /dev/null +++ b/app/src/main/java/com/activeandroid/query/Update.java @@ -0,0 +1,50 @@ +package com.activeandroid.query; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import com.activeandroid.Cache; +import com.activeandroid.Model; + +public final class Update implements Sqlable { + private Class mType; + + public Update(Class table) { + mType = table; + } + + public Set set(String set) { + return new Set(this, set); + } + + public Set set(String set, Object... args) { + return new Set(this, set, args); + } + + Class getType() { + return mType; + } + + @Override + public String toSql() { + StringBuilder sql = new StringBuilder(); + sql.append("UPDATE "); + sql.append(Cache.getTableName(mType)); + sql.append(" "); + + return sql.toString(); + } +} diff --git a/app/src/main/java/com/activeandroid/serializer/BigDecimalSerializer.java b/app/src/main/java/com/activeandroid/serializer/BigDecimalSerializer.java new file mode 100644 index 000000000..333f900f0 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/BigDecimalSerializer.java @@ -0,0 +1,29 @@ +package com.activeandroid.serializer; + +import java.math.BigDecimal; + +public final class BigDecimalSerializer extends TypeSerializer { + public Class getDeserializedType() { + return BigDecimal.class; + } + + public Class getSerializedType() { + return String.class; + } + + public String serialize(Object data) { + if (data == null) { + return null; + } + + return ((BigDecimal) data).toString(); + } + + public BigDecimal deserialize(Object data) { + if (data == null) { + return null; + } + + return new BigDecimal((String) data); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/CalendarSerializer.java b/app/src/main/java/com/activeandroid/serializer/CalendarSerializer.java new file mode 100644 index 000000000..55509bd08 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/CalendarSerializer.java @@ -0,0 +1,40 @@ +package com.activeandroid.serializer; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.util.Calendar; + +public final class CalendarSerializer extends TypeSerializer { + public Class getDeserializedType() { + return Calendar.class; + } + + public Class getSerializedType() { + return long.class; + } + + public Long serialize(Object data) { + return ((Calendar) data).getTimeInMillis(); + } + + public Calendar deserialize(Object data) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis((Long) data); + + return calendar; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/FileSerializer.java b/app/src/main/java/com/activeandroid/serializer/FileSerializer.java new file mode 100644 index 000000000..0aed072c0 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/FileSerializer.java @@ -0,0 +1,46 @@ +package com.activeandroid.serializer; + +import java.io.File; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + + +public final class FileSerializer extends TypeSerializer { + public Class getDeserializedType() { + return File.class; + } + + public Class getSerializedType() { + return String.class; + } + + public String serialize(Object data) { + if (data == null) { + return null; + } + + return ((File) data).toString(); + } + + public File deserialize(Object data) { + if (data == null) { + return null; + } + + return new File((String) data); + } +} diff --git a/app/src/main/java/com/activeandroid/serializer/SqlDateSerializer.java b/app/src/main/java/com/activeandroid/serializer/SqlDateSerializer.java new file mode 100644 index 000000000..530d1249b --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/SqlDateSerializer.java @@ -0,0 +1,45 @@ +package com.activeandroid.serializer; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.sql.Date; + +public final class SqlDateSerializer extends TypeSerializer { + public Class getDeserializedType() { + return Date.class; + } + + public Class getSerializedType() { + return long.class; + } + + public Long serialize(Object data) { + if (data == null) { + return null; + } + + return ((Date) data).getTime(); + } + + public Date deserialize(Object data) { + if (data == null) { + return null; + } + + return new Date((Long) data); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/TypeSerializer.java b/app/src/main/java/com/activeandroid/serializer/TypeSerializer.java new file mode 100644 index 000000000..af0a21ded --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/TypeSerializer.java @@ -0,0 +1,27 @@ +package com.activeandroid.serializer; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +public abstract class TypeSerializer { + public abstract Class getDeserializedType(); + + public abstract Class getSerializedType(); + + public abstract Object serialize(Object data); + + public abstract Object deserialize(Object data); +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/UUIDSerializer.java b/app/src/main/java/com/activeandroid/serializer/UUIDSerializer.java new file mode 100644 index 000000000..94ba37ff3 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/UUIDSerializer.java @@ -0,0 +1,29 @@ +package com.activeandroid.serializer; + +import java.util.UUID; + +public final class UUIDSerializer extends TypeSerializer { + public Class getDeserializedType() { + return UUID.class; + } + + public Class getSerializedType() { + return String.class; + } + + public String serialize(Object data) { + if (data == null) { + return null; + } + + return ((UUID) data).toString(); + } + + public UUID deserialize(Object data) { + if (data == null) { + return null; + } + + return UUID.fromString((String)data); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/serializer/UtilDateSerializer.java b/app/src/main/java/com/activeandroid/serializer/UtilDateSerializer.java new file mode 100644 index 000000000..a82c7ef15 --- /dev/null +++ b/app/src/main/java/com/activeandroid/serializer/UtilDateSerializer.java @@ -0,0 +1,45 @@ +package com.activeandroid.serializer; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.util.Date; + +public final class UtilDateSerializer extends TypeSerializer { + public Class getDeserializedType() { + return Date.class; + } + + public Class getSerializedType() { + return long.class; + } + + public Long serialize(Object data) { + if (data == null) { + return null; + } + + return ((Date) data).getTime(); + } + + public Date deserialize(Object data) { + if (data == null) { + return null; + } + + return new Date((Long) data); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/util/IOUtils.java b/app/src/main/java/com/activeandroid/util/IOUtils.java new file mode 100644 index 000000000..b3005f857 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/IOUtils.java @@ -0,0 +1,71 @@ + +package com.activeandroid.util; + +/* + * Copyright (C) 2014 Markus Pfeiffer + * + * 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. + */ + +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}. + *

+ * 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}. + *

+ * 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); + } + } +} diff --git a/app/src/main/java/com/activeandroid/util/Log.java b/app/src/main/java/com/activeandroid/util/Log.java new file mode 100644 index 000000000..1c2a384d5 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/Log.java @@ -0,0 +1,196 @@ +package com.activeandroid.util; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +public final class Log { + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC MEMBERS + ////////////////////////////////////////////////////////////////////////////////////// + + private static String sTag = "ActiveAndroid"; + private static boolean sEnabled = false; + + ////////////////////////////////////////////////////////////////////////////////////// + // CONSTRUCTORS + ////////////////////////////////////////////////////////////////////////////////////// + + private Log() { + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public static boolean isEnabled() { + return sEnabled; + } + + public static void setEnabled(boolean enabled) { + sEnabled = enabled; + } + + public static boolean isLoggingEnabled() { + return sEnabled; + } + + public static int v(String msg) { + if (sEnabled) { + return android.util.Log.v(sTag, msg); + } + return 0; + } + + public static int v(String tag, String msg) { + if (sEnabled) { + return android.util.Log.v(tag, msg); + } + return 0; + } + + public static int v(String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.v(sTag, msg, tr); + } + return 0; + } + + public static int v(String tag, String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.v(tag, msg, tr); + } + return 0; + } + + public static int d(String msg) { + if (sEnabled) { + return android.util.Log.d(sTag, msg); + } + return 0; + } + + public static int d(String tag, String msg) { + if (sEnabled) { + return android.util.Log.d(tag, msg); + } + return 0; + } + + public static int d(String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.d(sTag, msg, tr); + } + return 0; + } + + public static int d(String tag, String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.d(tag, msg, tr); + } + return 0; + } + + public static int i(String msg) { + if (sEnabled) { + return android.util.Log.i(sTag, msg); + } + return 0; + } + + public static int i(String tag, String msg) { + if (sEnabled) { + return android.util.Log.i(tag, msg); + } + return 0; + } + + public static int i(String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.i(sTag, msg, tr); + } + return 0; + } + + public static int i(String tag, String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.i(tag, msg, tr); + } + return 0; + } + + public static int w(String msg) { + if (sEnabled) { + return android.util.Log.w(sTag, msg); + } + return 0; + } + + public static int w(String tag, String msg) { + if (sEnabled) { + return android.util.Log.w(tag, msg); + } + return 0; + } + + public static int w(String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.w(sTag, msg, tr); + } + return 0; + } + + public static int w(String tag, String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.w(tag, msg, tr); + } + return 0; + } + + public static int e(String msg) { + if (sEnabled) { + return android.util.Log.e(sTag, msg); + } + return 0; + } + + public static int e(String tag, String msg) { + if (sEnabled) { + return android.util.Log.e(tag, msg); + } + return 0; + } + + public static int e(String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.e(sTag, msg, tr); + } + return 0; + } + + public static int e(String tag, String msg, Throwable tr) { + if (sEnabled) { + return android.util.Log.e(tag, msg, tr); + } + return 0; + } + + public static int t(String msg, Object... args) { + if (sEnabled) { + return android.util.Log.v("test", String.format(msg, args)); + } + return 0; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/util/NaturalOrderComparator.java b/app/src/main/java/com/activeandroid/util/NaturalOrderComparator.java new file mode 100644 index 000000000..b09de26fa --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/NaturalOrderComparator.java @@ -0,0 +1,141 @@ +package com.activeandroid.util; + +/* + NaturalOrderComparator.java -- Perform 'natural order' comparisons of strings in Java. + Copyright (C) 2003 by Pierre-Luc Paour + + Based on the C version by Martin Pool, of which this is more or less a straight conversion. + Copyright (C) 2000 by Martin Pool + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + */ + +import java.util.Comparator; + +public class NaturalOrderComparator implements Comparator { + int compareRight(String a, String b) { + int bias = 0; + int ia = 0; + int ib = 0; + + // The longest run of digits wins. That aside, the greatest + // value wins, but we can't know that it will until we've scanned + // both numbers to know that they have the same magnitude, so we + // remember it in BIAS. + for (;; ia++, ib++) { + char ca = charAt(a, ia); + char cb = charAt(b, ib); + + if (!Character.isDigit(ca) && !Character.isDigit(cb)) { + return bias; + } + else if (!Character.isDigit(ca)) { + return -1; + } + else if (!Character.isDigit(cb)) { + return +1; + } + else if (ca < cb) { + if (bias == 0) { + bias = -1; + } + } + else if (ca > cb) { + if (bias == 0) + bias = +1; + } + else if (ca == 0 && cb == 0) { + return bias; + } + } + } + + public int compare(Object o1, Object o2) { + String a = o1.toString(); + String b = o2.toString(); + + int ia = 0, ib = 0; + int nza = 0, nzb = 0; + char ca, cb; + int result; + + while (true) { + // only count the number of zeroes leading the last number compared + nza = nzb = 0; + + ca = charAt(a, ia); + cb = charAt(b, ib); + + // skip over leading spaces or zeros + while (Character.isSpaceChar(ca) || ca == '0') { + if (ca == '0') { + nza++; + } + else { + // only count consecutive zeroes + nza = 0; + } + + ca = charAt(a, ++ia); + } + + while (Character.isSpaceChar(cb) || cb == '0') { + if (cb == '0') { + nzb++; + } + else { + // only count consecutive zeroes + nzb = 0; + } + + cb = charAt(b, ++ib); + } + + // process run of digits + if (Character.isDigit(ca) && Character.isDigit(cb)) { + if ((result = compareRight(a.substring(ia), b.substring(ib))) != 0) { + return result; + } + } + + if (ca == 0 && cb == 0) { + // The strings compare the same. Perhaps the caller + // will want to call strcmp to break the tie. + return nza - nzb; + } + + if (ca < cb) { + return -1; + } + else if (ca > cb) { + return +1; + } + + ++ia; + ++ib; + } + } + + static char charAt(String s, int i) { + if (i >= s.length()) { + return 0; + } + else { + return s.charAt(i); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/util/ReflectionUtils.java b/app/src/main/java/com/activeandroid/util/ReflectionUtils.java new file mode 100644 index 000000000..32e995cc3 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/ReflectionUtils.java @@ -0,0 +1,110 @@ +package com.activeandroid.util; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.Set; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.serializer.TypeSerializer; + +public final class ReflectionUtils { + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public static boolean isModel(Class type) { + return isSubclassOf(type, Model.class) && (!Modifier.isAbstract(type.getModifiers())); + } + + public static boolean isTypeSerializer(Class type) { + return isSubclassOf(type, TypeSerializer.class); + } + + // Meta-data + + @SuppressWarnings("unchecked") + public static T getMetaData(Context context, String name) { + try { + final ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), + PackageManager.GET_META_DATA); + + if (ai.metaData != null) { + return (T) ai.metaData.get(name); + } + } + catch (Exception e) { + Log.w("Couldn't find meta-data: " + name); + } + + return null; + } + + public static Set getDeclaredColumnFields(Class type) { + Set declaredColumnFields = Collections.emptySet(); + + if (ReflectionUtils.isSubclassOf(type, Model.class) || Model.class.equals(type)) { + declaredColumnFields = new LinkedHashSet(); + + Field[] fields = type.getDeclaredFields(); + Arrays.sort(fields, new Comparator() { + @Override + public int compare(Field field1, Field field2) { + return field2.getName().compareTo(field1.getName()); + } + }); + for (Field field : fields) { + if (field.isAnnotationPresent(Column.class)) { + declaredColumnFields.add(field); + } + } + + Class parentType = type.getSuperclass(); + if (parentType != null) { + declaredColumnFields.addAll(getDeclaredColumnFields(parentType)); + } + } + + return declaredColumnFields; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public static boolean isSubclassOf(Class type, Class superClass) { + if (type.getSuperclass() != null) { + if (type.getSuperclass().equals(superClass)) { + return true; + } + + return isSubclassOf(type.getSuperclass(), superClass); + } + + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/activeandroid/util/SQLiteUtils.java b/app/src/main/java/com/activeandroid/util/SQLiteUtils.java new file mode 100644 index 000000000..cbf41eaee --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/SQLiteUtils.java @@ -0,0 +1,406 @@ +package com.activeandroid.util; + +/* + * Copyright (C) 2010 Michael Pardo + * + * 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. + */ + +import android.database.Cursor; +import android.os.Build; +import android.text.TextUtils; + +import com.activeandroid.Cache; +import com.activeandroid.Model; +import com.activeandroid.TableInfo; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Column.ConflictAction; +import com.activeandroid.serializer.TypeSerializer; + +import java.lang.Long; +import java.lang.String; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class SQLiteUtils { + ////////////////////////////////////////////////////////////////////////////////////// + // ENUMERATIONS + ////////////////////////////////////////////////////////////////////////////////////// + + public enum SQLiteType { + INTEGER, REAL, TEXT, BLOB + } + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC CONSTANTS + ////////////////////////////////////////////////////////////////////////////////////// + + public static final boolean FOREIGN_KEYS_SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; + + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE CONTSANTS + ////////////////////////////////////////////////////////////////////////////////////// + + @SuppressWarnings("serial") + private static final HashMap, SQLiteType> TYPE_MAP = new HashMap, SQLiteType>() { + { + put(byte.class, SQLiteType.INTEGER); + put(short.class, SQLiteType.INTEGER); + put(int.class, SQLiteType.INTEGER); + put(long.class, SQLiteType.INTEGER); + put(float.class, SQLiteType.REAL); + put(double.class, SQLiteType.REAL); + put(boolean.class, SQLiteType.INTEGER); + put(char.class, SQLiteType.TEXT); + put(byte[].class, SQLiteType.BLOB); + put(Byte.class, SQLiteType.INTEGER); + put(Short.class, SQLiteType.INTEGER); + put(Integer.class, SQLiteType.INTEGER); + put(Long.class, SQLiteType.INTEGER); + put(Float.class, SQLiteType.REAL); + put(Double.class, SQLiteType.REAL); + put(Boolean.class, SQLiteType.INTEGER); + put(Character.class, SQLiteType.TEXT); + put(String.class, SQLiteType.TEXT); + put(Byte[].class, SQLiteType.BLOB); + } + }; + + ////////////////////////////////////////////////////////////////////////////////////// + // PRIVATE MEMBERS + ////////////////////////////////////////////////////////////////////////////////////// + + private static HashMap> sIndexGroupMap; + private static HashMap> sUniqueGroupMap; + private static HashMap sOnUniqueConflictsMap; + + ////////////////////////////////////////////////////////////////////////////////////// + // PUBLIC METHODS + ////////////////////////////////////////////////////////////////////////////////////// + + public static void execSql(String sql) { + Cache.openDatabase().execSQL(sql); + } + + public static void execSql(String sql, Object[] bindArgs) { + Cache.openDatabase().execSQL(sql, bindArgs); + } + + public static List rawQuery(Class type, String sql, String[] selectionArgs) { + Cursor cursor = Cache.openDatabase().rawQuery(sql, selectionArgs); + List entities = processCursor(type, cursor); + cursor.close(); + + return entities; + } + + public static int intQuery(final String sql, final String[] selectionArgs) { + final Cursor cursor = Cache.openDatabase().rawQuery(sql, selectionArgs); + final int number = processIntCursor(cursor); + cursor.close(); + + return number; + } + + public static T rawQuerySingle(Class type, String sql, String[] selectionArgs) { + List entities = rawQuery(type, sql, selectionArgs); + + if (entities.size() > 0) { + return entities.get(0); + } + + return null; + } + + // Database creation + + public static ArrayList createUniqueDefinition(TableInfo tableInfo) { + final ArrayList definitions = new ArrayList(); + sUniqueGroupMap = new HashMap>(); + sOnUniqueConflictsMap = new HashMap(); + + for (Field field : tableInfo.getFields()) { + createUniqueColumnDefinition(tableInfo, field); + } + + if (sUniqueGroupMap.isEmpty()) { + return definitions; + } + + Set keySet = sUniqueGroupMap.keySet(); + for (String key : keySet) { + List group = sUniqueGroupMap.get(key); + ConflictAction conflictAction = sOnUniqueConflictsMap.get(key); + + definitions.add(String.format("UNIQUE (%s) ON CONFLICT %s", + TextUtils.join(", ", group), conflictAction.toString())); + } + + return definitions; + } + + public static void createUniqueColumnDefinition(TableInfo tableInfo, Field field) { + final String name = tableInfo.getColumnName(field); + final Column column = field.getAnnotation(Column.class); + + if (field.getName().equals("mId")) { + return; + } + + String[] groups = column.uniqueGroups(); + ConflictAction[] conflictActions = column.onUniqueConflicts(); + if (groups.length != conflictActions.length) + return; + + for (int i = 0; i < groups.length; i++) { + String group = groups[i]; + ConflictAction conflictAction = conflictActions[i]; + + if (TextUtils.isEmpty(group)) + continue; + + List list = sUniqueGroupMap.get(group); + if (list == null) { + list = new ArrayList(); + } + list.add(name); + + sUniqueGroupMap.put(group, list); + sOnUniqueConflictsMap.put(group, conflictAction); + } + } + + public static String[] createIndexDefinition(TableInfo tableInfo) { + final ArrayList definitions = new ArrayList(); + sIndexGroupMap = new HashMap>(); + + for (Field field : tableInfo.getFields()) { + createIndexColumnDefinition(tableInfo, field); + } + + if (sIndexGroupMap.isEmpty()) { + return new String[0]; + } + + for (Map.Entry> entry : sIndexGroupMap.entrySet()) { + definitions.add(String.format("CREATE INDEX IF NOT EXISTS %s on %s(%s);", + "index_" + tableInfo.getTableName() + "_" + entry.getKey(), + tableInfo.getTableName(), TextUtils.join(", ", entry.getValue()))); + } + + return definitions.toArray(new String[definitions.size()]); + } + + public static void createIndexColumnDefinition(TableInfo tableInfo, Field field) { + final String name = tableInfo.getColumnName(field); + final Column column = field.getAnnotation(Column.class); + + if (field.getName().equals("mId")) { + return; + } + + if (column.index()) { + List list = new ArrayList(); + list.add(name); + sIndexGroupMap.put(name, list); + } + + String[] groups = column.indexGroups(); + for (String group : groups) { + if (TextUtils.isEmpty(group)) + continue; + + List list = sIndexGroupMap.get(group); + if (list == null) { + list = new ArrayList(); + } + + list.add(name); + sIndexGroupMap.put(group, list); + } + } + + public static String createTableDefinition(TableInfo tableInfo) { + final ArrayList definitions = new ArrayList(); + + for (Field field : tableInfo.getFields()) { + String definition = createColumnDefinition(tableInfo, field); + if (!TextUtils.isEmpty(definition)) { + definitions.add(definition); + } + } + + definitions.addAll(createUniqueDefinition(tableInfo)); + + return String.format("CREATE TABLE IF NOT EXISTS %s (%s);", tableInfo.getTableName(), + TextUtils.join(", ", definitions)); + } + + @SuppressWarnings("unchecked") + public static String createColumnDefinition(TableInfo tableInfo, Field field) { + StringBuilder definition = new StringBuilder(); + + Class type = field.getType(); + final String name = tableInfo.getColumnName(field); + final TypeSerializer typeSerializer = Cache.getParserForType(field.getType()); + final Column column = field.getAnnotation(Column.class); + + if (typeSerializer != null) { + type = typeSerializer.getSerializedType(); + } + + if (TYPE_MAP.containsKey(type)) { + definition.append(name); + definition.append(" "); + definition.append(TYPE_MAP.get(type).toString()); + } + else if (ReflectionUtils.isModel(type)) { + definition.append(name); + definition.append(" "); + definition.append(SQLiteType.INTEGER.toString()); + } + else if (ReflectionUtils.isSubclassOf(type, Enum.class)) { + definition.append(name); + definition.append(" "); + definition.append(SQLiteType.TEXT.toString()); + } + + if (!TextUtils.isEmpty(definition)) { + + if (name.equals(tableInfo.getIdName())) { + definition.append(" PRIMARY KEY AUTOINCREMENT"); + }else if(column!=null){ + if (column.length() > -1) { + definition.append("("); + definition.append(column.length()); + definition.append(")"); + } + + if (column.notNull()) { + definition.append(" NOT NULL ON CONFLICT "); + definition.append(column.onNullConflict().toString()); + } + + if (column.unique()) { + definition.append(" UNIQUE ON CONFLICT "); + definition.append(column.onUniqueConflict().toString()); + } + } + + if (FOREIGN_KEYS_SUPPORTED && ReflectionUtils.isModel(type)) { + definition.append(" REFERENCES "); + definition.append(Cache.getTableInfo((Class) type).getTableName()); + definition.append("("+tableInfo.getIdName()+")"); + definition.append(" ON DELETE "); + definition.append(column.onDelete().toString().replace("_", " ")); + definition.append(" ON UPDATE "); + definition.append(column.onUpdate().toString().replace("_", " ")); + } + } + else { + Log.e("No type mapping for: " + type.toString()); + } + + return definition.toString(); + } + + @SuppressWarnings("unchecked") + public static List processCursor(Class type, Cursor cursor) { + TableInfo tableInfo = Cache.getTableInfo(type); + String idName = tableInfo.getIdName(); + final List entities = new ArrayList(); + + try { + Constructor entityConstructor = type.getConstructor(); + + if (cursor.moveToFirst()) { + /** + * Obtain the columns ordered to fix issue #106 (https://github.com/pardom/ActiveAndroid/issues/106) + * when the cursor have multiple columns with same name obtained from join tables. + */ + List columnsOrdered = new ArrayList(Arrays.asList(cursor.getColumnNames())); + do { + Model entity = Cache.getEntity(type, cursor.getLong(columnsOrdered.indexOf(idName))); + if (entity == null) { + entity = (T) entityConstructor.newInstance(); + } + + entity.loadFromCursor(cursor); + entities.add((T) entity); + } + while (cursor.moveToNext()); + } + + } + catch (NoSuchMethodException e) { + throw new RuntimeException( + "Your model " + type.getName() + " does not define a default " + + "constructor. The default constructor is required for " + + "now in ActiveAndroid models, as the process to " + + "populate the ORM model is : " + + "1. instantiate default model " + + "2. populate fields" + ); + } + catch (Exception e) { + Log.e("Failed to process cursor.", e); + } + + return entities; + } + + private static int processIntCursor(final Cursor cursor) { + if (cursor.moveToFirst()) { + return cursor.getInt(0); + } + return 0; + } + + public static List lexSqlScript(String sqlScript) { + ArrayList sl = new ArrayList(); + boolean inString = false, quoteNext = false; + StringBuilder b = new StringBuilder(100); + + for (int i = 0; i < sqlScript.length(); i++) { + char c = sqlScript.charAt(i); + + if (c == ';' && !inString && !quoteNext) { + sl.add(b.toString()); + b = new StringBuilder(100); + inString = false; + quoteNext = false; + continue; + } + + if (c == '\'' && !quoteNext) { + inString = !inString; + } + + quoteNext = c == '\\' && !quoteNext; + + b.append(c); + } + + if (b.length() > 0) { + sl.add(b.toString()); + } + + return sl; + } +} diff --git a/app/src/main/java/com/activeandroid/util/SqlParser.java b/app/src/main/java/com/activeandroid/util/SqlParser.java new file mode 100644 index 000000000..f9531b7c8 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/SqlParser.java @@ -0,0 +1,110 @@ + +package com.activeandroid.util; + +/* + * Copyright (C) 2014 Markus Pfeiffer + * + * 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. + */ + +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 parse(final InputStream stream) throws IOException { + + final BufferedInputStream buffer = new BufferedInputStream(stream); + final List commands = new ArrayList(); + 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().trim(); + 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().trim()); + } + + 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 == ' '; + } +} diff --git a/app/src/main/java/com/activeandroid/util/Tokenizer.java b/app/src/main/java/com/activeandroid/util/Tokenizer.java new file mode 100644 index 000000000..8ae34da32 --- /dev/null +++ b/app/src/main/java/com/activeandroid/util/Tokenizer.java @@ -0,0 +1,76 @@ + +package com.activeandroid.util; + +/* + * Copyright (C) 2014 Markus Pfeiffer + * + * 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. + */ + +import java.io.IOException; +import java.io.InputStream; + + +public class Tokenizer { + + private final InputStream mStream; + + private boolean mIsNext; + private int mCurrent; + + public Tokenizer(final InputStream in) { + this.mStream = in; + } + + public boolean hasNext() throws IOException { + + if (!this.mIsNext) { + this.mIsNext = true; + this.mCurrent = this.mStream.read(); + } + return this.mCurrent != -1; + } + + public int next() throws IOException { + + if (!this.mIsNext) { + this.mCurrent = this.mStream.read(); + } + this.mIsNext = false; + return this.mCurrent; + } + + public boolean skip(final String s) throws IOException { + + if (s == null || s.length() == 0) { + return false; + } + + if (s.charAt(0) != this.mCurrent) { + return false; + } + + final int len = s.length(); + this.mStream.mark(len - 1); + + for (int n = 1; n < len; n++) { + final int value = this.mStream.read(); + + if (value != s.charAt(n)) { + this.mStream.reset(); + return false; + } + } + return true; + } +} diff --git a/app/src/main/java/com/activeandroid/widget/ModelAdapter.java b/app/src/main/java/com/activeandroid/widget/ModelAdapter.java new file mode 100644 index 000000000..a38957636 --- /dev/null +++ b/app/src/main/java/com/activeandroid/widget/ModelAdapter.java @@ -0,0 +1,57 @@ +package com.activeandroid.widget; + +import java.util.Collection; +import java.util.List; + +import android.content.Context; +import android.widget.ArrayAdapter; + +import com.activeandroid.Model; + +public class ModelAdapter extends ArrayAdapter { + public ModelAdapter(Context context, int textViewResourceId) { + super(context, textViewResourceId); + } + + public ModelAdapter(Context context, int resource, int textViewResourceId) { + super(context, resource, textViewResourceId); + } + + public ModelAdapter(Context context, int textViewResourceId, List objects) { + super(context, textViewResourceId, objects); + } + + public ModelAdapter(Context context, int resource, int textViewResourceId, List objects) { + super(context, resource, textViewResourceId, objects); + } + + /** + * Clears the adapter and, if data != null, fills if with new Items. + * + * @param collection A Collection<? extends T> which members get added to the adapter. + */ + public void setData(Collection collection) { + clear(); + + if (collection != null) { + for (T item : collection) { + add(item); + } + } + } + + /** + * @return The Id of the record at position. + */ + @Override + public long getItemId(int position) { + T item = getItem(position); + + if (item != null) { + return item.getId(); + } + else { + return -1; + } + } +}