diff --git a/.travis.yml b/.travis.yml index 447d4fd..e1c8002 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,13 +12,16 @@ env: android: components: - - build-tools-19.1.0 + - platform-tools + - tools + - build-tools-23.0.3 - android-15 - android-16 - android-17 - android-18 - android-19 - android-21 + - android-23 - sys-img-armeabi-v7a-android-15 - sys-img-armeabi-v7a-android-16 - sys-img-armeabi-v7a-android-17 diff --git a/ProviGenLib/build.gradle b/ProviGenLib/build.gradle index 34867f5..7b9651a 100644 --- a/ProviGenLib/build.gradle +++ b/ProviGenLib/build.gradle @@ -9,4 +9,9 @@ sourceSets { srcDir 'src' } } +} + +task generateSourcesJar(type: Jar) { + from sourceSets.main.java.srcDirs + classifier 'sources' } \ No newline at end of file diff --git a/ProviGenLib/src/com/tjeannin/provigen/ProviGenBaseContract.java b/ProviGenLib/src/com/tjeannin/provigen/ProviGenBaseContract.java index 267aca9..deef5ee 100644 --- a/ProviGenLib/src/com/tjeannin/provigen/ProviGenBaseContract.java +++ b/ProviGenLib/src/com/tjeannin/provigen/ProviGenBaseContract.java @@ -13,8 +13,8 @@ public interface ProviGenBaseContract { /** * The unique ID for a row. */ - @Id + @Id(autoincrement = true) @Column(Type.INTEGER) - public static final String _ID = BaseColumns._ID; + String _ID = BaseColumns._ID; } diff --git a/ProviGenLib/src/com/tjeannin/provigen/ProviGenOpenHelper.java b/ProviGenLib/src/com/tjeannin/provigen/ProviGenOpenHelper.java index 5214b6f..7e6db3f 100644 --- a/ProviGenLib/src/com/tjeannin/provigen/ProviGenOpenHelper.java +++ b/ProviGenLib/src/com/tjeannin/provigen/ProviGenOpenHelper.java @@ -42,9 +42,14 @@ public void onCreate(SQLiteDatabase database) { @Override public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { + // create new tables if not exists + onCreate(database); + + // add missing columns if (newVersion > oldVersion) { - for (Class contract : contracts) + for (Class contract : contracts) { TableUpdater.addMissingColumns(database, contract); + } } } } diff --git a/ProviGenLib/src/com/tjeannin/provigen/ProviGenProvider.java b/ProviGenLib/src/com/tjeannin/provigen/ProviGenProvider.java index 975019c..379dca8 100644 --- a/ProviGenLib/src/com/tjeannin/provigen/ProviGenProvider.java +++ b/ProviGenLib/src/com/tjeannin/provigen/ProviGenProvider.java @@ -4,27 +4,36 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; +import android.os.Build; import android.text.TextUtils; +import com.tjeannin.provigen.helper.ContractUtil; import com.tjeannin.provigen.model.Contract; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** * Behaves as a {@link ContentProvider} for the given contract class. */ public abstract class ProviGenProvider extends ContentProvider { - private List contracts = new ArrayList(); - - private UriMatcher uriMatcher; private static final int ITEM = 1; private static final int ITEM_ID = 2; - private SQLiteOpenHelper openHelper; + private static final int INNER_JOIN = 3; + private static final int LEFT_OUTER_JOIN = 4; + private static final int CROSS_JOIN = 5; + + private List contracts = new ArrayList<>(); + private UriMatcher uriMatcher; + private SQLiteOpenHelper[] openHelpers; /** * This method should return an instance of a {@link android.database.sqlite.SQLiteOpenHelper}. + * If you want to use multiple databases override {@link #openHelpers(Context)} and you may return null in this method. * It will be called only once, so you can safely create a new instance of the SQLiteOpenHelper on the method body. * * @param context A context to pass to the SQLiteOpenHelper instance while creating it. @@ -32,6 +41,15 @@ public abstract class ProviGenProvider extends ContentProvider { */ public abstract SQLiteOpenHelper openHelper(Context context); + /** + * Override if you want to use multiple databases. + * @param context A context to pass to the SQLiteOpenHelper instance while creating it. + * @return the SQLiteOpenHelper[] that the ProviGenProvider will use. + */ + public SQLiteOpenHelper [] openHelpers(Context context) { + return new SQLiteOpenHelper[] { openHelper(context) }; + } + /** * This method should return the list of contract classes that the ProviGenProvider will use. * It will be called only once. @@ -40,154 +58,302 @@ public abstract class ProviGenProvider extends ContentProvider { */ public abstract Class[] contractClasses(); + /** + * Override if you want to use multiple databases. + * This method should return the array of array contract classes that the ProviGenProvider will use. + * It will be called only once. + * + * @return an array of array of contract classes. + */ + public Class[][] contractClassesMultipleDb() { + return new Class[][] { contractClasses() }; + } + + /** + * Override for insert conflict resolver. By default return SQLiteDatabase.CONFLICT_NONE. + */ + public int conflictAlgorithm() { + return SQLiteDatabase.CONFLICT_NONE; + } + + /** + * Whether to call the getContentResolver().notifyChange(uri, null) method after insert. + * @return true or false + */ + public boolean insertNotifyChange() { + return true; + } + + /** + * Whether to call the getContentResolver().notifyChange(uri, null) method after update. + * @return true or false + */ + public boolean updateNotifyChange() { + return true; + } + + /** + * Whether to call the getContentResolver().notifyChange(uri, null) method after delete. + * @return true or false + */ + public boolean deleteNotifyChange() { + return true; + } + + /** + * Get max number of join entities (by default 10) + * @return Max number of join entities + */ + public int maxJoinEntities() { + return 10; + } + @Override public boolean onCreate() { + openHelpers = openHelpers(getContext()); - openHelper = openHelper(getContext()); - for (Class contract : contractClasses()) { - contracts.add(new Contract(contract)); + for(Class[] contractArray : contractClassesMultipleDb()) { + for (Class contract : contractArray) { + contracts.add(new Contract(contract)); + } } uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); for (Contract contract : contracts) { - uriMatcher.addURI(contract.getAuthority(), contract.getTable(), ITEM); - uriMatcher.addURI(contract.getAuthority(), contract.getTable() + "/#", ITEM_ID); + StringBuilder innerJoinPathSegment; + StringBuilder leftOuterJoinPathSegment; + StringBuilder crossJoinPathSegment; + + if(contract.getDbName() == null) { + uriMatcher.addURI(contract.getAuthority(), contract.getTable(), ITEM); + uriMatcher.addURI(contract.getAuthority(), contract.getTable() + "/#", ITEM_ID); + + innerJoinPathSegment = new StringBuilder(contract.getTable()).append("/inner_join/*/*"); + leftOuterJoinPathSegment = new StringBuilder(contract.getTable()).append("/left_outer_join/*/*"); + crossJoinPathSegment = new StringBuilder(contract.getTable()).append("/cross_join/*/*"); + } else { + uriMatcher.addURI(contract.getAuthority(), contract.getDbName() + "/" + contract.getTable(), ITEM); + uriMatcher.addURI(contract.getAuthority(), contract.getDbName() + "/" + contract.getTable() + "/#", ITEM_ID); + + innerJoinPathSegment = new StringBuilder(contract.getDbName()).append("/").append(contract.getTable()).append("/inner_join/*/*"); + leftOuterJoinPathSegment = new StringBuilder(contract.getDbName()).append("/").append(contract.getTable()).append("/left_outer_join/*/*"); + crossJoinPathSegment = new StringBuilder(contract.getDbName()).append("/").append(contract.getTable()).append("/cross_join/*/*"); + } + + for(int i = 0; i < maxJoinEntities(); i++) { + uriMatcher.addURI(contract.getAuthority(), innerJoinPathSegment.toString(), INNER_JOIN); + uriMatcher.addURI(contract.getAuthority(), leftOuterJoinPathSegment.toString(), LEFT_OUTER_JOIN); + uriMatcher.addURI(contract.getAuthority(), crossJoinPathSegment.toString(), CROSS_JOIN); + innerJoinPathSegment.append("/*/*"); + leftOuterJoinPathSegment.append("/*/*"); + crossJoinPathSegment.append("/*/*"); + } } return true; } @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - SQLiteDatabase database = openHelper.getWritableDatabase(); - - int numberOfRowsAffected = 0; + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); Contract contract = findMatchingContract(uri); + SQLiteDatabase db = openDatabase(contract, true); + Cursor cursor; switch (uriMatcher.match(uri)) { case ITEM: - numberOfRowsAffected = database.delete(contract.getTable(), selection, selectionArgs); + queryBuilder.setTables(contract.getTable()); + cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); break; + case ITEM_ID: String itemId = String.valueOf(ContentUris.parseId(uri)); - if (TextUtils.isEmpty(selection)) { - numberOfRowsAffected = database.delete(contract.getTable(), contract.getIdField() + " = ? ", new String[]{itemId}); + queryBuilder.setTables(contract.getTable()); + cursor = queryBuilder.query(db, projection, contract.getIdFields().get(0) + " = ? ", new String[]{itemId}, null, null, sortOrder); } else { - numberOfRowsAffected = database.delete(contract.getTable(), selection + " AND " + - contract.getIdField() + " = ? ", appendToStringArray(selectionArgs, itemId)); + queryBuilder.setTables(contract.getTable()); + cursor = queryBuilder.query(db, projection, selection + " AND " + contract.getIdFields().get(0) + " = ? ", appendToStringArray(selectionArgs, itemId), null, null, sortOrder); } break; + + // JOINs + case INNER_JOIN: + case LEFT_OUTER_JOIN: + case CROSS_JOIN: + Map joinProjectionMap = new LinkedHashMap<>(); + for(String field : projection) { + joinProjectionMap.put(field, field.contains(".") ? field + " AS " + ContractUtil.joinName(field.substring(0, field.indexOf(".")), field.substring(field.indexOf(".") + 1)) : field); + } + queryBuilder.setProjectionMap(joinProjectionMap); + queryBuilder.setTables(parseJoinUri(uri, contract.getDbName() != null)); + cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); + break; + default: throw new IllegalArgumentException("Unknown uri " + uri); } - if (numberOfRowsAffected > 0) { - getContext().getContentResolver().notifyChange(uri, null); - } - return numberOfRowsAffected; + // Make sure that potential listeners are getting notified. + cursor.setNotificationUri(getContext().getContentResolver(), uri); + + return cursor; } @Override - public String getType(Uri uri) { - + public Uri insert(Uri uri, ContentValues values) { Contract contract = findMatchingContract(uri); + SQLiteDatabase db = openDatabase(contract, false); switch (uriMatcher.match(uri)) { case ITEM: - return "vnd.android.cursor.dir/vdn." + contract.getTable(); - case ITEM_ID: - return "vnd.android.cursor.item/vdn." + contract.getTable(); + long rowId; + if(conflictAlgorithm() == SQLiteDatabase.CONFLICT_NONE) { + rowId = db.insert(contract.getTable(), null, values); + } else { + rowId = db.insertWithOnConflict(contract.getTable(), null, values, conflictAlgorithm()); + } + + Uri rowUri = Uri.EMPTY; + if (rowId > 0) { + rowUri = ContentUris.withAppendedId(uri, rowId); + } + if(insertNotifyChange()) { + getContext().getContentResolver().notifyChange(uri, null); + } + return rowUri; + default: throw new IllegalArgumentException("Unknown uri " + uri); } } @Override - public Uri insert(Uri uri, ContentValues values) { - SQLiteDatabase database = openHelper.getWritableDatabase(); - + public int bulkInsert(Uri uri, ContentValues[] values) { Contract contract = findMatchingContract(uri); + SQLiteDatabase db = openDatabase(contract, false); + switch (uriMatcher.match(uri)) { case ITEM: - long itemId = database.insert(contract.getTable(), null, values); - getContext().getContentResolver().notifyChange(uri, null); - return Uri.withAppendedPath(uri, String.valueOf(itemId)); + db.beginTransaction(); + try { + for (ContentValues cv : values) { + if(conflictAlgorithm() == SQLiteDatabase.CONFLICT_NONE) { + db.insert(contract.getTable(), null, cv); + } else { + db.insertWithOnConflict(contract.getTable(), null, cv, conflictAlgorithm()); + } + } + db.setTransactionSuccessful(); + + if(insertNotifyChange()) { + getContext().getContentResolver().notifyChange(uri, null); + } + } finally { + db.endTransaction(); + } + return values.length; + default: throw new IllegalArgumentException("Unknown uri " + uri); } } @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - SQLiteDatabase database = openHelper.getReadableDatabase(); - + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { Contract contract = findMatchingContract(uri); - Cursor cursor = null; + SQLiteDatabase db = openDatabase(contract, false); + int numberOfRowsAffected; switch (uriMatcher.match(uri)) { case ITEM: - cursor = database.query(contract.getTable(), projection, selection, selectionArgs, "", "", sortOrder); + numberOfRowsAffected = db.update(contract.getTable(), values, selection, selectionArgs); break; + case ITEM_ID: String itemId = String.valueOf(ContentUris.parseId(uri)); if (TextUtils.isEmpty(selection)) { - cursor = database.query(contract.getTable(), projection, contract.getIdField() + " = ? ", new String[]{itemId}, "", "", sortOrder); + numberOfRowsAffected = db.update(contract.getTable(), values, contract.getIdFields().get(0) + " = ? ", new String[]{itemId}); } else { - cursor = database.query(contract.getTable(), projection, selection + " AND " + contract.getIdField() + " = ? ", - appendToStringArray(selectionArgs, itemId), "", "", sortOrder); + numberOfRowsAffected = db.update(contract.getTable(), values, selection + " AND " + contract.getIdFields().get(0) + " = ? ", + appendToStringArray(selectionArgs, itemId)); } break; + default: throw new IllegalArgumentException("Unknown uri " + uri); } - - // Make sure that potential listeners are getting notified. - cursor.setNotificationUri(getContext().getContentResolver(), uri); - - return cursor; + if(updateNotifyChange()) { + getContext().getContentResolver().notifyChange(uri, null); + } + return numberOfRowsAffected; } @Override - public int update(Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - SQLiteDatabase database = openHelper.getWritableDatabase(); - + public int delete(Uri uri, String selection, String[] selectionArgs) { Contract contract = findMatchingContract(uri); - int numberOfRowsAffected = 0; + SQLiteDatabase db = openDatabase(contract, false); + int numberOfRowsAffected; switch (uriMatcher.match(uri)) { case ITEM: - numberOfRowsAffected = database.update(contract.getTable(), values, selection, selectionArgs); + case INNER_JOIN: + case LEFT_OUTER_JOIN: + case CROSS_JOIN: + numberOfRowsAffected = db.delete(contract.getTable(), selection, selectionArgs); break; + case ITEM_ID: String itemId = String.valueOf(ContentUris.parseId(uri)); - if (TextUtils.isEmpty(selection)) { - numberOfRowsAffected = database.update(contract.getTable(), values, contract.getIdField() + " = ? ", new String[]{itemId}); + numberOfRowsAffected = db.delete(contract.getTable(), contract.getIdFields().get(0) + " = ? ", new String[]{itemId}); } else { - numberOfRowsAffected = database.update(contract.getTable(), values, selection + " AND " + contract.getIdField() + " = ? ", - appendToStringArray(selectionArgs, itemId)); + numberOfRowsAffected = db.delete(contract.getTable(), selection + " AND " + + contract.getIdFields().get(0) + " = ? ", appendToStringArray(selectionArgs, itemId)); } break; + default: throw new IllegalArgumentException("Unknown uri " + uri); } - - if (numberOfRowsAffected > 0) { + if(deleteNotifyChange()) { getContext().getContentResolver().notifyChange(uri, null); } return numberOfRowsAffected; } + @Override + public String getType(Uri uri) { + Contract contract = findMatchingContract(uri); + switch (uriMatcher.match(uri)) { + case ITEM: + case INNER_JOIN: + case LEFT_OUTER_JOIN: + case CROSS_JOIN: + return "vnd.android.cursor.dir/vdn." + contract.getAuthority() + '.' + contract.getTable(); + + case ITEM_ID: + return "vnd.android.cursor.item/vdn." + contract.getAuthority() + '.' + contract.getTable(); + + default: + throw new IllegalArgumentException("Unknown uri " + uri); + } + } + /** * @param uri The {@link Uri} to be matched. * @return A {@link com.tjeannin.provigen.model.Contract} matching the given {@link Uri}. */ public Contract findMatchingContract(Uri uri) { for (Contract contract : contracts) { - if (contract.getTable().equals(uri.getPathSegments().get(0))) { - return contract; + if(contract.getDbName() == null) { + if (contract.getTable().equals(uri.getPathSegments().get(0))) { + return contract; + } + } else { + if (contract.getDbName().equals(uri.getPathSegments().get(0)) && contract.getTable().equals(uri.getPathSegments().get(1))) { + return contract; + } } } return null; @@ -211,4 +377,76 @@ private static String[] appendToStringArray(String[] array, String element) { } } + /** + * Open database + * @param contract Contract class + * @param readOnly Open database only for read + * @return SQLiteDatabase object + */ + private SQLiteDatabase openDatabase(Contract contract, boolean readOnly) { + if(contract.getDbName() == null) { + return readOnly ? openHelpers[0].getReadableDatabase() : openHelpers[0].getWritableDatabase(); + } + else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + for (SQLiteOpenHelper helper : openHelpers) { + if (contract.getDbName().equals(helper.getDatabaseName())) { + return readOnly ? helper.getReadableDatabase() : helper.getWritableDatabase(); + } + } + } + return readOnly ? openHelpers[1].getReadableDatabase() : openHelpers[1].getWritableDatabase(); + } + } + + /** + * Return join expression as string from URI + * @param uri Join URI + * @param hasDbName True if URI has database name + * @return Join expression + */ + private String parseJoinUri(Uri uri, boolean hasDbName) { + List pathSegments = uri.getPathSegments(); + if(pathSegments.size() > 3) { + StringBuilder builder = new StringBuilder(); + String table1 = pathSegments.get(hasDbName ? 1 : 0); + String joinType; + switch (pathSegments.get(hasDbName ? 2 : 1)) { + case "inner_join": + joinType = " INNER JOIN "; + break; + + case "left_outer_join": + joinType = " LEFT OUTER JOIN "; + break; + + case "cross_join": + joinType = " CROSS JOIN "; + break; + + default: + joinType = ""; + } + builder.append(table1); + + List subList = pathSegments.subList(hasDbName ? 3 : 2, pathSegments.size()); // ex: {"doctor", "doctor_id:id", ...} + for(String tableOrFields : subList) { + // fields separate by ":" + if(tableOrFields.contains(":")) { + builder.append(" ON ") + .append(tableOrFields.substring(0, tableOrFields.indexOf(":"))) + .append(" = ") + .append(tableOrFields.substring(tableOrFields.indexOf(":") + 1)); + } + // table name + else { + builder.append(joinType).append(" ").append(tableOrFields); + } + } + + return builder.toString(); + } else { + throw new IllegalArgumentException("Illegal join URI: " + uri); + } + } } diff --git a/ProviGenLib/src/com/tjeannin/provigen/annotation/ForeignKey.java b/ProviGenLib/src/com/tjeannin/provigen/annotation/ForeignKey.java new file mode 100644 index 0000000..2d512b5 --- /dev/null +++ b/ProviGenLib/src/com/tjeannin/provigen/annotation/ForeignKey.java @@ -0,0 +1,19 @@ +package com.tjeannin.provigen.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Identifies a field of a contract class that should be used as a foreign key in the database.
+ * This annotation should be used alongside with a {@link Column} annotation.

+ * table - The name of the table on which the foreign key is referenced.
+ * column - The name of the column on which the foreign key is referenced.
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ForeignKey { + String table(); + String column(); +} diff --git a/ProviGenLib/src/com/tjeannin/provigen/annotation/Id.java b/ProviGenLib/src/com/tjeannin/provigen/annotation/Id.java index c2326de..11e5234 100644 --- a/ProviGenLib/src/com/tjeannin/provigen/annotation/Id.java +++ b/ProviGenLib/src/com/tjeannin/provigen/annotation/Id.java @@ -7,9 +7,10 @@ /** * Identifies a field of a contract class that should be used as a primary key in the database.
- * This annotation should be used alongside with a {@link Column} annotation. + * This annotation should be used alongside with a {@link Column} annotation. The autoincrement attribute should be true or false (by default). */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Id { + boolean autoincrement() default false; } diff --git a/ProviGenLib/src/com/tjeannin/provigen/helper/ContractUtil.java b/ProviGenLib/src/com/tjeannin/provigen/helper/ContractUtil.java new file mode 100644 index 0000000..82e4b0a --- /dev/null +++ b/ProviGenLib/src/com/tjeannin/provigen/helper/ContractUtil.java @@ -0,0 +1,37 @@ +package com.tjeannin.provigen.helper; + +/** + * Created by Dre on 08.04.2016. + * + */ +public class ContractUtil { + + /** + * Return full name of field + * @param table Table + * @param field Field + * @return Full name of field + */ + public static String fullName(String table, String field) { + return table + "." + field; + } + + /** + * Return name of field after join + * @param table Table + * @param field Field + * @return Name of field after join + */ + public static String joinName(String table, String field) { + if(table.endsWith("_") && field.startsWith("_")) { + return table + field.substring(1 , field.length()); + } + else if(table.endsWith("_") || field.startsWith("_")) { + return table + field; + } + else { + return table + "_" + field; + } + } + +} diff --git a/ProviGenLib/src/com/tjeannin/provigen/helper/ProviGenUriBuilder.java b/ProviGenLib/src/com/tjeannin/provigen/helper/ProviGenUriBuilder.java new file mode 100644 index 0000000..954259c --- /dev/null +++ b/ProviGenLib/src/com/tjeannin/provigen/helper/ProviGenUriBuilder.java @@ -0,0 +1,88 @@ +package com.tjeannin.provigen.helper; + +import android.net.Uri; + +import com.tjeannin.provigen.model.JoinEntity; + +/** + * Created by Dre on 07.04.2016. + * + */ +public class ProviGenUriBuilder { + + public enum JoinType { + INNER_JOIN, LEFT_OUTER_JOIN, CROSS_JOIN + } + + /** + * Create content URI for contract class + * @param authority authority + * @param tableName tableName + * @return Content URI + */ + public static Uri contentUri(String authority, String tableName) { + return contentUri(authority, tableName, null); + } + + /** + * Create content URI with database name for contract class + * @param authority authority + * @param tableName tableName + * @param dbName database name + * @return Content URI + */ + public static Uri contentUri(String authority, String tableName, String dbName) { + return Uri.parse("content://" + authority + "/" + (dbName != null ? dbName + "/" : "") + tableName); + } + + /** + * Create URI for [inner, left outer, cross] join query + * + * @param joinType Type of join - INNER_JOIN, LEFT_OUTER_JOIN or CROSS_JOIN + * @param uriTable1 URI of left table + * @param joinEntities List of join entities (right table) + * @return URI + */ + public static Uri joinUri(JoinType joinType, Uri uriTable1, JoinEntity ... joinEntities) { + if(joinEntities.length == 0) { + throw new IllegalArgumentException("joinEntities is empty"); + } + + boolean isCrossJoin = false; + String joinPath; + switch (joinType) { + case INNER_JOIN: + joinPath = "inner_join/"; + break; + + case LEFT_OUTER_JOIN: + joinPath = "left_outer_join/"; + break; + + case CROSS_JOIN: + joinPath = "cross_join/"; + isCrossJoin = true; + break; + + default: + joinPath = null; + break; + } + + Uri joinUri = Uri.withAppendedPath(uriTable1, joinPath); + for(int i = 0; i < joinEntities.length; i++) { + String pathSegment; + if(isCrossJoin) { + pathSegment = joinEntities[i].getTableName(); + } else { + pathSegment = joinEntities[i].getTableName() + "/" + joinEntities[i].getLeftField() + ":" + joinEntities[i].getRightField(); + } + if(i < joinEntities.length - 1) { + pathSegment += "/"; + } + joinUri = Uri.withAppendedPath(joinUri, pathSegment); + } + + return joinUri; + } +} diff --git a/ProviGenLib/src/com/tjeannin/provigen/helper/TableBuilder.java b/ProviGenLib/src/com/tjeannin/provigen/helper/TableBuilder.java index 7677c30..d2661e5 100644 --- a/ProviGenLib/src/com/tjeannin/provigen/helper/TableBuilder.java +++ b/ProviGenLib/src/com/tjeannin/provigen/helper/TableBuilder.java @@ -1,9 +1,12 @@ package com.tjeannin.provigen.helper; import android.database.sqlite.SQLiteDatabase; + +import com.tjeannin.provigen.annotation.Column; import com.tjeannin.provigen.model.Constraint; import com.tjeannin.provigen.model.Contract; import com.tjeannin.provigen.model.ContractField; +import com.tjeannin.provigen.model.ForeignKeyConstraint; import java.util.ArrayList; import java.util.List; @@ -21,7 +24,7 @@ public class TableBuilder { */ public TableBuilder(Class contractClass) { contract = new Contract(contractClass); - constraints = new ArrayList(); + constraints = new ArrayList<>(); } /** @@ -55,23 +58,62 @@ public TableBuilder addConstraint(String columnName, String constraintType, Stri */ public String getSQL() { - StringBuilder builder = new StringBuilder("CREATE TABLE "); + StringBuilder builder = new StringBuilder("CREATE TABLE IF NOT EXISTS "); builder.append(contract.getTable()).append(" ( "); for (ContractField field : contract.getFields()) { builder.append(" ").append(field.name).append(" ").append(field.type); - if (field.name.equals(contract.getIdField())) { - builder.append(" PRIMARY KEY AUTOINCREMENT "); + + boolean isSinglePrimaryKey = false; + // single PRIMARY KEY + if(contract.getIdFields().size() == 1) { + if (field.name.equals(contract.getIdFields().get(0))) { + isSinglePrimaryKey = true; + builder.append(" NOT NULL PRIMARY KEY "); + + if (field.type.equals(Column.Type.INTEGER)) { + if(contract.isAutoincrement(field.name)) { + builder.append(" AUTOINCREMENT "); // 123 456 789 + } + } + } } - for (Constraint constraint : constraints) { - if (constraint.targetColumn.equals(field.name)) { - builder.append(" ").append(constraint.type).append(" ON CONFLICT ").append(constraint.conflictClause); + + if(!isSinglePrimaryKey) { + for (Constraint constraint : constraints) { + if (constraint.targetColumn.equals(field.name)) { + builder.append(" ").append(constraint.type).append(" ON CONFLICT ").append(constraint.conflictClause); + } } } + builder.append(", "); } - builder.deleteCharAt(builder.length() - 2); - builder.append(" ) "); + + // foreign keys + for(ForeignKeyConstraint foreignKey : contract.getForeignKeys()) { + builder.append("FOREIGN KEY (").append(foreignKey.getColumn()).append(") references ") + .append(foreignKey.getTableReferenced()).append("(").append(foreignKey.getColumnReferenced()).append("), "); + } + + // composite primary key + if(contract.getIdFields().size() > 1) { + builder.append("PRIMARY KEY ("); + // пробегаем по полям, помеченными как @Id + for(int i = 0; i < contract.getIdFields().size(); i++) { + builder.append(contract.getIdFields().get(i)); + if(i < contract.getIdFields().size() - 1) { + builder.append(", "); + } + } + builder.append(")"); + } + else { + // delete ',' in the end + builder.deleteCharAt(builder.length() - 2); + } + builder.append(")"); + return builder.toString(); } @@ -83,4 +125,4 @@ public String getSQL() { public void createTable(SQLiteDatabase database) { database.execSQL(getSQL()); } -} +} \ No newline at end of file diff --git a/ProviGenLib/src/com/tjeannin/provigen/model/Contract.java b/ProviGenLib/src/com/tjeannin/provigen/model/Contract.java index 44e6d98..7137ad7 100644 --- a/ProviGenLib/src/com/tjeannin/provigen/model/Contract.java +++ b/ProviGenLib/src/com/tjeannin/provigen/model/Contract.java @@ -3,23 +3,30 @@ import android.net.Uri; import com.tjeannin.provigen.annotation.Column; import com.tjeannin.provigen.annotation.ContentUri; +import com.tjeannin.provigen.annotation.ForeignKey; import com.tjeannin.provigen.annotation.Id; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class Contract { private String authority; - private String idField; + private String dbName; private String tableName; private List contractFields; + private Map idFieldMap; + private List foreignKeys; @SuppressWarnings({"rawtypes", "unchecked"}) public Contract(Class contractClass) { - contractFields = new ArrayList(); + idFieldMap = new HashMap<>(); + contractFields = new ArrayList<>(); + foreignKeys = new ArrayList<>(); Field[] fields = contractClass.getFields(); for (Field field : fields) { @@ -30,6 +37,12 @@ public Contract(Class contractClass) { Uri uri = (Uri) field.get(null); authority = uri.getAuthority(); tableName = uri.getLastPathSegment(); + + List pathSegments = uri.getPathSegments(); + if(pathSegments.size() == 2) { + dbName = pathSegments.get(0); + } + } catch (Exception e) { e.printStackTrace(); } @@ -38,7 +51,23 @@ public Contract(Class contractClass) { Id id = field.getAnnotation(Id.class); if (id != null) { try { - idField = (String) field.get(null); + String idField = (String) field.get(null); + idFieldMap.put(idField, id.autoincrement()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + ForeignKey foreignKey = field.getAnnotation(ForeignKey.class); + if(foreignKey != null) { + try { + ForeignKeyConstraint foreignKeyConstraint = null; + if(foreignKey.table() != null && foreignKey.column() != null) { + foreignKeyConstraint = new ForeignKeyConstraint((String) field.get(null), foreignKey.table(), foreignKey.column()); + } + if(foreignKeyConstraint != null) { + foreignKeys.add(foreignKeyConstraint); + } } catch (Exception e) { e.printStackTrace(); } @@ -59,15 +88,27 @@ public String getAuthority() { return authority; } - public String getIdField() { - return idField; - } - public String getTable() { return tableName; } + public String getDbName() { + return dbName; + } + + public List getIdFields() { + return new ArrayList<>(idFieldMap.keySet()); + } + public List getFields() { return contractFields; } + + public boolean isAutoincrement(String field) { + return idFieldMap.get(field); + } + + public List getForeignKeys() { + return foreignKeys; + } } diff --git a/ProviGenLib/src/com/tjeannin/provigen/model/ForeignKeyConstraint.java b/ProviGenLib/src/com/tjeannin/provigen/model/ForeignKeyConstraint.java new file mode 100644 index 0000000..04d04ed --- /dev/null +++ b/ProviGenLib/src/com/tjeannin/provigen/model/ForeignKeyConstraint.java @@ -0,0 +1,44 @@ +package com.tjeannin.provigen.model; + +/** + * Created by Dre on 14.04.2016. + * + */ +public class ForeignKeyConstraint { + + private String column; + private String tableReferenced; + private String columnReferenced; + + public ForeignKeyConstraint() {} + + public ForeignKeyConstraint(String column, String tableReferenced, String columnReferenced) { + this.column = column; + this.tableReferenced = tableReferenced; + this.columnReferenced = columnReferenced; + } + + public String getColumn() { + return column; + } + + public void setColumn(String column) { + this.column = column; + } + + public String getTableReferenced() { + return tableReferenced; + } + + public void setTableReferenced(String tableReferenced) { + this.tableReferenced = tableReferenced; + } + + public String getColumnReferenced() { + return columnReferenced; + } + + public void setColumnReferenced(String columnReferenced) { + this.columnReferenced = columnReferenced; + } +} diff --git a/ProviGenLib/src/com/tjeannin/provigen/model/JoinEntity.java b/ProviGenLib/src/com/tjeannin/provigen/model/JoinEntity.java new file mode 100644 index 0000000..d778259 --- /dev/null +++ b/ProviGenLib/src/com/tjeannin/provigen/model/JoinEntity.java @@ -0,0 +1,73 @@ +package com.tjeannin.provigen.model; + +import android.net.Uri; + +/** + * This model needed to execute a join-query + * + * Created by Dre on 07.04.2016. + */ +public class JoinEntity { + + private Uri contentUri; + private String leftField; + private String rightField; + + public JoinEntity() { } + + /** + * For cross join. + * @param contentUri URI of right join table + */ + public JoinEntity(Uri contentUri) { + this.contentUri = contentUri; + } + + /** + * @param contentUri URI of right join table + * @param leftField Name of left field + * @param rightField Name of right field + */ + public JoinEntity(Uri contentUri, String leftField, String rightField) { + this.contentUri = contentUri; + this.leftField = leftField; + this.rightField = rightField; + } + + public Uri getContentUri() { + return contentUri; + } + + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; + } + + public String getLeftField() { + return leftField; + } + + public void setLeftField(String leftField) { + this.leftField = leftField; + } + + public String getRightField() { + return rightField; + } + + public void setRightField(String rightField) { + this.rightField = rightField; + } + + public String getTableName() { + return contentUri.getLastPathSegment(); + } + + @Override + public String toString() { + return "JoinEntity{" + + "contentUri=" + contentUri + + ", leftField='" + leftField + '\'' + + ", rightField='" + rightField + '\'' + + '}'; + } +} diff --git a/ProviGenSample/AndroidManifest.xml b/ProviGenSample/AndroidManifest.xml index 46f3175..d4110c3 100644 --- a/ProviGenSample/AndroidManifest.xml +++ b/ProviGenSample/AndroidManifest.xml @@ -1,32 +1,33 @@ + package="com.tjeannin.provigen.sample" + android:versionCode="1" + android:versionName="1.0"> + android:minSdkVersion="15" + android:targetSdkVersion="23"/> + android:allowBackup="false" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@style/AppTheme"> + + android:name=".MainActivity" + android:label="@string/app_name"> - + android:name="com.tjeannin.provigen.sample.SampleContentProvider" + android:authorities="com.tjeannin.provigen.sample" + android:exported="false"> + \ No newline at end of file diff --git a/ProviGenSample/build.gradle b/ProviGenSample/build.gradle index d21758c..d83bc63 100644 --- a/ProviGenSample/build.gradle +++ b/ProviGenSample/build.gradle @@ -1,7 +1,12 @@ android { - buildToolsVersion "19.1.0" - compileSdkVersion 19 + buildToolsVersion "23.0.3" + compileSdkVersion 23 + + dependencies { + compile 'com.android.support:support-v4:23.3.0' + } + sourceSets { main { manifest.srcFile 'AndroidManifest.xml' diff --git a/ProviGenSample/project.properties b/ProviGenSample/project.properties index ed6ad53..a3fff05 100644 --- a/ProviGenSample/project.properties +++ b/ProviGenSample/project.properties @@ -11,5 +11,5 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-19 +target=android-23 diff --git a/ProviGenSample/res/layout/person_item.xml b/ProviGenSample/res/layout/person_item.xml index 7953f35..402acf3 100644 --- a/ProviGenSample/res/layout/person_item.xml +++ b/ProviGenSample/res/layout/person_item.xml @@ -1,20 +1,29 @@ - + + android:id="@+id/person_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="90dp" + android:textStyle="bold" + android:padding="10dp" + android:text="Name"/> + android:id="@+id/person_age" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="10dp" + android:text="25"/> - \ No newline at end of file + + + \ No newline at end of file diff --git a/ProviGenSample/src/com/tjeannin/provigen/sample/MainActivity.java b/ProviGenSample/src/com/tjeannin/provigen/sample/MainActivity.java index b882fef..5c4eef0 100644 --- a/ProviGenSample/src/com/tjeannin/provigen/sample/MainActivity.java +++ b/ProviGenSample/src/com/tjeannin/provigen/sample/MainActivity.java @@ -2,6 +2,7 @@ import android.content.ContentValues; import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager.LoaderCallbacks; @@ -13,8 +14,14 @@ import android.view.View.OnClickListener; import android.widget.ListView; +import com.tjeannin.provigen.helper.ContractUtil; +import com.tjeannin.provigen.helper.ProviGenUriBuilder; +import com.tjeannin.provigen.model.JoinEntity; + public class MainActivity extends FragmentActivity implements LoaderCallbacks, OnClickListener { + private static final String[] NAMES = {"David", "Stephanie", "John", "Anna", "Thomas", "Natalie", "Andrew", "Sofia", "Richard", "Alexandra", "Matthew", "Grace", "Christian", "Mary"}; + private static final String[] SPECIALTIES = {"Android Developer", "iOS Developer", "Backend Developer", "Frontend Developer", "Web Developer", "Software Tester", "System Administrator", "Graphic Designer", "Project Manager", "Team Lead", "CEO"}; private SimpleCursorAdapter adapter; @Override @@ -23,12 +30,17 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); String[] columns = new String[]{ - SampleContentProvider.Person.AGE, - SampleContentProvider.Person.NAME}; + SampleContract.Person.AGE, + // important for correct obtaining of the columns + ContractUtil.joinName(SampleContract.Person.TABLE_NAME, SampleContract.Person.NAME), + ContractUtil.joinName(SampleContract.Specialty.TABLE_NAME, SampleContract.Specialty.NAME) + }; int[] ids = { R.id.person_age, - R.id.person_name}; + R.id.person_name, + R.id.person_spec + }; adapter = new SimpleCursorAdapter(this, R.layout.person_item, null, columns, ids, 0); @@ -37,6 +49,7 @@ public void onCreate(Bundle savedInstanceState) { findViewById(R.id.add).setOnClickListener(this); findViewById(R.id.delete).setOnClickListener(this); + initSpecialties(); getSupportLoaderManager().initLoader(0, null, this); } @@ -46,9 +59,39 @@ public boolean onCreateOptionsMenu(Menu menu) { return true; } + private void initSpecialties() { + ContentValues[] valuesArray = new ContentValues[SPECIALTIES.length]; + for(int i = 0; i < SPECIALTIES.length; i++) { + ContentValues values = new ContentValues(); + values.put(SampleContract.Specialty.ID, i); + values.put(SampleContract.Specialty.NAME, SPECIALTIES[i]); + valuesArray[i] = values; + } + getContentResolver().bulkInsert(SampleContract.Specialty.CONTENT_URI, valuesArray); + } + + private void addPerson() { + ContentValues values = new ContentValues(); + values.put(SampleContract.Person.AGE, (int) (Math.random() * 40 + 20)); + values.put(SampleContract.Person.NAME, NAMES[(int) (Math.random() * NAMES.length)]); + values.put(SampleContract.Person.SPECIALTY_ID, (int) (Math.random() * SPECIALTIES.length)); + getContentResolver().insert(SampleContract.Person.CONTENT_URI, values); + } + + private void clearPerson() { + getContentResolver().delete(SampleContract.Person.CONTENT_URI, null, null); + } + @Override public Loader onCreateLoader(int id, Bundle args) { - return new CursorLoader(this, SampleContentProvider.Person.CONTENT_URI, null, "", null, ""); + // create special join URI and execute the query + Uri innerJoinUri = ProviGenUriBuilder.joinUri( + ProviGenUriBuilder.JoinType.INNER_JOIN, + SampleContract.Person.CONTENT_URI, + new JoinEntity(SampleContract.Specialty.CONTENT_URI, SampleContract.Person.SPECIALTY_ID , SampleContract.Specialty.ID) + ); + + return new CursorLoader(this, innerJoinUri, SampleContract.Person.JOIN_PROJECTION, null, null, null); } @Override @@ -66,17 +109,14 @@ public void onClick(View view) { switch (view.getId()) { case R.id.add: - ContentValues values = new ContentValues(); - values.put(SampleContentProvider.Person.AGE, 20); - values.put(SampleContentProvider.Person.NAME, "Some Name"); - getContentResolver().insert(SampleContentProvider.Person.CONTENT_URI, values); + addPerson(); break; case R.id.delete: - getContentResolver().delete(SampleContentProvider.Person.CONTENT_URI, "", null); + clearPerson(); break; - default: + default: break; } diff --git a/ProviGenSample/src/com/tjeannin/provigen/sample/SampleContentProvider.java b/ProviGenSample/src/com/tjeannin/provigen/sample/SampleContentProvider.java index f141dd6..ae33243 100644 --- a/ProviGenSample/src/com/tjeannin/provigen/sample/SampleContentProvider.java +++ b/ProviGenSample/src/com/tjeannin/provigen/sample/SampleContentProvider.java @@ -1,37 +1,58 @@ package com.tjeannin.provigen.sample; import android.content.Context; +import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; -import com.tjeannin.provigen.ProviGenBaseContract; + import com.tjeannin.provigen.ProviGenOpenHelper; import com.tjeannin.provigen.ProviGenProvider; -import com.tjeannin.provigen.annotation.Column; -import com.tjeannin.provigen.annotation.Column.Type; -import com.tjeannin.provigen.annotation.ContentUri; public class SampleContentProvider extends ProviGenProvider { + public static final String AUTHORITY = "com.tjeannin.provigen.sample"; + + public static final String DB_NAME = "ProviGenDatabase"; + public static final int DB_VERSION = 1; + + public static final String SECOND_DB_NAME = "ProviGenDatabaseSecond"; + public static final int SECOND_DB_VERSION = 1; + + private static final Class[] CONTRACTS = new Class[] { + SampleContract.Person.class, + SampleContract.Specialty.class, + SampleContract.Passport.class + }; + + private static final Class[] CONTRACTS_SECOND = new Class[] { + SampleContract.PersonSecondDb.class + }; + @Override public SQLiteOpenHelper openHelper(Context context) { - return new ProviGenOpenHelper(getContext(), "ProviGenDatabase", null, 1, new Class[]{Person.class}); + return null; // you can return null } @Override - public Class[] contractClasses() { - return new Class[]{Person.class}; + public SQLiteOpenHelper[] openHelpers(Context context) { + return new SQLiteOpenHelper[] { + new ProviGenOpenHelper(getContext(), DB_NAME, null, DB_VERSION, CONTRACTS), // first db + new ProviGenOpenHelper(getContext(), SECOND_DB_NAME, null, SECOND_DB_VERSION, CONTRACTS_SECOND) // second db + }; } - public static interface Person extends ProviGenBaseContract { - - @Column(Type.INTEGER) - public static final String AGE = "int"; + @Override + public Class[] contractClasses() { + return null; // you can return null + } - @Column(Type.TEXT) - public static final String NAME = "string"; + @Override + public Class[][] contractClassesMultipleDb() { + return new Class[][] {CONTRACTS, CONTRACTS_SECOND}; + } - @ContentUri - public static final Uri CONTENT_URI = Uri.parse("content://com.tjeannin.provigen.sample/persons"); + @Override + public int conflictAlgorithm() { + return SQLiteDatabase.CONFLICT_REPLACE; } } diff --git a/ProviGenSample/src/com/tjeannin/provigen/sample/SampleContract.java b/ProviGenSample/src/com/tjeannin/provigen/sample/SampleContract.java new file mode 100644 index 0000000..6d05c91 --- /dev/null +++ b/ProviGenSample/src/com/tjeannin/provigen/sample/SampleContract.java @@ -0,0 +1,108 @@ +package com.tjeannin.provigen.sample; + +import android.net.Uri; + +import com.tjeannin.provigen.ProviGenBaseContract; +import com.tjeannin.provigen.annotation.Column; +import com.tjeannin.provigen.annotation.ContentUri; +import com.tjeannin.provigen.annotation.ForeignKey; +import com.tjeannin.provigen.annotation.Id; +import com.tjeannin.provigen.helper.ContractUtil; +import com.tjeannin.provigen.helper.ProviGenUriBuilder; + +/** + * Created by Dre on 11.04.2016. + * + */ +public class SampleContract { + + public static class Person implements ProviGenBaseContract { + + public static final String TABLE_NAME = "person"; + + @Column(Column.Type.INTEGER) + public static final String AGE = "age"; + + @Column(Column.Type.TEXT) + public static final String NAME = "name"; + + @ForeignKey(table = Specialty.TABLE_NAME, column = Specialty.ID) + @Column(Column.Type.INTEGER) + public static final String SPECIALTY_ID = "specialty_id"; + + @ContentUri + public static final Uri CONTENT_URI = ProviGenUriBuilder.contentUri(SampleContentProvider.AUTHORITY, TABLE_NAME); + + public static final String[] DEFAULT_PROJECTION = new String[] { _ID, AGE, NAME }; + + public static final String[] JOIN_PROJECTION = new String[] { + _ID, + AGE, + // ContractUtil.fullName() used to avoid ambiguous column name + ContractUtil.fullName(TABLE_NAME, NAME), + ContractUtil.fullName(Specialty.TABLE_NAME, Specialty.NAME) + }; + } + + // not implement ProviGenBaseContract interface (because non-autoincrement primary key) + public static class Specialty { + + public static final String TABLE_NAME = "specialty"; + + @Id(autoincrement = false) + @Column(Column.Type.INTEGER) + public static final String ID = "id"; + + @Column(Column.Type.TEXT) + public static final String NAME = "name"; + + @ContentUri + public static final Uri CONTENT_URI = ProviGenUriBuilder.contentUri(SampleContentProvider.AUTHORITY, TABLE_NAME); + + public static final String[] DEFAULT_PROJECTION = new String[] { ID, NAME }; + } + + // not implement ProviGenBaseContract interface (because non-autoincrement composite primary key) + public static class Passport { + + public static final String TABLE_NAME = "passport"; + + @Id + @Column(Column.Type.TEXT) + public static final String SERIES = "series"; + + @Id + @Column(Column.Type.INTEGER) + public static final String NUMBER = "number"; + + @ContentUri + public static final Uri CONTENT_URI = ProviGenUriBuilder.contentUri(SampleContentProvider.AUTHORITY, TABLE_NAME); + + public static final String[] DEFAULT_PROJECTION = new String[] { + SERIES, + NUMBER + }; + } + + /** + * Contract class for second database + */ + public static class PersonSecondDb implements ProviGenBaseContract { + + public static final String TABLE_NAME = "persons_second"; + + @Column(Column.Type.INTEGER) + public static final String AGE = "age"; + + @Column(Column.Type.TEXT) + public static final String NAME = "name"; + + @ContentUri + public static final Uri CONTENT_URI = ProviGenUriBuilder.contentUri( + SampleContentProvider.AUTHORITY, + TABLE_NAME, + SampleContentProvider.SECOND_DB_NAME); // name of second database + + public static final String[] DEFAULT_PROJECTION = new String[] { _ID, AGE, NAME }; + } +} diff --git a/ProviGenTests/build.gradle b/ProviGenTests/build.gradle index 2d1a49f..d9f30f9 100644 --- a/ProviGenTests/build.gradle +++ b/ProviGenTests/build.gradle @@ -1,6 +1,6 @@ android { - buildToolsVersion "19.1.0" - compileSdkVersion 19 + buildToolsVersion "23.0.3" + compileSdkVersion 23 sourceSets { main { diff --git a/ProviGenTests/src/com/tjeannin/provigen/test/ExtendedProviderTestCase.java b/ProviGenTests/src/com/tjeannin/provigen/test/ExtendedProviderTestCase.java index b93a09e..60fd086 100644 --- a/ProviGenTests/src/com/tjeannin/provigen/test/ExtendedProviderTestCase.java +++ b/ProviGenTests/src/com/tjeannin/provigen/test/ExtendedProviderTestCase.java @@ -2,8 +2,10 @@ import android.content.ContentValues; import android.database.Cursor; +import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.test.ProviderTestCase2; + import com.tjeannin.provigen.ProviGenOpenHelper; import com.tjeannin.provigen.ProviGenProvider; import com.tjeannin.provigen.annotation.Column; @@ -61,11 +63,13 @@ protected void resetContractClasses(Class[] contractClasses) { getSuperClassField(getProvider(), "contracts").set(getProvider(), contracts); // Replace the open helper. - Field openHelperField = getSuperClassField(getProvider(), "openHelper"); - ProviGenOpenHelper openHelper = (ProviGenOpenHelper) openHelperField.get(getProvider()); + Field openHelpersField = getSuperClassField(getProvider(), "openHelpers"); + ProviGenOpenHelper openHelper = (ProviGenOpenHelper) ((SQLiteOpenHelper[]) openHelpersField.get(getProvider()))[0]; int version = getSuperClassField(openHelper, "mNewVersion").getInt(openHelper); - openHelperField.set(getProvider(), new ProviGenOpenHelper( - getProvider().getContext(), "ProviGenDatabase", null, version + 1, contractClasses)); + openHelpersField.set( + getProvider(), + new SQLiteOpenHelper[] {new ProviGenOpenHelper(getProvider().getContext(), "ProviGenDatabase", null, version + 1, contractClasses)} + ); } catch (IllegalAccessException e) { e.printStackTrace(); diff --git a/ProviGenTests/src/com/tjeannin/provigen/test/basis/SimpleContentProvider.java b/ProviGenTests/src/com/tjeannin/provigen/test/basis/SimpleContentProvider.java index dd1918a..5ad7830 100644 --- a/ProviGenTests/src/com/tjeannin/provigen/test/basis/SimpleContentProvider.java +++ b/ProviGenTests/src/com/tjeannin/provigen/test/basis/SimpleContentProvider.java @@ -24,15 +24,19 @@ public Class[] contractClasses() { public interface ContractOne extends ProviGenBaseContract { + String TABLE_NAME = "table_name_simple"; + @Column(Type.INTEGER) String MY_INT = "int"; @ContentUri - Uri CONTENT_URI = Uri.parse("content://com.test.simple/table_name_simple"); + Uri CONTENT_URI = Uri.parse("content://com.test.simple/" + TABLE_NAME); } public interface ContractTwo extends ProviGenBaseContract { + String TABLE_NAME = "table_name_simple"; + @Column(Type.INTEGER) String MY_INT = "int"; @@ -43,6 +47,6 @@ public interface ContractTwo extends ProviGenBaseContract { String MY_REAL = "real"; @ContentUri - Uri CONTENT_URI = Uri.parse("content://com.test.simple/table_name_simple"); + Uri CONTENT_URI = Uri.parse("content://com.test.simple/" + TABLE_NAME); } } diff --git a/ProviGenTests/src/com/tjeannin/provigen/test/basis/SimpleContentProviderTest.java b/ProviGenTests/src/com/tjeannin/provigen/test/basis/SimpleContentProviderTest.java index 83abd37..d883b81 100644 --- a/ProviGenTests/src/com/tjeannin/provigen/test/basis/SimpleContentProviderTest.java +++ b/ProviGenTests/src/com/tjeannin/provigen/test/basis/SimpleContentProviderTest.java @@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.test.mock.MockContentResolver; + import com.tjeannin.provigen.test.ExtendedProviderTestCase; import com.tjeannin.provigen.test.basis.SimpleContentProvider.ContractOne; import com.tjeannin.provigen.test.basis.SimpleContentProvider.ContractTwo; @@ -16,10 +17,11 @@ public class SimpleContentProviderTest extends ExtendedProviderTestCase { + private static final String PROVIDER_AUTHORITY = "com.test.simple"; private MockContentResolver contentResolver; public SimpleContentProviderTest() { - super(SimpleContentProvider.class, "com.test.simple"); + super(SimpleContentProvider.class, PROVIDER_AUTHORITY); } @Override @@ -41,16 +43,16 @@ public void testInsertQueryUpdateDelete() { // Update final ContentValues contentValues = new ContentValues(2); contentValues.put(ContractOne.MY_INT, 15); - contentResolver.update(ContractOne.CONTENT_URI, contentValues, "", null); + contentResolver.update(ContractOne.CONTENT_URI, contentValues, null, null); // Query - final Cursor cursor = contentResolver.query(ContractOne.CONTENT_URI, null, "", null, ""); + final Cursor cursor = contentResolver.query(ContractOne.CONTENT_URI, null, null, null, null); cursor.moveToFirst(); assertEquals(cursor.getInt(cursor.getColumnIndex(ContractOne.MY_INT)), 15); cursor.close(); // Delete - contentResolver.delete(Uri.withAppendedPath(ContractOne.CONTENT_URI, String.valueOf(1)), "", null); + contentResolver.delete(Uri.withAppendedPath(ContractOne.CONTENT_URI, String.valueOf(1)), null, null); assertEquals(0, getRowCount(ContractOne.CONTENT_URI)); } @@ -99,7 +101,7 @@ public void testAutoIncrement() { contentResolver.insert(ContractOne.CONTENT_URI, getContentValues(ContractOne.class)); contentResolver.insert(ContractOne.CONTENT_URI, getContentValues(ContractOne.class)); - contentResolver.delete(Uri.withAppendedPath(ContractOne.CONTENT_URI, String.valueOf(3)), "", null); + contentResolver.delete(Uri.withAppendedPath(ContractOne.CONTENT_URI, String.valueOf(3)), null, null); contentResolver.insert(ContractOne.CONTENT_URI, getContentValues(ContractOne.class)); @@ -155,7 +157,7 @@ private List getTableFields(final Uri contentUri) { final SQLiteDatabase sqLiteDatabase = getMockContext().openOrCreateDatabase("ProviGenDatabase", Context.MODE_PRIVATE, null); final String tableName = contentUri.getLastPathSegment(); final Cursor rawQuery = sqLiteDatabase.rawQuery("PRAGMA table_info(" + tableName + ')', null); - final List result = new ArrayList(rawQuery.getCount()); + final List result = new ArrayList<>(rawQuery.getCount()); if (rawQuery.moveToFirst()) { do { result.add(rawQuery.getString(rawQuery.getColumnIndex("name"))); @@ -167,6 +169,6 @@ private List getTableFields(final Uri contentUri) { public void testGetMimeType() { final String mimeType = getProvider().getType(ContractOne.CONTENT_URI); - assertEquals("vnd.android.cursor.dir/vdn.table_name_simple", mimeType); + assertEquals("vnd.android.cursor.dir/vdn." + PROVIDER_AUTHORITY + '.' + ContractOne.TABLE_NAME, mimeType); } } diff --git a/ProviGenTests/src/com/tjeannin/provigen/test/multiple/MultipleContractContentProvider.java b/ProviGenTests/src/com/tjeannin/provigen/test/multiple/MultipleContractContentProvider.java index cb9e0e9..d189e50 100644 --- a/ProviGenTests/src/com/tjeannin/provigen/test/multiple/MultipleContractContentProvider.java +++ b/ProviGenTests/src/com/tjeannin/provigen/test/multiple/MultipleContractContentProvider.java @@ -26,11 +26,13 @@ public Class[] contractClasses() { public static interface ContractOne extends ProviGenBaseContract { + String TABLE_NAME = "table_name_simple"; + @Column(Type.INTEGER) public static final String MY_INT = "int"; @ContentUri - public static final Uri CONTENT_URI = Uri.parse("content://com.test.simple/table_name_simple"); + public static final Uri CONTENT_URI = Uri.parse("content://com.test.simple/" + TABLE_NAME); } public static interface ContractTwo extends ProviGenBaseContract { diff --git a/README.md b/README.md index 4504eea..d117304 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,201 @@ new TableBuilder(MyContract.class) .createTable(database); ``` +### Primary key and foreign key + +Autoincrement or non-autoincrement primary key. + +```java +@Id(autoincrement = true) +@Column(Column.Type.INTEGER) +public static final String ID = "id"; +``` + +Composite primary key support. + +```java +public class Passport { + + public static final String TABLE_NAME = "passport"; + + @Id + @Column(Column.Type.TEXT) + public static final String SERIES = "series"; + + @Id + @Column(Column.Type.INTEGER) + public static final String NUMBER = "number"; + + @ContentUri + public static final Uri CONTENT_URI = ProviGenUriBuilder.contentUri(SampleContentProvider.AUTHORITY, TABLE_NAME); + +} +``` + +Foreign key support. + +```java +@ForeignKey(table = Specialty.TABLE_NAME, column = Specialty.ID) +@Column(Column.Type.INTEGER) +public static final String SPECIALTY_ID = "specialty_id"; +``` + +### Multiple databases support + +Add a name of the second database in your ContractClass. + +```java +public class PersonSecondDb implements ProviGenBaseContract { + + public static final String TABLE_NAME = "persons_second"; + + @Column(Column.Type.INTEGER) + public static final String AGE = "age"; + + @Column(Column.Type.TEXT) + public static final String NAME = "name"; + + @ContentUri + public static final Uri CONTENT_URI = ProviGenUriBuilder.contentUri( + SampleContentProvider.AUTHORITY, + TABLE_NAME, + SampleContentProvider.SECOND_DB_NAME); // name of second database +} +``` + +Override openHelpers(Context context) and contractClassesMultipleDb() methods in your content provider. + +```java +public class SampleContentProvider extends ProviGenProvider { + + public static final String AUTHORITY = "com.tjeannin.provigen.sample"; + + public static final String DB_NAME = "ProviGenDatabase"; + public static final int DB_VERSION = 1; + + public static final String SECOND_DB_NAME = "ProviGenDatabaseSecond"; + public static final int SECOND_DB_VERSION = 1; + + private static final Class[] CONTRACTS = new Class[] { + SampleContract.Person.class, + SampleContract.Specialty.class, + SampleContract.Passport.class + }; + + private static final Class[] CONTRACTS_SECOND = new Class[] { + SampleContract.PersonSecondDb.class + }; + + @Override + public SQLiteOpenHelper openHelper(Context context) { + return null; // you can return null + } + + @Override + public SQLiteOpenHelper[] openHelpers(Context context) { + return new SQLiteOpenHelper[] { + new ProviGenOpenHelper(getContext(), DB_NAME, null, DB_VERSION, CONTRACTS), // first db + new ProviGenOpenHelper(getContext(), SECOND_DB_NAME, null, SECOND_DB_VERSION, CONTRACTS_SECOND) // second db + }; + } + + @Override + public Class[] contractClasses() { + return null; // you can return null + } + + @Override + public Class[][] contractClassesMultipleDb() { + return new Class[][] {CONTRACTS, CONTRACTS_SECOND}; + } +} +``` + +### Joins support + +Inner join, left outer join and cross join support. +Your ContractClasses. + +```java +public static class Person implements ProviGenBaseContract { + + public static final String TABLE_NAME = "person"; + + @Column(Column.Type.INTEGER) + public static final String AGE = "age"; + + @Column(Column.Type.TEXT) + public static final String NAME = "name"; + + @Column(Column.Type.INTEGER) + public static final String SPECIALTY_ID = "specialty_id"; + + @ContentUri + public static final Uri CONTENT_URI = ProviGenUriBuilder.contentUri(SampleContentProvider.AUTHORITY, TABLE_NAME); + + public static final String[] DEFAULT_PROJECTION = new String[] { _ID, AGE, NAME }; + + // projection for join-query + public static final String[] JOIN_PROJECTION = new String[] { + _ID, + AGE, + // ContractUtil.fullName() used to avoid ambiguous column name + ContractUtil.fullName(TABLE_NAME, NAME), + ContractUtil.fullName(Specialty.TABLE_NAME, Specialty.NAME) + }; +} + +public static class Specialty { + + public static final String TABLE_NAME = "specialty"; + + @Id + @Column(Column.Type.INTEGER) + public static final String ID = "id"; + + @Column(Column.Type.TEXT) + public static final String NAME = "name"; + + @ContentUri + public static final Uri CONTENT_URI = ProviGenUriBuilder.contentUri(SampleContentProvider.AUTHORITY, TABLE_NAME); + + public static final String[] DEFAULT_PROJECTION = new String[] { ID, NAME }; +} +``` + +Use the method ContractUtil.joinName() for correct obtaining of the columns. + +```java +String[] columns = new String[]{ + SampleContract.Person.AGE, + // important for correct obtaining of the columns + ContractUtil.joinName(SampleContract.Person.TABLE_NAME, SampleContract.Person.NAME), + ContractUtil.joinName(SampleContract.Specialty.TABLE_NAME, SampleContract.Specialty.NAME) +}; + +int[] ids = { + R.id.person_age, + R.id.person_name, + R.id.person_spec +}; + +SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.person_item, null, columns, ids, 0); +``` + +Create special join URI using the method ProviGenUriBuilder.joinUri() and execute the query. + +```java +@Override +public Loader onCreateLoader(int id, Bundle args) { + Uri innerJoinUri = ProviGenUriBuilder.joinUri( + ProviGenUriBuilder.JoinType.INNER_JOIN, // inner, left outer or cross join + SampleContract.Person.CONTENT_URI, + new JoinEntity(SampleContract.Specialty.CONTENT_URI, SampleContract.Person.SPECIALTY_ID , SampleContract.Specialty.ID)); + + return new CursorLoader(this, innerJoinUri, SampleContract.Person.JOIN_PROJECTION, null, null, null); +} +``` + ## License This content is released under the MIT License. diff --git a/build.gradle b/build.gradle index 9613b1f..907e98d 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenLocal() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.android.tools.build:gradle:2.0.0' } } @@ -16,7 +16,7 @@ allprojects { } project(":ProviGenLib") { - apply plugin: "java" + apply plugin: 'java' dependencies { compile 'com.google.android:android:4.1.1.4' @@ -24,16 +24,15 @@ project(":ProviGenLib") { } project(":ProviGenSample") { - apply plugin: "android" + apply plugin: 'com.android.application' dependencies { compile project(":ProviGenLib") - compile 'com.android.support:support-v4:21.0.3' } } project(":ProviGenTests") { - apply plugin: "android" + apply plugin: 'com.android.application' dependencies { compile project(":ProviGenLib") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7b6f538..1d4094d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-all.zip