storable classsesadded for a while

This commit is contained in:
Gavriil Sitnikov 2016-03-17 11:17:09 +03:00
parent 7f46dd2d7f
commit eeb9a8483a
16 changed files with 758 additions and 0 deletions

View File

@ -0,0 +1,13 @@
package roboswag.org.storable;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}

View File

@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="roboswag.org.storable">
<application android:allowBackup="true" android:label="@string/app_name"
android:supportsRtl="true">
</application>
</manifest>

View File

@ -0,0 +1,150 @@
package roboswag.org.core;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import roboswag.org.core.exceptions.ObjectIsMutableException;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO: fill description
*/
public final class ObjectUtils {
private static final List<Class> IMMUTABLE_COLLECTIONS_TYPES
= Arrays.asList(AbstractList.class, AbstractMap.class, AbstractSet.class);
public static boolean equals(@Nullable Object object1, @Nullable Object object2) {
// copy of Arrays.deepEqualsElements
if (object1 == object2) {
return true;
}
if (object1 == null || object2 == null) {
return false;
}
Class<?> cl1 = object1.getClass().getComponentType();
Class<?> cl2 = object2.getClass().getComponentType();
if (cl1 != cl2) {
return false;
}
if (cl1 == null) {
return object1.equals(object2);
}
/*
* compare as arrays
*/
if (object1 instanceof Object[]) {
return Arrays.deepEquals((Object[]) object1, (Object[]) object2);
} else if (cl1 == int.class) {
return Arrays.equals((int[]) object1, (int[]) object2);
} else if (cl1 == char.class) {
return Arrays.equals((char[]) object1, (char[]) object2);
} else if (cl1 == boolean.class) {
return Arrays.equals((boolean[]) object1, (boolean[]) object2);
} else if (cl1 == byte.class) {
return Arrays.equals((byte[]) object1, (byte[]) object2);
} else if (cl1 == long.class) {
return Arrays.equals((long[]) object1, (long[]) object2);
} else if (cl1 == float.class) {
return Arrays.equals((float[]) object1, (float[]) object2);
} else if (cl1 == double.class) {
return Arrays.equals((double[]) object1, (double[]) object2);
} else {
return Arrays.equals((short[]) object1, (short[]) object2);
}
}
public static void checkIfIsImmutable(@NonNull Class<?> objectClass) throws ObjectIsMutableException {
checkIfIsImmutable(objectClass, false, new HashSet<>());
}
private static void checkIfIsImmutable(@NonNull Class<?> objectClass,
boolean isSuperclass,
@NonNull Set<Class> checkedClasses)
throws ObjectIsMutableException {
if (checkedClasses.contains(objectClass)) {
return;
}
checkedClasses.add(objectClass);
if (objectClass.isArray()) {
throw new ObjectIsMutableException(objectClass + " is array which is mutable");
}
if (objectClass.isPrimitive() || objectClass.getSuperclass() == Number.class
|| objectClass.isEnum() || objectClass == Boolean.class
|| objectClass == String.class || objectClass == Object.class) {
return;
}
if (isImmutableCollection(objectClass, objectClass.getGenericSuperclass(), checkedClasses)) {
return;
}
if (!isSuperclass
&& (!Modifier.isFinal(objectClass.getModifiers())
|| (objectClass.isMemberClass() && !Modifier.isStatic(objectClass.getModifiers())))) {
throw new ObjectIsMutableException(objectClass + " is not final and static");
}
for (Field field : objectClass.getDeclaredFields()) {
if (!Modifier.isFinal(field.getModifiers())) {
throw new ObjectIsMutableException("Field " + field.getName() + " of class " + objectClass + " is not final");
}
if (isImmutableCollection(field.getType(), field.getGenericType(), checkedClasses)) {
continue;
}
checkIfIsImmutable(field.getType(), false, checkedClasses);
}
if (objectClass.getSuperclass() != null) {
checkIfIsImmutable(objectClass.getSuperclass(), true, checkedClasses);
}
}
private static boolean isImmutableCollection(@NonNull Class<?> objectClass,
@Nullable Type genericType,
@NonNull Set<Class> checkedClasses)
throws ObjectIsMutableException {
for (Class<?> collectionClass : IMMUTABLE_COLLECTIONS_TYPES) {
if (collectionClass != objectClass && collectionClass != objectClass.getSuperclass()) {
continue;
}
if (!(genericType instanceof ParameterizedType)) {
throw new ObjectIsMutableException(objectClass + " is immutable collection but generic type " + genericType + " is not ParameterizedType");
}
for (Type parameterType : ((ParameterizedType) genericType).getActualTypeArguments()) {
if (!(parameterType instanceof Class)) {
throw new ObjectIsMutableException(objectClass + " is immutable collection but generic parameterType " + parameterType + "is not ParameterizedType");
}
checkIfIsImmutable((Class) parameterType, false, checkedClasses);
}
return true;
}
return false;
}
private ObjectUtils() {
}
}

View File

@ -0,0 +1,15 @@
package roboswag.org.core.exceptions;
import android.support.annotation.NonNull;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO: fill description
*/
public class ObjectIsMutableException extends Exception {
public ObjectIsMutableException(@NonNull String message){
super(message);
}
}

View File

@ -0,0 +1,24 @@
package roboswag.org.storable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import roboswag.org.storable.exceptions.ConversionException;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO: fill description
*/
public interface Converter<TObject, TStoreObject> {
@Nullable
TStoreObject toStoreObject(@NonNull Class<TObject> objectClass,
@NonNull Class<TStoreObject> storeObjectClass,
@Nullable TObject object) throws ConversionException;
@Nullable
TObject toObject(@NonNull Class<TObject> objectClass,
@NonNull Class<TStoreObject> storeObjectClass,
@Nullable TStoreObject storeObject) throws ConversionException;
}

View File

@ -0,0 +1,73 @@
package roboswag.org.storable;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import roboswag.org.storable.exceptions.MigrationException;
import roboswag.org.storable.exceptions.StoreException;
/**
* Created by Gavriil Sitnikov on 06/10/2015.
* TODO: fill description
*/
public class Migration<TKey> {
private final long latestVersion;
@NonNull
private final Store<TKey, Long> versionsStore;
@NonNull
private final List<Migrator<TKey, ?, ?>> migrators = new ArrayList<>();
public Migration(@NonNull Store<TKey, Long> versionsStore, long latestVersion) {
this.versionsStore = versionsStore;
this.latestVersion = latestVersion;
}
public void addMigrator(@NonNull Migrator<TKey, ?, ?> migrator) {
migrators.add(migrator);
}
public void migrateToLatestVersion(@NonNull TKey key) throws MigrationException {
Long version;
try {
version = versionsStore.loadObject(Long.class, key);
} catch (StoreException throwable) {
throw new MigrationException("Version for key " + key + " is null", throwable);
}
if (version == null) {
version = Migrator.DEFAULT_VERSION;
}
while (!version.equals(latestVersion)) {
long oldVersion = version;
boolean migrationTriggered = false;
for (Migrator<TKey, ?, ?> migrator : migrators) {
if (migrator.supportMigrationFor(version) && migrator.getOldStore().contains(key)) {
version = migrator.migrate(key, version);
migrationTriggered = true;
}
}
if (oldVersion > version) {
throw new MigrationException("Version downgraded from [" + oldVersion + "] to [" + version + "]");
} else if (oldVersion == version) {
if (migrationTriggered) {
throw new MigrationException("Migration not changed version [" + version + "]");
} else {
break;
}
} else if (version > latestVersion) {
throw new MigrationException("Version [" + version + "] is higher than latest version [" + latestVersion + "]");
}
}
try {
versionsStore.storeObject(Long.class, key, latestVersion);
} catch (StoreException throwable) {
throw new MigrationException("Storing version failed for " + key, throwable);
}
}
}

View File

@ -0,0 +1,47 @@
package roboswag.org.storable;
import android.support.annotation.NonNull;
import roboswag.org.storable.exceptions.MigrationException;
/**
* Created by Gavriil Sitnikov on 05/10/2015.
* TODO: fill description
*/
public abstract class Migrator<TKey, TOldStoreObject, TNewStoreObject> {
public static final long DEFAULT_VERSION = -1L;
@NonNull
private final Store<TKey, TOldStoreObject> oldStore;
@NonNull
private final Store<TKey, TNewStoreObject> newStore;
public Migrator(@NonNull Store<TKey, TOldStoreObject> oldStore,
@NonNull Store<TKey, TNewStoreObject> newStore) {
this.oldStore = oldStore;
this.newStore = newStore;
}
@NonNull
public Store<TKey, TOldStoreObject> getOldStore() {
return oldStore;
}
@NonNull
public Store<TKey, TNewStoreObject> getNewStore() {
return newStore;
}
public abstract boolean supportMigrationFor(long version);
public long migrate(TKey oldKey, long version) throws MigrationException {
if (!supportMigrationFor(version)) {
throw new MigrationException("Version " + version + " not supported by " + this);
}
return migrateInternal(oldKey);
}
protected abstract long migrateInternal(TKey key) throws MigrationException;
}

View File

@ -0,0 +1,295 @@
package roboswag.org.storable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import roboswag.org.core.ObjectUtils;
import roboswag.org.core.exceptions.ObjectIsMutableException;
import roboswag.org.storable.exceptions.ConversionException;
import roboswag.org.storable.exceptions.MigrationException;
import roboswag.org.storable.exceptions.StoreException;
import roboswag.org.storable.exceptions.ValidationException;
import rx.Observable;
import rx.functions.Actions;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO
*/
public abstract class Storable<TKey, TObject, TStoreObject> {
private static final String LOG_TAG = "Storable";
private static int globalLogLevel = Log.ERROR;
private static boolean isInDebugMode = false;
public static void setLogLevel(int logLevel) {
globalLogLevel = logLevel;
}
public static void setDebugMode(boolean debugMode) {
isInDebugMode = debugMode;
}
@NonNull
private final String name;
@NonNull
private final TKey key;
@NonNull
private final Class<TObject> objectClass;
@NonNull
private final Class<TStoreObject> storeObjectClass;
@NonNull
private final Store<TKey, TStoreObject> store;
@NonNull
private final Converter<TObject, TStoreObject> converter;
private final boolean cloneOnGet;
@Nullable
private final Migration<TKey> migration;
@Nullable
private final Validator<TObject> validator;
@Nullable
private final TObject defaultValue;
@NonNull
private final PublishSubject<TObject> valueSubject = PublishSubject.create();
@NonNull
private final Observable<TObject> valueObservable = Observable.<TObject>create(subscriber -> {
try {
subscriber.onNext(get());
} catch (Exception throwable) {
if (globalLogLevel <= Log.ERROR) {
Log.e(LOG_TAG, "Error during get: " + Log.getStackTraceString(throwable));
}
subscriber.onError(throwable);
}
}).subscribeOn(Schedulers.io())
.concatWith(valueSubject)
.replay(1).autoConnect();
@Nullable
private CachedValue<TStoreObject> cachedStoreDefaultValue;
@Nullable
private CachedValue<TStoreObject> cachedStoreValue;
@Nullable
private CachedValue<TObject> cachedValue;
protected Storable(@NonNull String name,
@NonNull TKey key,
@NonNull Class<TObject> objectClass,
@NonNull Class<TStoreObject> storeObjectClass,
@NonNull Store<TKey, TStoreObject> store,
@NonNull Converter<TObject, TStoreObject> converter,
boolean cloneOnGet,
@Nullable Migration<TKey> migration,
@Nullable Validator<TObject> validator,
@Nullable TObject defaultValue) {
this.name = name;
this.key = key;
this.objectClass = objectClass;
this.storeObjectClass = storeObjectClass;
this.store = store;
this.converter = converter;
this.cloneOnGet = cloneOnGet;
this.migration = migration;
this.validator = validator;
this.defaultValue = defaultValue;
if (isInDebugMode && !cloneOnGet) {
try {
ObjectUtils.checkIfIsImmutable(objectClass);
} catch (ObjectIsMutableException throwable) {
Log.w(LOG_TAG, Log.getStackTraceString(throwable));
}
}
}
@NonNull
public String getName() {
return name;
}
@NonNull
public TKey getKey() {
return key;
}
@NonNull
public Store<TKey, TStoreObject> getStore() {
return store;
}
@NonNull
public Converter<TObject, TStoreObject> getConverter() {
return converter;
}
@Nullable
public TObject getDefaultValue() {
return defaultValue;
}
@Nullable
public Validator<TObject> getValidator() {
return validator;
}
@NonNull
private CachedValue<TStoreObject> getCachedStoreDefaultValue() throws ConversionException {
if (cachedStoreDefaultValue == null) {
cachedStoreDefaultValue = new CachedValue<>(converter.toStoreObject(objectClass, storeObjectClass, defaultValue));
}
return cachedStoreDefaultValue;
}
@Nullable
private TStoreObject getStoreValue() throws StoreException, ConversionException, MigrationException {
synchronized (this) {
if (cachedStoreValue == null) {
if (migration != null) {
try {
migration.migrateToLatestVersion(key);
} catch (MigrationException throwable) {
if (isInDebugMode) {
throw throwable;
} else if (globalLogLevel <= Log.ERROR) {
Log.e(LOG_TAG, "Error during migration: " + Log.getStackTraceString(throwable));
}
}
}
TStoreObject storeObject = store.loadObject(storeObjectClass, key);
cachedStoreValue = storeObject == null && defaultValue != null
? getCachedStoreDefaultValue()
: new CachedValue<>(storeObject);
}
return cachedStoreValue.value;
}
}
@Nullable
private TObject getDirectValue() throws StoreException, ConversionException {
synchronized (this) {
if (cachedValue == null) {
TStoreObject storeObject = store.loadObject(storeObjectClass, key);
cachedValue = storeObject == null && defaultValue != null
? new CachedValue<>(defaultValue)
: new CachedValue<>(converter.toObject(objectClass, storeObjectClass, storeObject));
}
return cachedValue.value;
}
}
@Nullable
public TObject get() throws StoreException, ConversionException, MigrationException {
synchronized (this) {
if (cloneOnGet) {
TStoreObject storeValue = getStoreValue();
return storeValue != null ? converter.toObject(objectClass, storeObjectClass, storeValue) : null;
} else {
return getDirectValue();
}
}
}
private void updateCachedValue(@Nullable TObject value, @Nullable TStoreObject storeObject) throws ConversionException {
cachedValue = null;
cachedStoreValue = null;
if (cloneOnGet) {
cachedStoreValue = storeObject == null && defaultValue != null
? getCachedStoreDefaultValue()
: new CachedValue<>(storeObject);
} else {
cachedValue = storeObject == null && defaultValue != null
? new CachedValue<>(defaultValue)
: new CachedValue<>(value);
}
}
public void set(@Nullable TObject value)
throws ValidationException, ConversionException, StoreException, MigrationException {
synchronized (this) {
if (validator != null) {
validator.validate(value);
}
TObject oldValue = null;
if (!cloneOnGet && cachedValue != null) {
oldValue = cachedValue.value;
if (ObjectUtils.equals(oldValue, value)) {
return;
}
}
TStoreObject valueToStore = converter.toStoreObject(objectClass, storeObjectClass, value);
try {
TStoreObject storedValue = getStoreValue();
if (ObjectUtils.equals(storedValue, valueToStore)) {
return;
}
if (oldValue == null) {
oldValue = converter.toObject(objectClass, storeObjectClass, storedValue);
}
} catch (Exception throwable) {
// some invalid value in store
if (globalLogLevel <= Log.WARN) {
Log.w(LOG_TAG, "Can't get current store value: " + Log.getStackTraceString(throwable));
}
}
store.storeObject(storeObjectClass, key, valueToStore);
updateCachedValue(value, valueToStore);
onValueChanged(get(), oldValue);
}
}
public void setAsync(@Nullable TObject value) {
setObservable(value).subscribe(Actions.empty(), this::onSetError);
}
private void onSetError(Throwable throwable) {
if (globalLogLevel <= Log.ERROR) {
Log.e(LOG_TAG, "Error during set: " + Log.getStackTraceString(throwable));
}
}
public Observable<?> setObservable(@Nullable TObject value) {
return Observable.create(subscriber -> {
try {
set(value);
} catch (Exception throwable) {
if (globalLogLevel <= Log.ERROR) {
Log.e(LOG_TAG, "Error during set: " + Log.getStackTraceString(throwable));
}
subscriber.onError(throwable);
}
subscriber.onCompleted();
}).subscribeOn(Schedulers.io());
}
public Observable<TObject> observe() {
return valueObservable;
}
protected void onValueChanged(@Nullable TObject newValue, TObject oldValue) {
valueSubject.onNext(newValue);
if (globalLogLevel <= Log.INFO) {
Log.w(LOG_TAG, "Value changed from '" + oldValue + "' to '" + newValue + "'");
}
}
private class CachedValue<T> {
@Nullable
private final T value;
private CachedValue(@Nullable T value) {
this.value = value;
}
}
}

View File

@ -0,0 +1,23 @@
package roboswag.org.storable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import roboswag.org.storable.exceptions.StoreException;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO: fill description
*/
public interface Store<TKey, TStoreObject> {
boolean contains(@NonNull TKey key);
void storeObject(@NonNull Class<TStoreObject> storeObjectClass,
@NonNull TKey key,
@Nullable TStoreObject storeObject) throws StoreException;
@Nullable
TStoreObject loadObject(@NonNull Class<TStoreObject> storeObjectClass, @NonNull TKey key) throws StoreException;
}

View File

@ -0,0 +1,15 @@
package roboswag.org.storable;
import android.support.annotation.Nullable;
import roboswag.org.storable.exceptions.ValidationException;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO: fill description
*/
public interface Validator<T> {
void validate(@Nullable T value) throws ValidationException;
}

View File

@ -0,0 +1,19 @@
package roboswag.org.storable.exceptions;
import android.support.annotation.NonNull;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO: fill description
*/
public class ConversionException extends Exception {
public ConversionException(@NonNull String message) {
super(message);
}
public ConversionException(@NonNull String message, @NonNull Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,19 @@
package roboswag.org.storable.exceptions;
import android.support.annotation.NonNull;
/**
* Created by Gavriil Sitnikov on 05/10/2015.
* TODO: fill description
*/
public class MigrationException extends Exception {
public MigrationException(@NonNull String message) {
super(message);
}
public MigrationException(@NonNull String message, @NonNull Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,19 @@
package roboswag.org.storable.exceptions;
import android.support.annotation.NonNull;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO: fill description
*/
public class StoreException extends Exception {
public StoreException(@NonNull String message) {
super(message);
}
public StoreException(@NonNull String message, @NonNull Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,19 @@
package roboswag.org.storable.exceptions;
import android.support.annotation.NonNull;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* TODO: fill description
*/
public class ValidationException extends Exception {
public ValidationException(@NonNull String message) {
super(message);
}
public ValidationException(@NonNull String message, @NonNull Throwable throwable) {
super(message, throwable);
}
}

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Storable</string>
</resources>

View File

@ -0,0 +1,15 @@
package roboswag.org.storable;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* To work on unit tests, switch the Test Artifact in the Build Variants view.
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}