diff --git a/mockrealm/src/main/java/info/juanmendez/mockrealm/decorators/RealmModelDecorator.java b/mockrealm/src/main/java/info/juanmendez/mockrealm/decorators/RealmModelDecorator.java index 48949fb..b0d92ed 100644 --- a/mockrealm/src/main/java/info/juanmendez/mockrealm/decorators/RealmModelDecorator.java +++ b/mockrealm/src/main/java/info/juanmendez/mockrealm/decorators/RealmModelDecorator.java @@ -80,6 +80,7 @@ public static RealmModel decorate(RealmModel realmModel ){ if( realmModel instanceof RealmObject ){ RealmObjectDecorator.handleDeleteActions( (RealmObject) realmModel); RealmObjectDecorator.handleAsyncMethods( (RealmObject) realmModel); + setValid( realmModel, true ); } return realmModel; diff --git a/mockrealm/src/main/res/values/strings.xml b/mockrealm/src/main/res/values/strings.xml index 49fc91e..ac631f2 100644 --- a/mockrealm/src/main/res/values/strings.xml +++ b/mockrealm/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - library + Mocking Realm diff --git a/proguard-rules.pro b/proguard-rules.pro new file mode 100644 index 0000000..63c7067 --- /dev/null +++ b/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Users\musta\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0507c52 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/main/java/info/juanmendez/mockrealm/MockRealm.java b/src/main/java/info/juanmendez/mockrealm/MockRealm.java new file mode 100644 index 0000000..e7a7bff --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/MockRealm.java @@ -0,0 +1,62 @@ +package info.juanmendez.mockrealm; + +import info.juanmendez.mockrealm.decorators.RealmConfigurationDecorator; +import info.juanmendez.mockrealm.decorators.RealmDecorator; +import info.juanmendez.mockrealm.decorators.RealmListDecorator; +import info.juanmendez.mockrealm.decorators.RealmModelDecorator; +import info.juanmendez.mockrealm.decorators.RealmObjectDecorator; +import info.juanmendez.mockrealm.dependencies.RealmStorage; +import info.juanmendez.mockrealm.models.RealmAnnotation; +import io.realm.Realm; +import io.realm.RealmConfiguration; +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.RealmQuery; +import io.realm.RealmResults; + +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +/** + * Created by @juanmendezinfo on 2/15/2017. + */ +public class MockRealm { + + /** + * This is a method required in order to start up + * testing. + * @throws Exception + */ + public static void prepare() throws Exception { + mockStatic( RealmList.class ); + mockStatic( Realm.class ); + mockStatic( RealmConfiguration.class); + mockStatic( RealmQuery.class ); + mockStatic( RealmResults.class ); + mockStatic( RealmObject.class ); + + RealmListDecorator.prepare(); + RealmModelDecorator.prepare(); + RealmObjectDecorator.prepare(); + RealmDecorator.prepare(); + RealmConfigurationDecorator.prepare(); + } + + /** + * Make sure to include each of your class annotation references through RealmAnnotation + * before testing each type of realmModel in your project + * @param annotations + */ + public static void addAnnotations(RealmAnnotation ... annotations ){ + for( RealmAnnotation annotation: annotations ){ + RealmStorage.addAnnotations( annotation ); + } + } + + /** + * Call this method each time you want to clear your realm entries; + * specially, when starting a new test. + */ + public static void clearData(){ + RealmStorage.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/decorators/RealmConfigurationDecorator.java b/src/main/java/info/juanmendez/mockrealm/decorators/RealmConfigurationDecorator.java new file mode 100644 index 0000000..7c10745 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/decorators/RealmConfigurationDecorator.java @@ -0,0 +1,51 @@ +package info.juanmendez.mockrealm.decorators; + +import java.io.File; + +import io.realm.RealmConfiguration; +import io.realm.RealmMigration; +import io.realm.rx.RxObservableFactory; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.anyVararg; +import static org.powermock.api.mockito.PowerMockito.doAnswer; +import static org.powermock.api.mockito.PowerMockito.doReturn; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +/** + * Created by Juan Mendez on 2/23/2017. + * www.juanmendez.info + * contact@juanmendez.info + */ + +public class RealmConfigurationDecorator { + + + public static void prepare() throws Exception { + + RealmConfiguration mockRealmConfig = mock(RealmConfiguration.class); + + // make a mockery of our inner class + RealmConfiguration.Builder mockedBuilder = mock(RealmConfiguration.Builder.class); + + // magically return the mock when a new instance is required + whenNew(RealmConfiguration.Builder.class).withNoArguments().thenAnswer(invocation -> mockedBuilder); + whenNew(RealmConfiguration.Builder.class).withAnyArguments().thenAnswer(invocation -> mockedBuilder); + doAnswer(invocation -> mockRealmConfig ).when(mockedBuilder ).build(); + + //what to do with builder configs + doReturn(mockedBuilder).when( mockedBuilder ).name(anyString()); + doReturn(mockedBuilder).when( mockedBuilder ).directory(any(File.class)); + doReturn(mockedBuilder).when( mockedBuilder ).encryptionKey(any()); + doReturn(mockedBuilder).when( mockedBuilder ).schemaVersion(anyLong()); + doReturn(mockedBuilder).when( mockedBuilder ).migration(any(RealmMigration.class)); + doReturn(mockedBuilder).when( mockedBuilder ).deleteRealmIfMigrationNeeded(); + doReturn(mockedBuilder).when( mockedBuilder ).inMemory(); + doReturn(mockedBuilder).when( mockedBuilder ).modules(any(), anyVararg()); + doReturn(mockedBuilder).when( mockedBuilder ).rxFactory(any(RxObservableFactory.class)); + doReturn(mockedBuilder).when( mockedBuilder ).assetFile(anyString()); + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/decorators/RealmDecorator.java b/src/main/java/info/juanmendez/mockrealm/decorators/RealmDecorator.java new file mode 100644 index 0000000..d22e4a0 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/decorators/RealmDecorator.java @@ -0,0 +1,383 @@ +package info.juanmendez.mockrealm.decorators; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.HashMap; +import java.util.concurrent.Callable; + +import info.juanmendez.mockrealm.dependencies.Compare; +import info.juanmendez.mockrealm.dependencies.RealmMatchers; +import info.juanmendez.mockrealm.dependencies.RealmStorage; +import info.juanmendez.mockrealm.dependencies.TransactionObservable; +import info.juanmendez.mockrealm.models.Query; +import info.juanmendez.mockrealm.models.TransactionEvent; +import info.juanmendez.mockrealm.utils.QueryTracker; +import info.juanmendez.mockrealm.utils.RealmModelUtil; +import io.realm.Realm; +import io.realm.RealmAsyncTask; +import io.realm.RealmConfiguration; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.RealmQuery; +import rx.Observable; +import rx.Scheduler; +import rx.functions.Func0; +import rx.schedulers.Schedulers; + +import static org.mockito.Matchers.any; +import static org.powermock.api.mockito.PowerMockito.doAnswer; +import static org.powermock.api.mockito.PowerMockito.doNothing; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Created by @juanmendezinfo on 2/15/2017. + */ +public class RealmDecorator { + + /** + * Only stick to Schedulers.immediate() for now. I tried others but they don't seem to work well + * in Robolectric + */ + private static Scheduler observerScheduler = Schedulers.immediate(); + private static Scheduler subscriberScheduler = Schedulers.immediate(); + + public static Realm prepare() throws Exception { + + Realm realm = mock(Realm.class ); + prepare(realm); + handleAsyncTransactions(realm); + handleSyncTransactions(realm); + return realm; + } + + public static Scheduler getTransactionScheduler() { + return observerScheduler; + } + + public static Scheduler getResponseScheduler() { + return subscriberScheduler; + } + + private static void prepare(Realm realm) throws Exception { + + doNothing().when( Realm.class, "init", any()); + + when( Realm.deleteRealm( any(RealmConfiguration.class))).thenReturn( true ); + + HashMap> realmMap = RealmStorage.getRealmMap(); + + when(Realm.getDefaultInstance()).thenReturn(realm); + + when( Realm.deleteRealm(any(RealmConfiguration.class))).thenAnswer(invocation -> { + RealmStorage.clear(); + return null; + }); + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + return null; + } + }).when(Realm.class, "setDefaultConfiguration", any() ); + + when( realm.createObject( Mockito.argThat(new RealmMatchers.ClassMatcher<>(RealmModel.class)) ) ).thenAnswer(invocation -> { + Class clazz = (Class) invocation.getArguments()[0]; + + if( !realmMap.containsKey(clazz)){ + realmMap.put(clazz, RealmListDecorator.create()); + } + + RealmModel realmModel = RealmModelDecorator.create(clazz, true); + RealmStorage.addModel( realmModel ); + + return realmModel; + }); + + //realm.copyToRealm( realmModel ) + when( realm.copyToRealm(Mockito.any( RealmModel.class ))).thenAnswer( new Answer(){ + + @Override + public RealmModel answer(InvocationOnMock invocationOnMock) throws Throwable { + + RealmModel newRealmModel = (RealmModel) invocationOnMock.getArguments()[0]; + return createOrUpdate( newRealmModel ); + } + }); + + //realm.copyToRealmOrUpdate( realmModel ) same as realm.copyToRealm( realmModel ) + when( realm.copyToRealmOrUpdate(Mockito.any( RealmModel.class ))).thenAnswer( new Answer(){ + + @Override + public RealmModel answer(InvocationOnMock invocationOnMock) throws Throwable { + + RealmModel newRealmModel = (RealmModel) invocationOnMock.getArguments()[0]; + return createOrUpdate( newRealmModel ); + } + }); + + when( realm.where( Mockito.argThat( new RealmMatchers.ClassMatcher<>(RealmModel.class)) ) ).then(new Answer(){ + + + @Override + public RealmQuery answer(InvocationOnMock invocationOnMock) throws Throwable { + + //clear list being queried + Class clazz = (Class) invocationOnMock.getArguments()[0]; + QueryTracker queryTracker = new QueryTracker(clazz); + + RealmQuery realmQuery = RealmQueryDecorator.create(queryTracker); + + if( !realmMap.containsKey(clazz)) + { + realmMap.put(clazz, new RealmList<>()); + } + + queryTracker.appendQuery( Query.build().setCondition(Compare.startTopGroup).setArgs(new Object[]{realmMap.get(clazz)}) ); + + + return realmQuery; + } + }); + } + + private static RealmModel createOrUpdate( RealmModel newRealmModel ){ + HashMap> realmMap = RealmStorage.getRealmMap(); + Class clazz = RealmModelUtil.getClass(newRealmModel); + + if( !realmMap.containsKey(clazz)){ + realmMap.put(clazz, RealmListDecorator.create()); + } + + RealmModel updatedRealmModel = RealmModelUtil.tryToUpdate( newRealmModel ); + + if( updatedRealmModel != null ){ + return updatedRealmModel; + } + + newRealmModel = RealmModelDecorator.decorate( newRealmModel ); + RealmStorage.addModel( newRealmModel ); + return newRealmModel; + } + + private static void handleAsyncTransactions(Realm realm ){ + + //call execute() in Realm.Transaction object received. + doAnswer( new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + + if( invocation.getArguments().length > 0 ){ + Realm.Transaction transaction = (Realm.Transaction) invocation.getArguments()[0]; + + queueTransaction(() -> { + transaction.execute( realm ); + return null; + }); + } + return null; + } + }).when( realm ).executeTransaction(any( Realm.Transaction.class )); + + doAnswer( new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + + if( invocation.getArguments().length > 0 ){ + + queueTransaction( () -> { + Observable.fromCallable(new Callable() { + @Override + public Void call() throws Exception { + Realm.Transaction transaction = (Realm.Transaction) invocation.getArguments()[0]; + transaction.execute( realm ); + return null; + } + }) + .subscribeOn(getTransactionScheduler()) + .observeOn( getResponseScheduler() ).subscribe(aVoid -> {}); + + return null; + }); + + } + + return null; + } + }).when( realm ).executeTransactionAsync(any( Realm.Transaction.class )); + + + when( realm.executeTransactionAsync(any( Realm.Transaction.class ), any( Realm.Transaction.OnSuccess.class )) ).thenAnswer( + new Answer() { + + @Override + public RealmAsyncTask answer(InvocationOnMock invocation) throws Throwable { + + Realm.Transaction transaction = (Realm.Transaction) invocation.getArguments()[0]; + + return queueTransaction(() -> { + + Observable.fromCallable(new Callable() { + @Override + public Boolean call() throws Exception { + if( invocation.getArguments().length >=1 ){ + transaction.execute( realm ); + return true; + } + return false; + } + }) + .subscribeOn(getTransactionScheduler()) + .observeOn( getResponseScheduler() ) + .subscribe(aBoolean -> { + if( aBoolean && invocation.getArguments().length >=2 ){ + Realm.Transaction.OnSuccess onSuccess = (Realm.Transaction.OnSuccess) invocation.getArguments()[1]; + onSuccess.onSuccess(); + } + }); + + return null; + }); + } + } + ); + + + when( realm.executeTransactionAsync(any( Realm.Transaction.class ), any( Realm.Transaction.OnSuccess.class ), any(Realm.Transaction.OnError.class)) ).thenAnswer( + new Answer() { + + @Override + public RealmAsyncTask answer(InvocationOnMock invocation) throws Throwable { + + return queueTransaction(() -> { + Observable.fromCallable(new Callable() { + @Override + public Boolean call() throws Exception { + if( invocation.getArguments().length >=1 ){ + Realm.Transaction transaction = (Realm.Transaction) invocation.getArguments()[0]; + transaction.execute(realm); + return true; + } + + return false; + } + }) + .subscribeOn(observerScheduler) + .observeOn( subscriberScheduler ) + .subscribe(aBoolean -> { + if( aBoolean && invocation.getArguments().length >=2 ){ + Realm.Transaction.OnSuccess onSuccess = (Realm.Transaction.OnSuccess) invocation.getArguments()[1]; + onSuccess.onSuccess(); + } + + }, throwable -> { + + if( invocation.getArguments().length >=3 ){ + Realm.Transaction.OnError onError = (Realm.Transaction.OnError) invocation.getArguments()[2]; + onError.onError(throwable); + } + + }); + + return null; + }); + } + } + ); + + + when( realm.executeTransactionAsync(any( Realm.Transaction.class ), any(Realm.Transaction.OnError.class)) ).thenAnswer( + new Answer() { + + @Override + public RealmAsyncTask answer(InvocationOnMock invocation) throws Throwable { + Realm.Transaction transaction = (Realm.Transaction) invocation.getArguments()[0]; + + return queueTransaction(() -> { + + Observable.fromCallable(new Callable() { + @Override + public Boolean call() throws Exception { + if( invocation.getArguments().length >=1 ){ + + + return true; + } + + return false; + } + }) + .subscribeOn(observerScheduler) + .observeOn( subscriberScheduler ) + .subscribe(aBoolean -> { + if( aBoolean && invocation.getArguments().length >=2 ){ + Realm.Transaction.OnSuccess onSuccess = (Realm.Transaction.OnSuccess) invocation.getArguments()[1]; + onSuccess.onSuccess(); + } + + }, throwable -> { + + if( invocation.getArguments().length >=2 ){ + Realm.Transaction.OnError onError = (Realm.Transaction.OnError) invocation.getArguments()[2]; + onError.onError(throwable); + } + + }); + + return null; + }); + } + } + ); + } + + private static void handleSyncTransactions(Realm realm ){ + + TransactionObservable.KeyTransaction transaction = new TransactionObservable.KeyTransaction( realm.toString() ); + + doAnswer(invocation -> { + TransactionObservable.startRequest(transaction); + return null; + }).when( realm ).beginTransaction(); + + + doAnswer(invocation -> { + TransactionObservable.endRequest(transaction); + return null; + }).when( realm ).commitTransaction(); + } + + /** + * Parameter funk serves as a key to TransactionObservable.startRequest() + * @param funk code to execute when TransactionObservable allows it. + * @return RealmAsyncTask uses funk key to cancel transaction, or check if it has been canceled + */ + private static RealmAsyncTask queueTransaction(Func0 funk){ + + TransactionObservable.startRequest(funk, + TransactionObservable.asObservable() + .filter(transactionEvent -> { + return transactionEvent.getState()== TransactionEvent.START_TRANSACTION && transactionEvent.getTarget() == funk; + }) + .subscribe(o -> { + funk.call(); + TransactionObservable.endRequest(funk); + }) + ); + + + return new RealmAsyncTask() { + @Override + public void cancel() { + TransactionObservable.cancel(funk); + } + + @Override + public boolean isCancelled() { + return TransactionObservable.isCanceled( funk ); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/decorators/RealmListDecorator.java b/src/main/java/info/juanmendez/mockrealm/decorators/RealmListDecorator.java new file mode 100644 index 0000000..e67379a --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/decorators/RealmListDecorator.java @@ -0,0 +1,41 @@ +package info.juanmendez.mockrealm.decorators; + +import info.juanmendez.mockrealm.models.RealmListStubbed; +import io.realm.RealmList; +import io.realm.RealmModel; + +import static org.mockito.Matchers.anyVararg; +import static org.powermock.api.mockito.PowerMockito.spy; +import static org.powermock.api.mockito.PowerMockito.whenNew; + + +/** + * Created by Juan Mendez on 2/25/2017. + * www.juanmendez.info + * contact@juanmendez.info + */ +public class RealmListDecorator { + + public static void prepare() throws Exception { + + spy(RealmList.class); + + whenNew( RealmList.class ).withArguments(anyVararg()).thenAnswer(invocation -> { + + RealmList realmList = create(); + Object[] args = invocation.getArguments(); + + for (Object arg:args) { + realmList.add((RealmModel)arg); + } + + return realmList; + }); + } + + public static RealmList create(){ + + return new RealmListStubbed(); + } + +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/decorators/RealmModelDecorator.java b/src/main/java/info/juanmendez/mockrealm/decorators/RealmModelDecorator.java new file mode 100644 index 0000000..48949fb --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/decorators/RealmModelDecorator.java @@ -0,0 +1,196 @@ +package info.juanmendez.mockrealm.decorators; + +import org.powermock.reflect.Whitebox; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.Set; + +import info.juanmendez.mockrealm.dependencies.RealmObservable; +import info.juanmendez.mockrealm.dependencies.RealmStorage; +import info.juanmendez.mockrealm.models.RealmEvent; +import info.juanmendez.mockrealm.utils.RealmModelUtil; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.RealmObject; + +import static org.powermock.api.mockito.PowerMockito.doReturn; +import static org.powermock.api.mockito.PowerMockito.spy; + +/** + * Created by Juan Mendez on 2/24/2017. + * www.juanmendez.info + * contact@juanmendez.info + */ + +public class RealmModelDecorator { + + public static void prepare(){ + } + + private static RealmModel createFromClass( Class clazz ){ + + Constructor constructor = null; + RealmModel realmModel = null; + + try { + constructor = clazz.getConstructor(); + realmModel = (RealmModel) constructor.newInstance(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + }catch (NoSuchMethodException e) { + e.printStackTrace(); + } + + return realmModel; + } + + public static RealmModel create(Class clazz, Boolean valid ) { + RealmModel realmModel = createFromClass( clazz ); + + if( realmModel instanceof RealmObject){ + realmModel = RealmModelDecorator.decorate( realmModel ); + } + + setValid( realmModel, valid ); + setLoaded( realmModel, valid ); + return realmModel; + } + + public static RealmModel decorate(RealmModel realmModel ){ + + Class clazz = RealmModelUtil.getClass( realmModel ); + + //only decorate new realmModels + if(RealmStorage.getRealmMap().get(clazz).contains( realmModel)){ + return realmModel; + } + + if( realmModel instanceof RealmObject ){ + realmModel = spy( realmModel ); + } + + startDeleteObservers( realmModel); + + if( realmModel instanceof RealmObject ){ + RealmObjectDecorator.handleDeleteActions( (RealmObject) realmModel); + RealmObjectDecorator.handleAsyncMethods( (RealmObject) realmModel); + } + + return realmModel; + } + + /** + * Through RealmObservable be notified of changes, and see if any other realmModel deleted + * is referenced by this realmModel and remove such reference. + * @param realmModel + */ + private static void startDeleteObservers(RealmModel realmModel ){ + + Set fieldSet = Whitebox.getAllInstanceFields(realmModel); + Class fieldClass; + + /** + * There is one observable per each member observed either it's a realmModel or a realmResult + */ + for (Field field: fieldSet) { + + fieldClass = field.getType(); + + if( RealmModel.class.isAssignableFrom(fieldClass) ){ + + RealmModel finalRealmModel = realmModel; + RealmObservable.add( realmModel, + + RealmObservable.asObservable() + .filter(realmEvent -> realmEvent.getState()== RealmEvent.MODEL_REMOVED) + .map(realmEvent -> realmEvent.getRealmModel()) + .ofType(fieldClass) + .subscribe( o -> { + Object variable = Whitebox.getInternalState(finalRealmModel, + field.getName()); + + if( variable != null && variable == o ){ + Whitebox.setInternalState(finalRealmModel, + field.getName(), (Object[]) null); + } + }) + ); + } + else if( fieldClass == RealmList.class ){ + + //RealmResults are not filtered + + RealmObservable.add( realmModel, + + RealmObservable.asObservable() + .filter(realmEvent -> realmEvent.getState() == RealmEvent.MODEL_REMOVED) + .map(realmEvent -> realmEvent.getRealmModel()) + .subscribe(o -> { + RealmList realmList = (RealmList) Whitebox.getInternalState(realmModel, field.getName()); + + if( realmList != null ){ + while( realmList.contains( o ) ){ + realmList.remove( o ); + } + } + }) + ); + } + } + } + + /** + * mark either realmModel or realmObject as valid or not + * @param realmModel + * @param flag + */ + public static void setValid(RealmModel realmModel, Boolean flag ){ + + if( realmModel instanceof RealmObject ){ + doReturn( flag ).when( (RealmObject) realmModel ).isValid(); + } + else { + + try { + doReturn( flag ).when( RealmObject.class, "isValid", realmModel ); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * mark either realmModel or realmObject as loaded or not + * @param realmModel + * @param flag + */ + public static void setLoaded(RealmModel realmModel, Boolean flag ){ + + if( realmModel instanceof RealmObject ){ + doReturn( flag ).when( ((RealmObject) realmModel) ).isLoaded(); + } + else { + + try { + doReturn( flag ).when( RealmObject.class, "isLoaded", realmModel ); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public static void deleteRealmModel( RealmModel realmModel ){ + if( realmModel instanceof RealmObject){ + ((RealmObject) realmModel ).deleteFromRealm(); + } + else{ + RealmObject.deleteFromRealm( realmModel ); + } + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/decorators/RealmObjectDecorator.java b/src/main/java/info/juanmendez/mockrealm/decorators/RealmObjectDecorator.java new file mode 100644 index 0000000..66123df --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/decorators/RealmObjectDecorator.java @@ -0,0 +1,267 @@ +package info.juanmendez.mockrealm.decorators; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.reflect.Whitebox; + +import java.lang.reflect.Field; +import java.util.Set; +import java.util.concurrent.Callable; + +import info.juanmendez.mockrealm.dependencies.RealmObservable; +import info.juanmendez.mockrealm.dependencies.RealmStorage; +import info.juanmendez.mockrealm.dependencies.TransactionObservable; +import info.juanmendez.mockrealm.models.TransactionEvent; +import info.juanmendez.mockrealm.utils.QueryTracker; +import info.juanmendez.mockrealm.utils.RealmModelUtil; +import info.juanmendez.mockrealm.utils.SubscriptionsUtil; +import io.realm.RealmChangeListener; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.RealmObject; +import io.realm.RealmResults; +import io.realm.exceptions.RealmException; +import rx.Observable; +import rx.subjects.BehaviorSubject; + +import static org.mockito.Matchers.any; +import static org.powermock.api.mockito.PowerMockito.doAnswer; + +/** + * Created by Juan Mendez on 3/19/2017. + * www.juanmendez.info + * contact@juanmendez.info + */ + +public class RealmObjectDecorator { + + private static SubscriptionsUtil subscriptionsUtil = new SubscriptionsUtil<>(); + + public static void prepare(){ + handleClassActions(); + } + + static void handleClassActions(){ + + try { + + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + + RealmModel realmModel = (RealmModel) invocation.getArguments()[0]; + RealmStorage.removeModel( realmModel ); + return null; + } + }).when( RealmObject.class, "deleteFromRealm", any( RealmModel.class ) ); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + static void handleDeleteActions(RealmObject realmObject){ + + //when deleting then also make all subscriptions be unsubscribed + doAnswer(new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + RealmObservable.unsubcribe( realmObject ); + + Set fieldSet = Whitebox.getAllInstanceFields(realmObject); + + for (Field field: fieldSet) { + + if( field.getType() == RealmList.class ){ + + RealmList list = (RealmList) Whitebox.getInternalState(realmObject, field.getName()); + + if( list != null ) + list.clear(); + } + } + + RealmObservable.unsubcribe( realmObject ); + RealmStorage.removeModel( realmObject ); + + //after deleting item, lets make it invalid + RealmModelDecorator.setValid( realmObject, false ); + RealmModelDecorator.setLoaded( realmObject, false ); + return null; + } + }).when( (RealmObject) realmObject ).deleteFromRealm(); + } + + + private void deleteModel( RealmModel realmModel ){ + + RealmObservable.unsubcribe( realmModel ); + + Set fieldSet = Whitebox.getAllInstanceFields(realmModel); + + for (Field field: fieldSet) { + + if( field.getType() == RealmList.class ){ + + RealmList list = (RealmList) Whitebox.getInternalState(realmModel, field.getName()); + + if( list != null ) + list.clear(); + } + } + + RealmObservable.unsubcribe( realmModel ); + RealmStorage.removeModel( realmModel ); + + //after deleting item, lets make it invalid + RealmModelDecorator.setValid( realmModel, false ); + RealmModelDecorator.setLoaded( realmModel, false ); + } + + /** + * avoid doing anything in case realmObject is part of a realmResult + * @param realmObject + */ + public static void handleAsyncMethods( RealmObject realmObject ){ + + doAnswer(invocation -> {throw new RealmException("Synchronous realmModel cannot be reached with a changeListener");}).when( realmObject ).addChangeListener( any(RealmChangeListener.class)); + doAnswer(invocation -> null).when( realmObject).removeChangeListener(any(RealmChangeListener.class)); + doAnswer(invocation -> null).when( realmObject).removeChangeListeners(); + doAnswer( invocation ->{throw new RealmException("Synchronous realmModel cannot be reached with a changeListener");} ).when( realmObject ).asObservable(); + } + + public static void handleAsyncMethods(RealmObject realmObject, QueryTracker queryTracker ){ + + doAnswer(invocation -> { + + //execute query once associated + RealmChangeListener listener = (RealmChangeListener) invocation.getArguments()[0]; + Observable.fromCallable(new Callable>() { + @Override + public RealmResults call() throws Exception { + + return queryTracker.rewind(); + } + }).subscribeOn(RealmDecorator.getTransactionScheduler()) + .observeOn( RealmDecorator.getResponseScheduler() ) + .subscribe(realmResults -> { + + if( !realmResults.isEmpty()) + listener.onChange( realmResults.get(0) ); + else + listener.onChange( null ); + + }); + + //whenever there is a transaction ending, we compare previous result with current one. + //we transform both results as json objects and just do a check if strings are not the same + subscriptionsUtil.add( realmObject, + listener, + TransactionObservable.asObservable() + .subscribe( transactionEvent -> { + + if( transactionEvent.getState() == TransactionEvent.END_TRANSACTION ){ + + String initialJson = "", currrentJson = ""; + + RealmResults realmResults = queryTracker.getRealmResults(); + + if( !realmResults.isEmpty() ){ + initialJson = RealmModelUtil.getState( realmResults.get(0) ); + } + + realmResults = queryTracker.rewind(); + + if( !realmResults.isEmpty() ){ + currrentJson = RealmModelUtil.getState( realmResults.get(0) ); + } + + if( !initialJson.equals( currrentJson )){ + + if( !realmResults.isEmpty() ) + listener.onChange( realmResults.get(0) ); + else + listener.onChange( null ); + } + } + }) + ); + + return null; + }).when( realmObject ).addChangeListener(any(RealmChangeListener.class)); + + + doAnswer(invocation -> { + RealmChangeListener listener = (RealmChangeListener) invocation.getArguments()[0]; + subscriptionsUtil.remove(listener); + return null; + }).when( realmObject ).removeChangeListener( any(RealmChangeListener.class)); + + + doAnswer(invocation -> { + subscriptionsUtil.removeAll( realmObject ); + return null; + }).when( realmObject ).removeChangeListeners(); + + + doAnswer(invocation -> { + BehaviorSubject subject = BehaviorSubject.create(); + + subject.subscribeOn(RealmDecorator.getTransactionScheduler()) + .observeOn( RealmDecorator.getResponseScheduler() ); + + //first time make a call! + Observable.fromCallable(new Callable() { + @Override + public RealmModel call() throws Exception { + + RealmResults realmResults = queryTracker.rewind(); + + if( !realmResults.isEmpty()) + return realmResults.get(0); + else + return realmObject; + } + }).subscribeOn(RealmDecorator.getTransactionScheduler()) + .observeOn( RealmDecorator.getResponseScheduler() ) + .subscribe(realmModel -> { + subject.onNext( realmModel); + }); + + TransactionObservable.asObservable() + .subscribe(transactionEvent -> { + + if( transactionEvent.getState() == TransactionEvent.END_TRANSACTION ){ + String initialJson = "", currrentJson = ""; + + RealmResults realmResults = queryTracker.getRealmResults(); + + if( !realmResults.isEmpty() ){ + initialJson = RealmModelUtil.getState( realmResults.get(0) ); + } + + realmResults = queryTracker.rewind(); + + if( !realmResults.isEmpty() ){ + currrentJson = RealmModelUtil.getState( realmResults.get(0) ); + } + + if( !initialJson.equals( currrentJson )){ + + if( !realmResults.isEmpty() ) + subject.onNext( realmResults.get(0) ); + else + subject.onNext( realmObject ); + } + } + }); + + return subject.asObservable(); + }).when( realmObject ).asObservable(); + + } + + public static void removeSubscriptions(){ + subscriptionsUtil.removeAll(); + } +} diff --git a/src/main/java/info/juanmendez/mockrealm/decorators/RealmQueryDecorator.java b/src/main/java/info/juanmendez/mockrealm/decorators/RealmQueryDecorator.java new file mode 100644 index 0000000..05d31b3 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/decorators/RealmQueryDecorator.java @@ -0,0 +1,509 @@ +package info.juanmendez.mockrealm.decorators; + +import org.mockito.internal.util.reflection.Whitebox; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +import java.util.Date; + +import info.juanmendez.mockrealm.dependencies.Compare; +import info.juanmendez.mockrealm.models.Query; +import info.juanmendez.mockrealm.utils.QuerySort; +import info.juanmendez.mockrealm.utils.QueryTracker; +import io.realm.Case; +import io.realm.RealmModel; +import io.realm.RealmObject; +import io.realm.RealmQuery; +import io.realm.RealmResults; +import io.realm.Sort; +import io.realm.exceptions.RealmException; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyByte; +import static org.mockito.Matchers.anyDouble; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyShort; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.anyVararg; +import static org.powermock.api.mockito.PowerMockito.doAnswer; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Created by @juanmendezinfo on 2/15/2017. + */ +public class RealmQueryDecorator { + + //collections queried keyed by immediate class + + public static RealmQuery create( QueryTracker queryTracker){ + + RealmQuery realmQuery = queryTracker.getRealmQuery(); + + when( realmQuery.toString() ).thenReturn( "Realm:" + queryTracker.getClazz() ); + Whitebox.setInternalState( realmQuery, "clazz", queryTracker.getClazz()); + + handleCollectionMethods(queryTracker); + handleGroupingQueries(queryTracker); + handleMathMethods(queryTracker); + handleSearchMethods(queryTracker); + handleSortingMethods(queryTracker); + handleDistinct( queryTracker ); + + return realmQuery; + } + + private static void handleCollectionMethods(QueryTracker queryTracker) { + RealmQuery realmQuery = queryTracker.getRealmQuery(); + + when( realmQuery.findAll() ).thenAnswer(invocation ->{ + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + return queryTracker.rewind(); + }); + + when( realmQuery.findAllAsync() ).thenAnswer(invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + return queryTracker.getRealmResults(); + }); + + when( realmQuery.findFirst()).thenAnswer(invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + RealmResults realmResults = queryTracker.rewind(); + + if( realmResults.isEmpty() ) + return null; + else + return realmResults.get(0); + }); + + when( realmQuery.findFirstAsync() ).thenAnswer(invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + RealmObject realmObject = (RealmObject) RealmModelDecorator.create( queryTracker.getClazz(), false ); + RealmObjectDecorator.handleAsyncMethods( realmObject, queryTracker ); + return realmObject; + }); + } + + private static void handleGroupingQueries(QueryTracker queryTracker) { + + RealmQuery realmQuery = queryTracker.getRealmQuery(); + when( realmQuery.or()).then( invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.or)); + return realmQuery; + }); + + when( realmQuery.beginGroup()).then( invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.startGroup)); + return realmQuery; + }); + + + when( realmQuery.endGroup()).then( invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.endGroup)); + return realmQuery; + }); + + + when( realmQuery.not() ).thenAnswer( invocation -> { + queryTracker.appendQuery(Query.build().setCondition(Compare.not)); + return realmQuery; + }); + } + + private static void handleMathMethods( QueryTracker queryTracker){ + + RealmQuery realmQuery = queryTracker.getRealmQuery(); + + when( realmQuery.count() ).thenAnswer(new Answer() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + RealmResults realmResults = queryTracker.rewind(); + return realmResults.size(); + } + }); + + when( realmQuery.sum( anyString()) ).thenAnswer(new Answer() { + @Override + public Number answer(InvocationOnMock invocation) throws Throwable { + + if( invocation.getArguments().length >= 1 ){ + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + RealmResults realmResults = queryTracker.rewind(); + + String fieldName = (String) invocation.getArguments()[0]; + return realmResults.sum( fieldName ); + } + + return null; + } + }); + + when( realmQuery.average(anyString()) ).thenAnswer(new Answer() { + @Override + public Number answer(InvocationOnMock invocation) throws Throwable { + if( invocation.getArguments().length >= 1 ){ + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + RealmResults realmResults = queryTracker.rewind(); + + String fieldName = (String) invocation.getArguments()[0]; + return realmResults.average(fieldName); + } + + return null; + } + }); + + + when( realmQuery.max(anyString()) ).thenAnswer(new Answer() { + @Override + public Number answer(InvocationOnMock invocation) throws Throwable { + if( invocation.getArguments().length >= 1 ){ + + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + RealmResults realmResults = queryTracker.rewind(); + + String fieldName = (String) invocation.getArguments()[0]; + return realmResults.max(fieldName); + } + return null; + } + }); + + when( realmQuery.min(anyString()) ).thenAnswer(new Answer() { + @Override + public Number answer(InvocationOnMock invocation) throws Throwable { + if( invocation.getArguments().length >= 1 ){ + + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + RealmResults realmResults = queryTracker.rewind(); + + String fieldName = (String) invocation.getArguments()[0]; + return realmResults.min(fieldName); + } + return null; + } + }); + + when( realmQuery.isNull(anyString())).thenAnswer( invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.isNull).setArgs(invocation.getArguments())); + return realmQuery; + }); + + when( realmQuery.isNotNull(anyString())).thenAnswer(invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.not)); + queryTracker.appendQuery( Query.build().setCondition(Compare.isNull).setArgs(invocation.getArguments())); + return realmQuery; + }); + } + + private static void handleSearchMethods( QueryTracker queryTracker){ + RealmQuery realmQuery = queryTracker.getRealmQuery(); + + when( realmQuery.lessThan( any(), anyInt() ) ).thenAnswer( createComparison(queryTracker, Compare.less ) ); + when( realmQuery.lessThan( anyString(), anyByte()) ).thenAnswer( createComparison(queryTracker, Compare.less ) ); + when( realmQuery.lessThan( anyString(), anyDouble() ) ).thenAnswer( createComparison(queryTracker, Compare.less ) ); + when( realmQuery.lessThan( anyString(), anyFloat() ) ).thenAnswer( createComparison(queryTracker, Compare.less ) ); + when( realmQuery.lessThan( anyString(), anyLong() ) ).thenAnswer( createComparison(queryTracker, Compare.less ) ); + when( realmQuery.lessThan( anyString(), anyShort() ) ).thenAnswer( createComparison(queryTracker, Compare.less ) ); + when( realmQuery.lessThan( anyString(), any(Date.class) ) ).thenAnswer( createComparison(queryTracker, Compare.less ) ); + + when( realmQuery.lessThanOrEqualTo( any(), anyInt() ) ).thenAnswer( createComparison(queryTracker, Compare.lessOrEqual ) ); + when( realmQuery.lessThanOrEqualTo( anyString(), anyByte()) ).thenAnswer( createComparison(queryTracker, Compare.lessOrEqual) ); + when( realmQuery.lessThanOrEqualTo( anyString(), anyDouble() ) ).thenAnswer( createComparison(queryTracker, Compare.lessOrEqual) ); + when( realmQuery.lessThanOrEqualTo( anyString(), anyFloat() ) ).thenAnswer( createComparison(queryTracker, Compare.lessOrEqual) ); + when( realmQuery.lessThanOrEqualTo( anyString(), anyLong() ) ).thenAnswer( createComparison(queryTracker, Compare.lessOrEqual) ); + when( realmQuery.lessThanOrEqualTo( anyString(), anyShort() ) ).thenAnswer( createComparison(queryTracker, Compare.lessOrEqual) ); + when( realmQuery.lessThanOrEqualTo( anyString(), any(Date.class) ) ).thenAnswer( createComparison(queryTracker, Compare.lessOrEqual) ); + + when( realmQuery.greaterThan( any(), anyInt() ) ).thenAnswer( createComparison(queryTracker, Compare.more) ); + when( realmQuery.greaterThan( anyString(), anyByte()) ).thenAnswer( createComparison(queryTracker, Compare.more) ); + when( realmQuery.greaterThan( anyString(), anyDouble() ) ).thenAnswer( createComparison(queryTracker, Compare.more) ); + when( realmQuery.greaterThan( anyString(), anyFloat() ) ).thenAnswer( createComparison(queryTracker, Compare.more) ); + when( realmQuery.greaterThan( anyString(), anyLong() ) ).thenAnswer( createComparison(queryTracker, Compare.more) ); + when( realmQuery.greaterThan( anyString(), anyShort() ) ).thenAnswer( createComparison(queryTracker, Compare.more) ); + when( realmQuery.greaterThan( anyString(), any(Date.class) ) ).thenAnswer( createComparison(queryTracker, Compare.more) ); + + when( realmQuery.greaterThanOrEqualTo( any(), anyInt() ) ).thenAnswer( createComparison(queryTracker, Compare.moreOrEqual) ); + when( realmQuery.greaterThanOrEqualTo( anyString(), anyByte()) ).thenAnswer( createComparison(queryTracker, Compare.moreOrEqual) ); + when( realmQuery.greaterThanOrEqualTo( anyString(), anyDouble() ) ).thenAnswer( createComparison(queryTracker, Compare.moreOrEqual) ); + when( realmQuery.greaterThanOrEqualTo( anyString(), anyFloat() ) ).thenAnswer( createComparison(queryTracker, Compare.moreOrEqual) ); + when( realmQuery.greaterThanOrEqualTo( anyString(), anyLong() ) ).thenAnswer( createComparison(queryTracker, Compare.moreOrEqual) ); + when( realmQuery.greaterThanOrEqualTo( anyString(), anyShort() ) ).thenAnswer( createComparison(queryTracker, Compare.moreOrEqual) ); + when( realmQuery.greaterThanOrEqualTo( anyString(), any(Date.class) ) ).thenAnswer( createComparison(queryTracker, Compare.moreOrEqual) ); + + when( realmQuery.between( anyString(), anyInt(), anyInt() ) ).thenAnswer( createComparison(queryTracker, Compare.between) ); + when( realmQuery.between( anyString(), any(Date.class), any(Date.class) ) ).thenAnswer( createComparison(queryTracker, Compare.between) ); + when( realmQuery.between( anyString(), anyDouble(), anyDouble() ) ).thenAnswer( createComparison(queryTracker, Compare.between) ); + when( realmQuery.between( anyString(), anyFloat(), anyFloat() ) ).thenAnswer( createComparison(queryTracker, Compare.between) ); + when( realmQuery.between( anyString(), anyLong(), anyLong() ) ).thenAnswer( createComparison(queryTracker, Compare.between) ); + when( realmQuery.between( anyString(), anyShort(), anyShort() ) ).thenAnswer( createComparison(queryTracker, Compare.between) ); + + + when( realmQuery.equalTo( anyString(), anyInt() ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), anyByte()) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), anyDouble() ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), anyFloat() ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), anyLong() ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), anyString() ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), anyString(), any(Case.class) ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), anyBoolean() ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), anyShort() ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + when( realmQuery.equalTo( anyString(), any(Date.class) ) ).thenAnswer( createComparison(queryTracker, Compare.equal ) ); + + + when( realmQuery.notEqualTo( anyString(), anyInt() ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), anyByte()) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), anyDouble() ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), anyFloat() ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), anyLong() ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), anyString() ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), anyString(), any(Case.class) ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), anyBoolean() ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), anyShort() ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + when( realmQuery.notEqualTo( anyString(), any(Date.class) ) ).thenAnswer( createComparison(queryTracker, Compare.equal, false ) ); + + when( realmQuery.contains( anyString(), anyString() ) ).thenAnswer( createComparison(queryTracker, Compare.contains ) ); + when( realmQuery.contains( anyString(), anyString(), any(Case.class) ) ).thenAnswer( createComparison(queryTracker, Compare.contains ) ); + when( realmQuery.endsWith( anyString(), anyString() ) ).thenAnswer( createComparison(queryTracker, Compare.endsWith ) ); + when( realmQuery.endsWith( anyString(), anyString(), any(Case.class) ) ).thenAnswer( createComparison(queryTracker, Compare.endsWith ) ); + + + when( realmQuery.in( anyString(), any(Integer[].class))).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(Byte[].class)) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(Double[].class) ) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(Float[].class) ) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(Long[].class) ) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(String[].class) ) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(String[].class), any(Case.class) ) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(Boolean[].class) ) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(Short[].class) ) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + when( realmQuery.in( anyString(), any(Date[].class) ) ).thenAnswer( createComparison(queryTracker, Compare.in ) ); + + when( realmQuery.isEmpty(anyString())).thenAnswer(createComparison(queryTracker, Compare.isEmpty)); + when( realmQuery.isNotEmpty(anyString())).thenAnswer(createComparison(queryTracker, Compare.isEmpty, false)); + } + + private static void handleSortingMethods( QueryTracker queryTracker ){ + RealmQuery realmQuery = queryTracker.getRealmQuery(); + + doAnswer(invocation -> { + String field = (String) invocation.getArguments()[0]; + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + queryTracker.appendQuery( Query.build() + .setCondition(Compare.sort) + .setArgs(new Object[]{new QuerySort.SortField(field, true)}) ); + + return queryTracker.rewind(); + }).when( realmQuery ).findAllSorted( anyString() ); + + + doAnswer(invocation -> { + String field = (String) invocation.getArguments()[0]; + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + + + queryTracker.appendQuery( Query.build() + .setCondition(Compare.sort) + .setArgs(new Object[]{new QuerySort.SortField(field, true)})); + + return queryTracker.getRealmResults(); + }).when( realmQuery ).findAllSortedAsync( anyString() ); + + + doAnswer(invocation -> { + String field = (String) invocation.getArguments()[0]; + Sort sort = (Sort) invocation.getArguments()[1]; + + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + queryTracker.appendQuery( Query.build().setCondition(Compare.sort).setArgs(new Object[]{new QuerySort.SortField(field, sort.getValue())})); + return queryTracker.rewind(); + }).when( realmQuery ).findAllSorted( anyString(), any(Sort.class)); + + doAnswer(invocation -> { + String field = (String) invocation.getArguments()[0]; + Sort sort = (Sort) invocation.getArguments()[1]; + + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + queryTracker.appendQuery( Query.build().setCondition(Compare.sort).setArgs(new Object[]{new QuerySort.SortField(field, sort.getValue())})); + return queryTracker.getRealmResults(); + }).when( realmQuery ).findAllSortedAsync( anyString(), any(Sort.class)); + + doAnswer(invocation -> { + + ArrayList sortFields = new ArrayList(); + + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + //sorting goes in reverse order! + sortFields.add( new QuerySort.SortField((String) invocation.getArguments()[2], ((Sort) invocation.getArguments()[3]).getValue() )); + sortFields.add( new QuerySort.SortField((String) invocation.getArguments()[0], ((Sort) invocation.getArguments()[1]).getValue() )); + + for (QuerySort.SortField sortField:sortFields) { + queryTracker.appendQuery( Query.build().setCondition(Compare.sort).setArgs(new Object[]{sortField})); + } + + return queryTracker.rewind(); + }).when( realmQuery ).findAllSorted( anyString(), any(Sort.class), anyString(), any(Sort.class)); + + + doAnswer(invocation -> { + + ArrayList sortFields = new ArrayList(); + + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + //sorting goes in reverse order! + sortFields.add( new QuerySort.SortField((String) invocation.getArguments()[2], ((Sort) invocation.getArguments()[3]).getValue() )); + sortFields.add( new QuerySort.SortField((String) invocation.getArguments()[0], ((Sort) invocation.getArguments()[1]).getValue() )); + + for (QuerySort.SortField sortField:sortFields) { + queryTracker.appendQuery( Query.build().setCondition(Compare.sort).setArgs(new Object[]{sortField})); + } + + return queryTracker.getRealmResults(); + }).when( realmQuery ).findAllSortedAsync( anyString(), any(Sort.class), anyString(), any(Sort.class)); + + + doAnswer(invocation -> { + + String[] fields = (String[])invocation.getArguments()[0]; + Sort[] sorts = (Sort[])invocation.getArguments()[1]; + + if( fields.length != sorts.length ){ + throw new RealmException("#mocking-realm: fields and sort arrays don't match" ); + } + + QuerySort.SortField sortField; + ArrayList sortFields = new ArrayList(); + int top = fields.length-1; + + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + //sorting goes in reverse order! + for( int i = 0; i <= top; i++ ){ + + sortField = new QuerySort.SortField( fields[top-i], sorts[top-i].getValue() ); + queryTracker.appendQuery( Query.build().setCondition(Compare.sort).setArgs(new Object[]{sortField})); + } + + return queryTracker.rewind(); + }).when( realmQuery ).findAllSorted( any(String[].class), any(Sort[].class)); + + + doAnswer(invocation -> { + + String[] fields = (String[])invocation.getArguments()[0]; + Sort[] sorts = (Sort[])invocation.getArguments()[1]; + + if( fields.length != sorts.length ){ + throw new RealmException("#mocking-realm: fields and sort arrays don't match" ); + } + + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + QuerySort.SortField sortField; + ArrayList sortFields = new ArrayList(); + int top = fields.length-1; + + //sorting goes in reverse order! + for( int i = 0; i <= top; i++ ){ + + sortField = new QuerySort.SortField( fields[top-i], sorts[top-i].getValue() ); + + queryTracker.appendQuery( Query.build() + .setCondition(Compare.sort) + .setArgs(new Object[]{sortField})); + } + + return queryTracker.getRealmResults(); + }).when( realmQuery ).findAllSortedAsync( any(String[].class), any(Sort[].class)); + } + + private static void handleDistinct( QueryTracker queryTracker ){ + + RealmQuery realmQuery = queryTracker.getRealmQuery(); + + doAnswer(invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + queryTracker.appendQuery( Query.build() + .setCondition(Compare.distinct) + .setArgs(invocation.getArguments())); + + return queryTracker.rewind(); + }).when(realmQuery).distinct(anyString()); + + doAnswer(invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + for( Object arg: invocation.getArguments() ){ + queryTracker.appendQuery( Query.build() + .setCondition(Compare.distinct) + .setArgs(new Object[]{arg})); + } + return queryTracker.rewind(); + }).when(realmQuery).distinct(anyString(), anyVararg() ); + + doAnswer(invocation -> { + queryTracker.appendQuery( Query.build().setCondition(Compare.endTopGroup)); + + + + queryTracker.appendQuery( Query.build() + .setCondition(Compare.distinct) + .setArgs(invocation.getArguments())); + + return queryTracker.getRealmResults(); + }).when(realmQuery).distinctAsync(anyString()); + } + + + /** + * This method filters using args.condition and updates queryMap + * @param queryTracker queryMap.get( realmQuery.clazz ) gives key to get collection from queryMap + * @param condition based on Compare.enums + * @return the same args.realmQuery + */ + + public static Answer createComparison(QueryTracker queryTracker, String condition ){ + return createComparison(queryTracker, condition, true ); + }; + + public static Answer createComparison(QueryTracker queryTracker, String condition, Boolean assertive ){ + + RealmQuery realmQuery = queryTracker.getRealmQuery(); + + return invocationOnMock -> { + + int argsLen = invocationOnMock.getArguments().length; + String type = ""; + + if( argsLen >= 1 ){ + type = (String) invocationOnMock.getArguments()[0]; + + if( type.isEmpty() ) + return realmQuery; + } + else if( argsLen < 2 ){ + return realmQuery; + } + + if( !assertive ){ + queryTracker.appendQuery(Query.build().setCondition(Compare.not)); + } + + queryTracker.appendQuery(Query.build() + .setCondition(condition) + .setField(type) + .setArgs(invocationOnMock.getArguments())); + + return realmQuery; + }; + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/decorators/RealmResultsDecorator.java b/src/main/java/info/juanmendez/mockrealm/decorators/RealmResultsDecorator.java new file mode 100644 index 0000000..b792774 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/decorators/RealmResultsDecorator.java @@ -0,0 +1,390 @@ +package info.juanmendez.mockrealm.decorators; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; + +import info.juanmendez.mockrealm.dependencies.Compare; +import info.juanmendez.mockrealm.dependencies.TransactionObservable; +import info.juanmendez.mockrealm.models.Query; +import info.juanmendez.mockrealm.models.TransactionEvent; +import info.juanmendez.mockrealm.utils.QuerySort; +import info.juanmendez.mockrealm.utils.QueryTracker; +import info.juanmendez.mockrealm.utils.RealmModelUtil; +import info.juanmendez.mockrealm.utils.SubscriptionsUtil; +import io.realm.RealmChangeListener; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.RealmObject; +import io.realm.RealmQuery; +import io.realm.RealmResults; +import io.realm.Sort; +import io.realm.exceptions.RealmException; +import rx.Observable; +import rx.subjects.BehaviorSubject; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.powermock.api.mockito.PowerMockito.doAnswer; +import static org.powermock.api.mockito.PowerMockito.when; + +/** + * Created by @juanmendezinfo on 2/15/2017. + * + * RealmResults methods rely on RealmListStubbed, which is a subclass of RealmList. + */ +public class RealmResultsDecorator { + + private static SubscriptionsUtil subscriptionsUtil = new SubscriptionsUtil<>(); + + public static RealmResults create(QueryTracker queryTracker ){ + + RealmResults realmResults = queryTracker.getRealmResults(); + RealmList realmList = queryTracker.getQueryList(); + + doAnswer(new Answer() { + + @Override + public RealmQuery answer(InvocationOnMock invocationOnMock) throws Throwable { + + QueryTracker resultsQueryTracker = queryTracker.clone(); + resultsQueryTracker.appendQuery( Query.build().setCondition(Compare.startTopGroup).setArgs(new Object[]{realmList})); + + RealmQuery realmQuery = RealmQueryDecorator.create(resultsQueryTracker); + + return realmQuery; + } + }).when(realmResults).where(); + + /** + * realmList is a shadow of realmResults. + */ + handleBasicActions( realmResults, realmList ); + handleDeleteMethods( realmResults, realmList ); + handleMathMethods( realmResults, realmList ); + handleAsyncMethods( queryTracker ); + handleSorting( queryTracker ); + + return realmResults; + } + + private static void handleBasicActions( RealmResults realmResults, RealmList list){ + + doAnswer(positionInvokation -> { + int position = (int) positionInvokation.getArguments()[0]; + return list.get( position ); + }).when( realmResults).get(anyInt()); + + doAnswer(invocation -> { + return list.size(); + }).when( realmResults ).size(); + + doAnswer(invocation -> { + return list.isEmpty(); + }).when( realmResults ).isEmpty(); + + doAnswer(invocation -> { + return list.iterator(); + }).when( realmResults ).iterator(); + + + doAnswer(new Answer() { + @Override + public RealmObject answer(InvocationOnMock invocationOnMock) throws Throwable { + int index = (int) invocationOnMock.getArguments()[0]; + RealmObject value = (RealmObject) invocationOnMock.getArguments()[0]; + list.set(index, value); + return value; + } + }).when( realmResults ).set(anyInt(), any(RealmObject.class) ); + + doAnswer(new Answer() { + @Override + public RealmModel answer(InvocationOnMock invocationOnMock) throws Throwable { + int index = (int) invocationOnMock.getArguments()[0]; + return list.get(index); + } + }).when( realmResults ).listIterator(anyInt()); + + } + + private static void handleDeleteMethods( RealmResults realmResults, RealmList list ){ + + + doAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + return list.deleteAllFromRealm(); + } + }).when(realmResults).deleteAllFromRealm(); + + + doAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + return list.deleteFirstFromRealm(); + } + }).when( realmResults ).deleteFirstFromRealm(); + + + doAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + return list.deleteLastFromRealm(); + } + }).when( realmResults ).deleteLastFromRealm(); + + + doAnswer( new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + + int position = (int) invocation.getArguments()[0]; + list.deleteFromRealm( position ); + + return null; + } + }).when( realmResults ).deleteFromRealm( anyInt() ); + } + + /** + * The supported methods from realmResults are calling + * and returning values from the counterpart methods of list + * @param realmResults + * @param list + */ + private static void handleMathMethods( RealmResults realmResults, RealmList list ){ + + //realmResults.sum( fieldString ) + when( realmResults.sum( anyString()) ).thenAnswer(new Answer() { + @Override + public Number answer(InvocationOnMock invocation) throws Throwable { + + if( invocation.getArguments().length >= 1 ){ + + String fieldName = (String) invocation.getArguments()[0]; + return list.sum( fieldName ); + } + + return null; + } + }); + + //realmResults.average( fieldString ) + when( realmResults.average(anyString()) ).thenAnswer(new Answer() { + @Override + public Number answer(InvocationOnMock invocation) throws Throwable { + if( invocation.getArguments().length >= 1 ){ + + String fieldName = (String) invocation.getArguments()[0]; + return list.average(fieldName); + } + + return null; + } + }); + + //realmResults.max( fieldString ) + when( realmResults.max(anyString()) ).thenAnswer(new Answer() { + @Override + public Number answer(InvocationOnMock invocation) throws Throwable { + if( invocation.getArguments().length >= 1 ){ + + String fieldName = (String) invocation.getArguments()[0]; + return list.max(fieldName); + } + return null; + } + }); + + //realmResults.min( fieldString ) + when( realmResults.min(anyString()) ).thenAnswer(new Answer() { + @Override + public Number answer(InvocationOnMock invocation) throws Throwable { + if( invocation.getArguments().length >= 1 ){ + + String fieldName = (String) invocation.getArguments()[0]; + return list.min(fieldName); + } + return null; + } + }); + } + + private static void handleAsyncMethods( QueryTracker queryTracker){ + + RealmResults realmResults = queryTracker.getRealmResults(); + + doAnswer(invocation -> { + + //execute query once associated + RealmChangeListener listener = (RealmChangeListener) invocation.getArguments()[0]; + Observable.fromCallable(() -> queryTracker.rewind()) + .subscribeOn(RealmDecorator.getTransactionScheduler()) + .observeOn( RealmDecorator.getResponseScheduler() ) + .subscribe(results -> { + listener.onChange( results ); + }); + + + final String[] json = new String[2]; + + //whenever there is a transaction ending, we compare previous result with current one. + //we transform both results as json objects and just do a check if strings are not the same + subscriptionsUtil.add( realmResults, + listener, + TransactionObservable.asObservable() + .subscribe( transactionEvent -> { + + if( transactionEvent.getState() == TransactionEvent.END_TRANSACTION ){ + + String initialJson = "", currrentJson = ""; + + RealmResults results = queryTracker.getRealmResults(); + initialJson = RealmModelUtil.getState( results ); + + results = queryTracker.rewind(); + currrentJson = RealmModelUtil.getState( results ); + + if( !initialJson.equals( currrentJson )){ + listener.onChange( results ); + } + } + }) + ); + + return null; + }).when( realmResults ).addChangeListener(any(RealmChangeListener.class)); + + + doAnswer(invocation -> { + RealmChangeListener listener = (RealmChangeListener) invocation.getArguments()[0]; + subscriptionsUtil.remove(listener); + return null; + }).when( realmResults ).removeChangeListener( any(RealmChangeListener.class)); + + + doAnswer(invocation -> { + subscriptionsUtil.removeAll( realmResults ); + return null; + }).when( realmResults ).removeChangeListeners(); + + + doAnswer(invocation -> { + BehaviorSubject subject = BehaviorSubject.create(); + + subject.subscribeOn(RealmDecorator.getTransactionScheduler()) + .observeOn( RealmDecorator.getResponseScheduler() ); + + //first time make a call! + Observable.fromCallable(() -> queryTracker.rewind()) + .subscribeOn(RealmDecorator.getTransactionScheduler()) + .observeOn( RealmDecorator.getResponseScheduler() ) + .subscribe(results -> { + subject.onNext( results ); + }); + + TransactionObservable.asObservable() + .subscribe(transactionEvent -> { + + if( transactionEvent.getState() == TransactionEvent.END_TRANSACTION ){ + String initialJson = "", currrentJson = ""; + + RealmResults results = queryTracker.getRealmResults(); + + initialJson = RealmModelUtil.getState( results ); + + results = queryTracker.rewind(); + currrentJson = RealmModelUtil.getState( results ); + + if( !initialJson.equals( currrentJson )){ + subject.onNext( results ); + } + } + }); + + return subject; + }).when( realmResults ).asObservable(); + } + + + private static void handleSorting(QueryTracker queryTracker) { + + RealmResults realmResults = queryTracker.getRealmResults(); + + doAnswer(invocation -> { + String field = (String) invocation.getArguments()[0]; + ArrayList sortFields = new ArrayList(); + sortFields.add( new QuerySort.SortField(field, true)); + return invokeSort( queryTracker, sortFields ); + }).when( realmResults ).sort( anyString() ); + + + doAnswer(invocation -> { + + String field = (String) invocation.getArguments()[0]; + Sort sort = (Sort) invocation.getArguments()[1]; + + ArrayList sortFields = new ArrayList(); + sortFields.add( new QuerySort.SortField(field, sort.getValue() )); + + return invokeSort( queryTracker, sortFields ); + }).when( realmResults ).sort( anyString(), any(Sort.class)); + + + doAnswer(invocation -> { + + ArrayList sortFields = new ArrayList(); + + //sorting goes in reverse order! + sortFields.add( new QuerySort.SortField((String) invocation.getArguments()[2], ((Sort) invocation.getArguments()[3]).getValue() )); + sortFields.add( new QuerySort.SortField((String) invocation.getArguments()[0], ((Sort) invocation.getArguments()[1]).getValue() )); + + return invokeSort( queryTracker, sortFields ); + }).when( realmResults ).sort( anyString(), any(Sort.class), anyString(), any(Sort.class)); + + + + doAnswer(invocation -> { + + String[] fields = (String[])invocation.getArguments()[0]; + Sort[] sorts = (Sort[])invocation.getArguments()[1]; + + if( fields.length != sorts.length ){ + throw new RealmException("#mocking-realm: fields and sort arrays don't match" ); + } + + ArrayList sortFields = new ArrayList(); + int top = fields.length-1; + + //sorting goes in reverse order! + for( int i = 0; i <= top; i++ ){ + sortFields.add( new QuerySort.SortField( fields[top-i], sorts[top-i].getValue() ) ); + } + + return invokeSort( queryTracker, sortFields ); + }).when( realmResults ).sort( any(String[].class), any(Sort[].class)); + + //doAnswer(invocation -> { return realmResults; }).when( realmResults ).sort( any(Comparator.class)); + } + + private static RealmResults invokeSort(QueryTracker queryTracker, ArrayList sortFields ){ + + QueryTracker resultsQueryTracker = queryTracker.clone(); + resultsQueryTracker.appendQuery(Query.build().setCondition(Compare.startTopGroup).setArgs(new Object[]{queryTracker.getQueryList()})); + resultsQueryTracker.appendQuery(Query.build().setCondition(Compare.endTopGroup)); + + for (QuerySort.SortField sortField: sortFields ) { + + resultsQueryTracker.appendQuery(Query.build().setCondition(Compare.sort).setArgs(new Object[]{sortField})); + } + + return resultsQueryTracker.rewind(); + } + + public static void removeSubscriptions(){ + subscriptionsUtil.removeAll(); + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/dependencies/Compare.java b/src/main/java/info/juanmendez/mockrealm/dependencies/Compare.java new file mode 100644 index 0000000..440c1cf --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/dependencies/Compare.java @@ -0,0 +1,33 @@ +package info.juanmendez.mockrealm.dependencies; + +/** + * Created by @juanmendezinfo on 2/15/2017. + */ +public class Compare{ + public static final String less = "less"; + public static final String lessOrEqual = "lessOrEqual"; + public static final String equal = "equal"; + public static final String more = "more"; + public static final String moreOrEqual = "moreOrEqual"; + public static final String between = "between"; + + public static final String contains = "contains"; + public static final String startWith = "starsWith"; + public static final String endsWith = "endsWith"; + public static final String in = "in"; + + public static final String or = "or"; + public static final String not = "not"; + public static final String startGroup = "startGroup"; + public static final String endGroup = "endGroup"; + public static final String startTopGroup = "startTopGroup"; + public static final String endTopGroup = "endTopGroup"; + public static final String isNull = "isNull"; + + public static final String findAll = "findAll"; + public static final String findFirst = "findFirst"; + public static final String distinct = "distinct"; + public static final String sort = "sort"; + public static final String async = "async"; + public static final String isEmpty = "isEmpty"; +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/dependencies/RealmMatchers.java b/src/main/java/info/juanmendez/mockrealm/dependencies/RealmMatchers.java new file mode 100644 index 0000000..8ff515a --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/dependencies/RealmMatchers.java @@ -0,0 +1,48 @@ +package info.juanmendez.mockrealm.dependencies; + +import org.mockito.ArgumentMatcher; + +/** + * Created by @juanmendezinfo on 2/15/2017. + */ +public class RealmMatchers { + + /** + * argument matcher checks if targetClass is the super class of the object passed. + * @param + */ + public static class ClassMatcher extends ArgumentMatcher> { + + private final Class targetClass; + + public ClassMatcher(Class targetClass) { + this.targetClass = targetClass; + } + + public boolean matches(Object obj) { + + if (obj != null && obj instanceof Class) { + return targetClass.isAssignableFrom((Class) obj); + } + return false; + } + } + + /** + * checks if instance matches class + * @param + */ + class InstanceMatcher extends ArgumentMatcher{ + + private final Class targetClass; + + public InstanceMatcher(Class targetClass) { + this.targetClass = targetClass; + } + + @Override + public boolean matches(Object o) { + return targetClass.isInstance( o ); + } + } +} diff --git a/src/main/java/info/juanmendez/mockrealm/dependencies/RealmObservable.java b/src/main/java/info/juanmendez/mockrealm/dependencies/RealmObservable.java new file mode 100644 index 0000000..08429eb --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/dependencies/RealmObservable.java @@ -0,0 +1,59 @@ +package info.juanmendez.mockrealm.dependencies; + +import info.juanmendez.mockrealm.models.RealmEvent; +import info.juanmendez.mockrealm.utils.SubscriptionsUtil; +import rx.Observable; +import rx.Subscription; +import rx.subjects.BehaviorSubject; + +/** + * Created by Juan Mendez on 3/10/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * Provides a subject which can be observed whenever realm is getting a realmModel added or removed + * Elements emitted are of type RealmEvent. RealmEvent wraps the state and realmModel + */ + +public class RealmObservable { + + private static BehaviorSubject realmModelObserver = BehaviorSubject.create(); + private static TransactionObservable to = new TransactionObservable(); + private static SubscriptionsUtil subscriptionsUtil = new SubscriptionsUtil(); + + + public static Observable asObservable() { + return realmModelObserver.asObservable(); + } + + public static void onNext(RealmEvent realmModelState){ + realmModelObserver.onNext( realmModelState); + } + + public static void add(Subscription subscription ){ + subscriptionsUtil.add(to, subscription ); + } + + public static void add( Object observer, Subscription subscription ){ + subscriptionsUtil.add(to, observer, subscription ); + } + + public static void remove( Subscription subscription ){ + subscriptionsUtil.remove(to, subscription ); + } + + public static void remove( Object observer, Subscription subscription ){ + subscriptionsUtil.remove(observer, subscription); + } + + public static void removeSubscriptions(){ + subscriptionsUtil.removeAll(); + + //recreate subject when RealmStorage.clear() + realmModelObserver = BehaviorSubject.create(); + } + + public static void unsubcribe( Object observer ){ + subscriptionsUtil.remove( observer ); + } +} diff --git a/src/main/java/info/juanmendez/mockrealm/dependencies/RealmStorage.java b/src/main/java/info/juanmendez/mockrealm/dependencies/RealmStorage.java new file mode 100644 index 0000000..ea6e6c1 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/dependencies/RealmStorage.java @@ -0,0 +1,85 @@ +package info.juanmendez.mockrealm.dependencies; + +import java.util.HashMap; + +import info.juanmendez.mockrealm.decorators.RealmObjectDecorator; +import info.juanmendez.mockrealm.decorators.RealmResultsDecorator; +import info.juanmendez.mockrealm.models.RealmAnnotation; +import info.juanmendez.mockrealm.models.RealmEvent; +import info.juanmendez.mockrealm.utils.RealmModelUtil; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.exceptions.RealmException; + +/** + * Created by @juanmendezinfo on 2/15/2017. + */ +public class RealmStorage { + + private static HashMap> realmMap = new HashMap<>(); + private static HashMap annotationMap = new HashMap<>(); + + /*keeps collections keyed by a sub-class of RealmModel.*/ + public static HashMap> getRealmMap() { + return realmMap; + } + + public static void removeModel( RealmModel realmModel ){ + + if( realmModel != null ){ + + Class clazz = RealmModelUtil.getClass(realmModel); + + if( RealmModel.class.isAssignableFrom(clazz) ){ + + if( realmMap.get(clazz) != null && realmMap.get(clazz).contains( realmModel ) ){ + realmMap.get( clazz ).remove( realmModel ); + RealmObservable.onNext( new RealmEvent( RealmEvent.MODEL_REMOVED, realmModel ) ); + }else{ + throw new RealmException( "Instance of " + clazz.getName() + " cannot be deleted as it's not part of the realm database" ); + } + + }else{ + + throw new RealmException( clazz.getName() + " is not an instance of RealmModel" ); + } + } + } + + public static void addModel( RealmModel realmModel ){ + + if( realmModel != null ){ + + Class clazz = RealmModelUtil.getClass(realmModel); + + if( RealmModel.class.isAssignableFrom(clazz) ){ + + if( !realmMap.get(clazz).contains( realmModel ) ){ + realmMap.get(RealmModelUtil.getClass(realmModel) ).add( realmModel ); + RealmObservable.onNext( new RealmEvent( RealmEvent.MODEL_ADDED, realmModel ) ); + }else{ + System.out.println( "#mocking-realm: Instance of " + clazz.getName() + " cannot be added more than once" ); + } + }else{ + + throw new RealmException( clazz.getName() + " is not an instance of RealmModel" ); + } + } + } + + public static void clear(){ + RealmObservable.removeSubscriptions(); + TransactionObservable.removeSubscriptions(); + RealmObjectDecorator.removeSubscriptions(); + RealmResultsDecorator.removeSubscriptions(); + realmMap.clear(); + } + + public static void addAnnotations( RealmAnnotation realmAnnotation ){ + annotationMap.put( realmAnnotation.getClazz(), realmAnnotation ); + } + + public static HashMap getAnnotationMap() { + return annotationMap; + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/dependencies/TransactionObservable.java b/src/main/java/info/juanmendez/mockrealm/dependencies/TransactionObservable.java new file mode 100644 index 0000000..c2031e0 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/dependencies/TransactionObservable.java @@ -0,0 +1,132 @@ +package info.juanmendez.mockrealm.dependencies; + +import java.util.ArrayList; + +import info.juanmendez.mockrealm.models.TransactionEvent; +import info.juanmendez.mockrealm.utils.SubscriptionsUtil; +import rx.Observable; +import rx.Subscription; +import rx.subjects.PublishSubject; + +/** + * Created by Juan Mendez on 3/17/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + */ +public class TransactionObservable { + private static TransactionObservable instance; + private static SubscriptionsUtil subscriptionsUtil = new SubscriptionsUtil(); + private static PublishSubject subject = PublishSubject.create(); + + private static ArrayList stackTransactions = new ArrayList<>(); + + public static void startRequest(Object keyTransaction, Subscription subscription ){ + + if( instance == null ){ + instance = new TransactionObservable(); + } + + subscriptionsUtil.add( instance, keyTransaction, subscription ); + + if( stackTransactions.isEmpty() ){ + stackTransactions.add( keyTransaction ); + next(); + }else{ + stackTransactions.add( keyTransaction ); + } + } + + /** + * this is a transaction which happens in the main thread, + * it is taken as a priority, and moved as first transaction + * @param keyTransaction + */ + public static void startRequest(Object keyTransaction ){ + stackTransactions.add(0, keyTransaction ); + next(); + } + + + /** + * closes stackTransactions subscription, but only fires event and requests to start next transaction + * if initiator is the first element at stackTransactions. + * @param keyTransaction + */ + public static void endRequest(Object keyTransaction ){ + + int keyIndex = stackTransactions.indexOf(keyTransaction); + + //notify when transaction ends only if it's the first on the list + if( keyIndex >= 0 && !stackTransactions.isEmpty() ){ + + stackTransactions.remove(keyIndex); + + if( keyIndex == 0 ){ + subject.onNext( new TransactionEvent(TransactionEvent.END_TRANSACTION, keyTransaction )); + } + + subscriptionsUtil.remove( keyTransaction ); + + if( keyIndex == 0 ){ + next(); + } + } + } + + /** + * canceling transaction simply means removing it from stackTransactions + * only if it's not the current transaction which means is the first in the list. + * @param keyTransaction + */ + public static void cancel(Object keyTransaction ){ + if( stackTransactions.indexOf(keyTransaction) > 0 ){ + stackTransactions.remove( keyTransaction ); + subscriptionsUtil.remove( keyTransaction ); + } + } + + /** + * check if transaction has been canceled + * @param keyTransaction + * @return true if is not in stackTransactions + */ + public static Boolean isCanceled(Object keyTransaction ){ + return stackTransactions.indexOf(keyTransaction) < 0; + } + + private static void next(){ + + if( !stackTransactions.isEmpty() ){ + subject.onNext( new TransactionEvent(TransactionEvent.START_TRANSACTION, stackTransactions.get(0) )); + } + } + + public static Observable asObservable(){ + return subject.asObservable(); + } + + + public static void removeSubscriptions(){ + subscriptionsUtil.removeAll(); + + //required in case of RealmStorage.clear() + subject = PublishSubject.create(); + } + + /** + * An instance of KeyTransaction pairs with each transaction. As a key it then notifies the next transaction, and + * is used again to request ending transaction, or canceling. + */ + public static class KeyTransaction{ + String name; + + public static KeyTransaction create( String name ){ + return new KeyTransaction(name); + } + + public KeyTransaction(String name){ + this.name = name; + } + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/models/Query.java b/src/main/java/info/juanmendez/mockrealm/models/Query.java new file mode 100644 index 0000000..1b21964 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/models/Query.java @@ -0,0 +1,57 @@ +package info.juanmendez.mockrealm.models; + +/** + * Created by Juan Mendez on 3/6/2017. + * www.juanmendez.info + * contact@juanmendez.info + */ + +public class Query { + + private String field; + private String condition; + private Object[] args; + private Boolean asTrue = true; + + public static Query build(){ + return new Query(); + } + + private Query(){} + + public Query setField(String field) { + this.field = field; + return this; + } + + public Query setCondition(String condition) { + this.condition = condition; + return this; + } + + public Query setArgs(Object[] args) { + this.args = args; + return this; + } + + public Query setAsTrue(Boolean asTrue) { + this.asTrue = asTrue; + return this; + } + + public String getField() { + return field; + } + + public String getCondition() { + return condition; + } + + public Object[] getArgs() { + return args; + } + + public Boolean getAsTrue() { + return asTrue; + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/models/RealmAnnotation.java b/src/main/java/info/juanmendez/mockrealm/models/RealmAnnotation.java new file mode 100644 index 0000000..941831e --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/models/RealmAnnotation.java @@ -0,0 +1,67 @@ +package info.juanmendez.mockrealm.models; + +import java.util.ArrayList; + +/** + * Created by Juan Mendez on 4/8/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * Because Realm annotations are of type RetentionPolicy.CLASS, we cannot + * use reflection, so this is a simple way to start declaring your reamModel classes + * before you start mocking. + */ +public class RealmAnnotation { + Class clazz; + String _primaryField; + ArrayList _indexedFields = new ArrayList<>(); + ArrayList _ignoredFields = new ArrayList<>(); + + public static RealmAnnotation build( Class clazz ){ + RealmAnnotation realmAnnotation = new RealmAnnotation(); + realmAnnotation.clazz = clazz; + return realmAnnotation; + } + + public RealmAnnotation primaryField(String field ){ + _primaryField = field; + return this; + } + + public RealmAnnotation indexedFields(String ... fields ){ + + _indexedFields.clear(); + + for( String field: fields ){ + _indexedFields.add( field ); + } + return this; + } + + + public RealmAnnotation ignoredFields(String ... fields ){ + + _ignoredFields.clear(); + + for( String field: fields ){ + _ignoredFields.add( field ); + } + return this; + } + + public Class getClazz() { + return clazz; + } + + public String getPrimaryField() { + return _primaryField; + } + + public ArrayList geIndexedFields() { + return _indexedFields; + } + + public ArrayList getIgnoredFields() { + return _ignoredFields; + } +} diff --git a/src/main/java/info/juanmendez/mockrealm/models/RealmEvent.java b/src/main/java/info/juanmendez/mockrealm/models/RealmEvent.java new file mode 100644 index 0000000..ce8c9a1 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/models/RealmEvent.java @@ -0,0 +1,36 @@ +package info.juanmendez.mockrealm.models; + +import io.realm.RealmModel; + +/** + * Created by Juan Mendez on 3/10/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * It captures a change in realm + */ + +public class RealmEvent { + public static final String MODEL_ADDED = "RealmModelAdded"; + public static final String MODEL_REMOVED = "RealmModelRemoved"; + + private String state; + private RealmModel realmModel; + + public RealmEvent( String state ){ + this.state = state; + } + + public RealmEvent(String state, RealmModel realmModel) { + this.state = state; + this.realmModel = realmModel; + } + + public String getState() { + return state; + } + + public RealmModel getRealmModel() { + return realmModel; + } +} diff --git a/src/main/java/info/juanmendez/mockrealm/models/RealmListStubbed.java b/src/main/java/info/juanmendez/mockrealm/models/RealmListStubbed.java new file mode 100644 index 0000000..87b603e --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/models/RealmListStubbed.java @@ -0,0 +1,141 @@ +package info.juanmendez.mockrealm.models; + +import org.mockito.internal.util.reflection.Whitebox; + +import info.juanmendez.mockrealm.decorators.RealmModelDecorator; +import io.realm.RealmList; +import io.realm.RealmModel; + + +/** + * Created by Juan Mendez on 3/1/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * This subclass replaces functionality from RealmList. + */ + +public class RealmListStubbed extends RealmList { + + @Override + public boolean deleteFirstFromRealm() { + if( !isEmpty() ){ + RealmModel realmModel = get(0); + RealmModelDecorator.deleteRealmModel( realmModel ); + return true; + } + + return false; + } + + @Override + public boolean deleteLastFromRealm() { + + if( !isEmpty() ){ + RealmModel realmModel = get( size()-1 ); + RealmModelDecorator.deleteRealmModel( realmModel ); + return true; + } + + return false; + } + + @Override + public void deleteFromRealm(int location) { + if( !isEmpty()){ + RealmModel realmModel = get( size() - 1 ); + + RealmModelDecorator.deleteRealmModel( realmModel ); + } + } + + @Override + public Number min(String fieldName) { + + Number value, minValue = null; + + for (Object item: this ) { + + value = (Number) Whitebox.getInternalState( item, fieldName ); + + if( minValue == null ) + minValue = value; + else + if( value.floatValue() < minValue.floatValue() ){ + minValue = value; + } + } + + return minValue; + } + + /** + * if realmModel is empty or null, then returns null + * @param fieldName + * @return + */ + @Override + public Number max(String fieldName) { + + Number value, maxValue = null; + + for (Object item: this ) { + + value = (Number) Whitebox.getInternalState( item, fieldName ); + + if( maxValue == null ) + maxValue = value; + else + if( value.floatValue() > maxValue.floatValue() ){ + maxValue = value; + } + } + + return maxValue; + } + + /** + * if realmModel is empty or null, then returns null + * @param fieldName + * @return + */ + @Override + public Number sum(String fieldName) { + + double sumValue = 0; + + for (Object item: this ) { + + sumValue += ((Number) Whitebox.getInternalState( item, fieldName )).floatValue(); + } + + return sumValue; + } + + /** + * if realmModel is empty or null, then returns null + * @param fieldName + * @return + */ + @Override + public double average(String fieldName) { + + float sumValue = 0; + + for (Object item: this ) { + + sumValue += ((Number) Whitebox.getInternalState( item, fieldName )).floatValue(); + } + + return (sumValue/size()); + } + + @Override + public boolean deleteAllFromRealm() { + for (Object realmModel: this) { + RealmModelDecorator.deleteRealmModel( (RealmModel) realmModel ); + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/models/TransactionEvent.java b/src/main/java/info/juanmendez/mockrealm/models/TransactionEvent.java new file mode 100644 index 0000000..4d82784 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/models/TransactionEvent.java @@ -0,0 +1,32 @@ +package info.juanmendez.mockrealm.models; + +/** + * Created by Juan Mendez on 3/17/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * In order to start and end transactions, we need to keep track of what's been added and removed.. + */ + +public class TransactionEvent { + + public static final String START_TRANSACTION = "RealmStartTransaction"; + public static final String END_TRANSACTION = "RealmEndTransaction"; + + + private String state; + private Object target; + + public TransactionEvent(String state, Object target) { + this.state = state; + this.target = target; + } + + public String getState() { + return state; + } + + public Object getTarget() { + return target; + } +} diff --git a/src/main/java/info/juanmendez/mockrealm/test/MockRealmTester.java b/src/main/java/info/juanmendez/mockrealm/test/MockRealmTester.java new file mode 100644 index 0000000..380c1e2 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/test/MockRealmTester.java @@ -0,0 +1,25 @@ +package info.juanmendez.mockrealm.test; + +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import io.realm.Realm; +import io.realm.RealmConfiguration; +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.RealmQuery; +import io.realm.RealmResults; + +/** + * Created by Juan Mendez on 4/6/2017. + * www.juanmendez.info + * contact@juanmendez.info + */ + +@RunWith(PowerMockRunner.class) +@PowerMockIgnore({"org.mockito.*", "android.*"}) +@PrepareForTest({ RealmConfiguration.class, Realm.class, RealmQuery.class, RealmResults.class, RealmList.class, RealmObject.class }) +public abstract class MockRealmTester { +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/utils/QueryDistinct.java b/src/main/java/info/juanmendez/mockrealm/utils/QueryDistinct.java new file mode 100644 index 0000000..4072058 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/utils/QueryDistinct.java @@ -0,0 +1,97 @@ +package info.juanmendez.mockrealm.utils; + +import org.mockito.internal.util.reflection.Whitebox; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; + +import info.juanmendez.mockrealm.decorators.RealmListDecorator; +import info.juanmendez.mockrealm.models.Query; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.exceptions.RealmException; + +/** + * Created by Juan Mendez on 3/27/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * This class takes care of making a realmList have distinct values + */ + +public class QueryDistinct { + + ArrayList types; + + public RealmList perform(Query query, RealmList realmList ){ + return perform( (String)query.getArgs()[0], realmList ); + } + + /** + * takes only one filed to sort! + * @param field (must have field to sort, and either desc/asc order) + * @param realmList list to sort + */ + public RealmList perform(String field, RealmList realmList ){ + this.types = new ArrayList<>(Arrays.asList(((String) field).split("\\."))); + + RealmList distinctList = RealmListDecorator.create(); + + if( realmList != null && !realmList.isEmpty()){ + + Object value; + ArrayList distinctValues = new ArrayList<>(); + + for (RealmModel realmModel : realmList) { + value = searchInModel(realmModel, 0); + + if( !distinctValues.contains(value)){ + distinctValues.add(value); + distinctList.add( realmModel ); + } + } + } + + return distinctList; + } + + /** + * find the current value based on the array types. if it's the final element from such array + * then it returns that value, otherwises it checks if the current value is a realmModel or realmList, + * and then does another iteration. + * @param model + * @param level + * @return + */ + private Object searchInModel(Object model, int level) { + + Object o; + + try { + o = Whitebox.getInternalState(model, types.get(level)); + } catch (Exception e) { + throw (new RealmException(RealmModelUtil.getClass(model).getName() + " doesn't have the attribute " + types.get(level))); + } + + if (o != null) { + + if (level < types.size() - 1) { + + if (o instanceof RealmList) { + throw new RealmException("#mocking-realm: 'RealmList' field '" + types.get(level) + "' is not a supported link field here." ); + } else if (o instanceof RealmModel) { + throw new RealmException("#mocking-realm: 'RealmObject' field '" + types.get(level) + "' is not a supported link field here." ); + } else{ + return searchInModel( o, level + 1); + } + } + } + + if( o instanceof Date){ + o = ((Date)o).getTime(); + } + + return o; + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/utils/QuerySearch.java b/src/main/java/info/juanmendez/mockrealm/utils/QuerySearch.java new file mode 100644 index 0000000..58a189e --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/utils/QuerySearch.java @@ -0,0 +1,270 @@ +package info.juanmendez.mockrealm.utils; + +import org.mockito.internal.util.reflection.Whitebox; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; + +import info.juanmendez.mockrealm.decorators.RealmListDecorator; +import info.juanmendez.mockrealm.dependencies.Compare; +import info.juanmendez.mockrealm.models.Query; +import io.realm.Case; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.exceptions.RealmException; + +/** + * Created by Juan Mendez on 3/7/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * This util does a deep search through and check if the given haystack + * has any realmModel which meets the condition. + */ +public class QuerySearch { + + Query query; + ArrayList types; + Object needle; + ArrayList needles; + + Class clazz; + Case casing = Case.SENSITIVE; + + Object inLeft; + Object inRight; + + public RealmList search(Query query, RealmList haystack ) { + + this.query = query; + String condition = query.getCondition(); + Object[] arguments = query.getArgs(); + + this.types = new ArrayList<>(Arrays.asList(((String) arguments[0]).split("\\."))); + + int argsLen = arguments.length; + + if( argsLen >= 2){ + this.needle = arguments[1]; + this.clazz = RealmModelUtil.getClass(needle); + + if ((clazz == String.class || clazz == String[].class) && argsLen >= 3) { + casing = (Case) arguments[2]; + } + } + + if (condition == Compare.between) { + this.inLeft = arguments[1]; + this.inRight = arguments[2]; + } + else if (condition == Compare.in) { + needles = new ArrayList<>(Arrays.asList((Object[]) needle)); + } + + if (casing == Case.INSENSITIVE) { + + if (condition == Compare.in) { + + for (int i = 0; i < needles.size(); i++) { + needles.set(i, ((String) needles.get(i)).toLowerCase()); + } + } else { + this.needle = ((String) needle).toLowerCase(); + } + } + + RealmList queriedList = RealmListDecorator.create(); + Boolean returnValue; + for (RealmModel realmModel : haystack) { + returnValue = checkRealmObject(realmModel, 0); + + if( !query.getAsTrue() ) + returnValue = !returnValue; + + if (returnValue) { + queriedList.add(realmModel); + } + } + + return queriedList; + } + + private boolean checkRealmObject(RealmModel realmModel, int level) { + //RunTimeErrorException if search field is not found in realmQueryClass + + Object value; + String condition = query.getCondition(); + + try { + value = Whitebox.getInternalState(realmModel, types.get(level)); + } catch (Exception e) { + throw (new RealmException( "'" + RealmModelUtil.getClass(realmModel).getName() + "' doesn't have the attribute '" + types.get(level) + "'")); + } + + if (value != null) { + + if (level < types.size() - 1) { + + if (value instanceof RealmList) { + RealmList valueList = (RealmList) value; + + for (RealmModel rm : valueList) { + //at least one item must meet the requirements! + if (checkRealmObject(rm, level + 1)) { + return true; + } + } + + return false; + } else if (value instanceof RealmModel) { + return checkRealmObject((RealmModel) value, level + 1); + } + + throw (new RealmException(types.get(level) + " is of neither type RealmList, or RealmModel")); + } + + if (condition == Compare.equal) { + + if (clazz == Date.class && (((Date) value)).compareTo((Date) needle) == 0) { + return true; + } else if (casing == Case.INSENSITIVE) { + return needle.equals(((String) value).toLowerCase()); + } else if (needle.equals(value)) { + return true; + } + } else if (condition == Compare.less) { + + if (clazz == Date.class && (((Date) value)).compareTo((Date) needle) < 0) { + return true; + } else if (clazz == Byte.class && ((byte) value) < ((byte) needle)) { + return true; + } else if (clazz == Integer.class && ((int) value) < ((int) needle)) { + return true; + } else if (clazz == Double.class && ((double) value) < ((double) needle)) { + return true; + } else if (clazz == Long.class && ((long) value) < ((long) needle)) { + return true; + } else if (clazz == Float.class && ((float) value) < ((float) needle)) { + return true; + } else if (clazz == Short.class && ((short) value) < ((short) needle)) { + return true; + } + } else if (condition == Compare.lessOrEqual) { + + if (clazz == Date.class && (((Date) value)).compareTo((Date) needle) <= 0) { + return true; + } else if (clazz == Byte.class && ((byte) value) <= ((byte) needle)) { + return true; + } else if (clazz == Integer.class && ((int) value) <= ((int) needle)) { + return true; + } else if (clazz == Double.class && ((double) value) <= ((double) needle)) { + return true; + } else if (clazz == Long.class && ((long) value) <= ((long) needle)) { + return true; + } else if (clazz == Float.class && ((float) value) <= ((float) needle)) { + return true; + } else if (clazz == Short.class && ((short) value) <= ((short) needle)) { + return true; + } + } else if (condition == Compare.more) { + + if (clazz == Date.class && (((Date) value)).compareTo((Date) needle) > 0) { + return true; + } else if (clazz == Byte.class && ((byte) value) > ((byte) needle)) { + return true; + } else if (clazz == Integer.class && ((int) value) > ((int) needle)) { + return true; + } else if (clazz == Double.class && ((double) value) > ((double) needle)) { + return true; + } else if (clazz == Long.class && ((long) value) > ((long) needle)) { + return true; + } else if (clazz == Float.class && ((float) value) > ((float) needle)) { + return true; + } else if (clazz == Short.class && ((short) value) > ((short) needle)) { + return true; + } + } else if (condition == Compare.moreOrEqual) { + + if (clazz == Date.class && (((Date) value)).compareTo((Date) needle) >= 0) { + return true; + } else if (clazz == Byte.class && ((byte) value) >= ((byte) needle)) { + return true; + } else if (clazz == Integer.class && ((int) value) >= ((int) needle)) { + return true; + } else if (clazz == Double.class && ((double) value) >= ((double) needle)) { + return true; + } else if (clazz == Long.class && ((long) value) >= ((long) needle)) { + return true; + } else if (clazz == Float.class && ((float) value) >= ((float) needle)) { + return true; + } else if (clazz == Short.class && ((short) value) >= ((short) needle)) { + return true; + } + } else if (condition == Compare.between) { + + if (clazz == Date.class) { + + if (((Date) value).getTime() >= ((Date) inLeft).getTime() && ((Date) value).getTime() <= ((Date) inRight).getTime()) + return true; + } else if (clazz == Integer.class && ((int) value) >= ((int) inLeft) && ((int) value) <= ((int) inRight)) { + return true; + } else if (clazz == Double.class && ((double) value) >= ((double) inLeft) && ((double) value) <= ((double) inRight)) { + return true; + } else if (clazz == Long.class && ((long) value) >= ((long) inLeft) && ((long) value) <= ((long) inRight)) { + return true; + } else if (clazz == Float.class && ((float) value) >= ((float) inLeft) && ((float) value) <= ((float) inRight)) { + return true; + } else if (clazz == Short.class && ((short) value) >= ((short) inLeft) && ((short) value) <= ((short) inRight)) { + return true; + } + } else if (condition == Compare.in) { + + if (clazz == Date[].class) { + + for (Object needle : needles) { + if ((((Date) value)).compareTo((Date) needle) == 0) + return true; + } + } else { + if (clazz == String[].class && casing == Case.INSENSITIVE) { + return needles.contains(((String) value).toLowerCase()); + } else { + return needles.contains(value); + } + } + } else if (clazz == String.class && (condition == Compare.contains || condition == Compare.endsWith)) { + + if (condition == Compare.contains) { + if (casing == Case.SENSITIVE && ((String) value).contains((String) needle)) { + return true; + } else if (casing == Case.INSENSITIVE && (((String) value).toLowerCase()).contains(((String) needle))) { + return true; + } + } else if (condition == Compare.endsWith) { + if (casing == Case.SENSITIVE && ((String) value).endsWith((String) needle)) { + return true; + } else if (casing == Case.INSENSITIVE && (((String) value).toLowerCase()).endsWith(((String) needle))) { + return true; + } + } + }else if( condition == Compare.isEmpty ){ + + if( value instanceof String || value instanceof AbstractList ){ + if( value instanceof String && ((String)value).isEmpty() ) + return true; + else + if( value instanceof AbstractList && ((AbstractList)value).isEmpty() ) + return true; + }else{ + throw new RealmException( "Field '" + types.get(level) + "': type mismatch. Was OBJECT, expected [STRING, BINARY, LIST]" ); + } + } + }else if( condition == Compare.isNull){ + return true; + } + + return false; + } +} diff --git a/src/main/java/info/juanmendez/mockrealm/utils/QuerySort.java b/src/main/java/info/juanmendez/mockrealm/utils/QuerySort.java new file mode 100644 index 0000000..8b22a2f --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/utils/QuerySort.java @@ -0,0 +1,252 @@ +package info.juanmendez.mockrealm.utils; + +import org.mockito.internal.util.reflection.Whitebox; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; + +import info.juanmendez.mockrealm.decorators.RealmListDecorator; +import info.juanmendez.mockrealm.models.Query; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.exceptions.RealmException; + +/** + * Created by Juan Mendez on 3/27/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * This class takes care of sorting realmLists. The realmList can be in any level. + * For example persons.dogs.age, srots each dogs realmList, only. + * In case it's just persons.favoriteDog, then persons realmList is the one sorted + */ + +public class QuerySort { + + ArrayList types; + private int desc = 1; + + + + public RealmList perform(Query query, RealmList realmList ){ + return perform( (SortField)query.getArgs()[0], realmList ); + } + + /** + * takes only one filed to sort! + * @param sortField (must have field to sort, and either desc/asc order) + * @param realmList list to sort + */ + public RealmList perform( SortField sortField, RealmList realmList ){ + this.types = new ArrayList<>(Arrays.asList(((String) sortField.field).split("\\."))); + this.desc = sortField.desc?1:-1; + + RealmList listToSort = RealmListDecorator.create(); + listToSort.addAll(realmList); + + searchInList( listToSort, 0 ); + return listToSort; + } + + private Object searchInList(RealmList realmList, int level ){ + + if( realmList != null && !realmList.isEmpty()){ + ModelKey modelKey; + ArrayList modelKeys = new ArrayList<>(); + + + for (RealmModel realmModel : realmList) { + modelKeys.add( new ModelKey( searchInModel(realmModel, level), realmModel ) ); + } + + Collections.sort(modelKeys, new GenericComparator() ); + + Iterator itr= modelKeys.iterator(); + realmList.clear(); + + while(itr.hasNext()){ + modelKey = (ModelKey) itr.next(); + realmList.add( modelKey.realmModel ); + } + } + + return null; + } + + /** + * find the current value based on the array types. if it's the final element from such array + * then it returns that value, otherwises it checks if the current value is a realmModel or realmList, + * and then does another iteration. + * @param realmModel + * @param level + * @return + */ + private Object searchInModel(RealmModel realmModel, int level) { + + Object o; + + try { + o = Whitebox.getInternalState(realmModel, types.get(level)); + } catch (Exception e) { + throw (new RealmException(RealmModelUtil.getClass(realmModel).getName() + " doesn't have the attribute " + types.get(level))); + } + + if (o != null) { + + if (level < types.size() - 1) { + + if (o instanceof RealmList) { + throw new RealmException("#mocking-realm: 'RealmList' field '" + types.get(level) + "' is not a supported link field here." ); + //could have sorted another level: return searchInList( (RealmList) o, level + 1 ); + } else if (o instanceof RealmModel) { + return searchInModel((RealmModel) o, level + 1); + } + + throw (new RealmException(types.get(level) + " is of neither type RealmList, or RealmModel")); + } + } + + return o; + } + + /** + * we sort by having an instance of each realmModel and its value wrapped in a ValueKey + */ + class ModelKey { + Object key; + RealmModel realmModel; + + public ModelKey(Object key, RealmModel realmModel) { + this.key = key; + this.realmModel = realmModel; + } + } + + /** + * This is default Comparator. It welcomes ModelKeys, and based on their + * keys, it is able to sort. + */ + class GenericComparator implements Comparator{ + + /** + * ValueKey's value is an object, so within compare we do comparisson + * based on their original class. + * @param q1 + * @param q2 + * @return + */ + @Override + public int compare(ModelKey q1, ModelKey q2) { + Object k1 = q1.key; + Object k2 = q2.key; + int returnValue = 0; + + if( k1!=null && k2!=null){ + Class clazz = k1.getClass(); + + if (clazz == Date.class) { + returnValue = (((Date) k1)).compareTo((Date) k2); + } else if( clazz == String.class ){ + returnValue = (((String) k1)).compareTo((String) k2); + } else if (clazz == Integer.class ) { + int b1 = (int)k1; + int b2 = (int)k2; + + if( b1 > b2 ){ + returnValue = 1; + }else if( b1 == b2 ){ + returnValue = 0; + }else { + returnValue = -1; + } + } else if (clazz == Double.class ) { + double b1 = (double)k1; + double b2 = (double)k2; + + if( b1 > b2 ){ + returnValue = 1; + }else if( b1 == b2 ){ + returnValue = 0; + }else { + returnValue = -1; + } + } else if (clazz == Long.class ) { + long b1 = (long)k1; + long b2 = (long)k2; + + if( b1 > b2 ){ + returnValue = 1; + }else if( b1 == b2 ){ + returnValue = 0; + }else { + returnValue = -1; + } + + } else if (clazz == Float.class ) { + float b1 = (float)k1; + float b2 = (float)k2; + + if( b1 > b2 ){ + returnValue = 1; + }else if( b1 == b2 ){ + returnValue = 0; + }else { + returnValue = -1; + } + } else if (clazz == Short.class ) { + short b1 = (short)k1; + short b2 = (short)k2; + + if( b1 > b2 ){ + returnValue = 1; + }else if( b1 == b2 ){ + returnValue = 0; + }else { + returnValue = -1; + } + }else if (clazz == Byte.class ) { + byte b1 = (byte)k1; + byte b2 = (byte)k2; + + if( b1 > b2 ){ + returnValue = 1; + }else if( b1 == b2 ){ + returnValue = 0; + }else { + returnValue = -1; + } + } + }else{ + + if( k1 == null && k2 == null ) + returnValue = 0; + else + if( k1 != null ) + returnValue = 1; + else + if( k2 != null ) + returnValue = -1; + } + + return returnValue * desc; + } + } + + public static class SortField{ + private String field; + private Boolean desc; + + public SortField(String field, Boolean desc) { + this.field = field; + this.desc = desc; + } + + public String getField() { + return field; + } + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/utils/QueryTracker.java b/src/main/java/info/juanmendez/mockrealm/utils/QueryTracker.java new file mode 100644 index 0000000..137baf9 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/utils/QueryTracker.java @@ -0,0 +1,255 @@ +package info.juanmendez.mockrealm.utils; + +import org.powermock.api.mockito.PowerMockito; + +import java.util.ArrayList; + +import info.juanmendez.mockrealm.decorators.RealmListDecorator; +import info.juanmendez.mockrealm.decorators.RealmResultsDecorator; +import info.juanmendez.mockrealm.dependencies.Compare; +import info.juanmendez.mockrealm.models.Query; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.RealmQuery; +import io.realm.RealmResults; +import io.realm.exceptions.RealmException; + +import static org.powermock.api.mockito.PowerMockito.mock; + +/** + * Created by @juanmendezinfo on 2/19/2017. + */ +public class QueryTracker { + + private ArrayList queries = new ArrayList<>(); + + private ArrayList queryAndList = new ArrayList<>(); + private ArrayList queryYesList = new ArrayList<>(); + private ArrayList> groupResults = new ArrayList<>(); + private int groupLevel = 0; + private Class clazz; + private RealmQuery realmQuery; + private RealmResults realmResults; + + /** + * cloned object preserves realmList, check clone method + */ + private RealmList parentRealmList; + + public QueryTracker(Class clazz){ + this.clazz = clazz; + realmQuery = mock(RealmQuery.class); + setUpRealmResults(); + } + + private void setUpRealmResults(){ + //beforehand we are going to take care of realmResults + realmResults = PowerMockito.mock( RealmResults.class ); + + //level 0, we are going to start a realmList + groupResults.add( RealmListDecorator.create() ); + RealmResultsDecorator.create( this ); + } + + private void onTopGroupBegin(RealmList realmList ){ + + //we update the current list instead of assigning one. + groupResults.get(groupLevel).clear(); + groupResults.get(groupLevel).addAll( realmList ); + queryAndList.add( true ); + queryYesList.add( true ); + + onBeginGroupClause(); + } + + public void setQueryList( RealmList queryList ){ + if( queryAndList.get( groupLevel ) ){ + groupResults.get( groupLevel ).clear(); + groupResults.get( groupLevel ).addAll( queryList ); + }else{ + + RealmList currentGroupList = groupResults.get( groupLevel ); + + for (RealmModel realmModel: queryList) { + if( !currentGroupList.contains( realmModel)){ + currentGroupList.add( realmModel ); + } + } + } + + //if the las query is based on OR(), then bounce back to AND() + queryAndList.set( groupLevel, true ); + } + + private void onOrClause() { + queryAndList.set( groupLevel, false ); + } + + private void onNotClause(){ + queryYesList.set( groupLevel, false ); + } + + private void onBeginGroupClause(){ + + RealmList previousGroupList = groupResults.get( groupLevel ); + + RealmList nextGroupList = RealmListDecorator.create(); + nextGroupList.addAll( previousGroupList ); + + groupLevel++; + groupResults.add( nextGroupList ); + queryAndList.add( true ); + queryYesList.add( true ); + } + + private void onCloseGroupClause() { + + RealmList currentGroupList = groupResults.get(groupLevel); + Boolean isThereAYes = queryYesList.get(groupLevel); + + groupResults.remove(groupLevel); + queryAndList.remove(groupLevel); + queryYesList.remove(groupLevel); + + groupLevel--; + + if (groupLevel < 0) { + throw (new RealmException("There is an attempt to close more than the number of groups created")); + } + + //If there is a not(), we are going to exclude the items from currentGroupList from the previousGroup. + //Otherwise, we are confident to replace the elements from currentGroupList into previousGroup + if (isThereAYes) { + groupResults.get(groupLevel).clear(); + groupResults.get(groupLevel).addAll(currentGroupList); + } else { + RealmList previousGroup = groupResults.get(groupLevel); + for (RealmModel realmModel : currentGroupList) { + if (previousGroup.contains(realmModel)) { + previousGroup.remove(realmModel); + } + } + } + } + + private void onTopGroupClose(){ + onCloseGroupClause(); + + if( groupLevel > 0 ){ + throw( new RealmException("Required to close all groups. Current group level is " + groupLevel )); + } + } + + private Boolean executeGroupQuery(Query query ){ + + Boolean used = false; + + switch ( query.getCondition() ){ + + case Compare.startTopGroup: + onTopGroupBegin( (RealmList) query.getArgs()[0] ); + used = true; + break; + case Compare.startGroup: + onBeginGroupClause(); + used = true; + break; + case Compare.or: + onOrClause(); + used = true; + break; + case Compare.not: + onNotClause(); + used = true; + break; + case Compare.endGroup: + onCloseGroupClause(); + used = true; + break; + case Compare.endTopGroup: + onTopGroupClose(); + used = true; + break; + } + + return used; + } + + public Class getClazz() { + return clazz; + } + + public void appendQuery( Query query ){ + queries.add( query ); + } + + public RealmList getQueryList(){ + + if( !queryAndList.isEmpty() && !queryAndList.get( groupLevel )){ + return groupResults.get(groupLevel-1); + } + + return groupResults.get(groupLevel); + } + + public ArrayList getQueries(){ + return queries; + } + + @Override + public QueryTracker clone(){ + QueryTracker cloned = new QueryTracker( this.clazz ); + + if( parentRealmList != null ){ + cloned.parentRealmList = parentRealmList; + }else{ + cloned.parentRealmList = getQueryList(); + } + + return cloned; + } + + public RealmQuery getRealmQuery() { + return realmQuery; + } + + public RealmResults getRealmResults() { + return realmResults; + } + + public RealmList getParentRealmList() { + return parentRealmList; + } + + public RealmResults rewind(){ + ArrayList queries = getQueries(); + RealmList searchList; + + for ( Query query: queries ){ + + if( !executeGroupQuery( query ) ){ + + if( groupLevel >=1 ){ + query.setAsTrue( queryYesList.get(groupLevel) ); + } + + if( query.getCondition() == Compare.sort ){ + searchList = new QuerySort().perform( query, getQueryList() ); + setQueryList( searchList ); + + } else if( query.getCondition() == Compare.distinct ){ + searchList = new QueryDistinct().perform( query, getQueryList() ); + setQueryList( searchList ); + } else{ + searchList = new QuerySearch().search( query, getQueryList() ); + setQueryList( searchList ); + } + + //set back current level to yes, instead of no. + queryYesList.set( groupLevel, true ); + } + } + + return realmResults; + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/utils/RealmAnnotationUtil.java b/src/main/java/info/juanmendez/mockrealm/utils/RealmAnnotationUtil.java new file mode 100644 index 0000000..bf3a22a --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/utils/RealmAnnotationUtil.java @@ -0,0 +1,60 @@ +package info.juanmendez.mockrealm.utils; + +import org.powermock.reflect.Whitebox; + +import java.lang.reflect.Field; + +import info.juanmendez.mockrealm.dependencies.RealmStorage; +import info.juanmendez.mockrealm.models.RealmAnnotation; +import io.realm.RealmModel; + +/** + * Created by Juan Mendez on 4/8/2017. + * www.juanmendez.info + * contact@juanmendez.info + */ + +public class RealmAnnotationUtil { + + public static Object findPrimaryKey(RealmModel realmModel ){ + Class clazz = RealmModelUtil.getClass( realmModel); + String primaryField = getPrimaryFieldName( clazz ); + + if( primaryField != null ){ + return Whitebox.getInternalState( realmModel, primaryField ); + } + + return null; + } + + public static String getPrimaryFieldName( Class clazz ){ + RealmAnnotation annotation = RealmStorage.getAnnotationMap().get(clazz ); + + if( annotation == null ) + return null; + + return annotation.getPrimaryField(); + } + + public static Boolean isIndexed( Class clazz, String field ){ + RealmAnnotation annotation = RealmStorage.getAnnotationMap().get(clazz ); + + if( annotation == null ) + return false; + + return annotation.geIndexedFields().contains( field ); + } + + public static Boolean isIgnored( Class clazz, String field ){ + RealmAnnotation annotation = RealmStorage.getAnnotationMap().get(clazz ); + + if( annotation == null ) + return false; + + return annotation.getIgnoredFields().contains( field ); + } + + public static Boolean isIgnored( Field field ){ + return isIgnored( field.getDeclaringClass(), field.getName() ); + } +} diff --git a/src/main/java/info/juanmendez/mockrealm/utils/RealmModelUtil.java b/src/main/java/info/juanmendez/mockrealm/utils/RealmModelUtil.java new file mode 100644 index 0000000..97a72c4 --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/utils/RealmModelUtil.java @@ -0,0 +1,168 @@ +package info.juanmendez.mockrealm.utils; + +import org.powermock.reflect.Whitebox; + +import java.lang.reflect.Field; +import java.util.AbstractList; +import java.util.HashMap; +import java.util.Set; + +import info.juanmendez.mockrealm.dependencies.RealmStorage; +import info.juanmendez.mockrealm.models.RealmListStubbed; +import io.realm.RealmList; +import io.realm.RealmModel; +import io.realm.RealmObject; + +import static org.mockito.Mockito.mockingDetails; + +/** + * Created by Juan Mendez on 3/17/2017. + * www.juanmendez.info + * contact@juanmendez.info + */ + +public class RealmModelUtil { + + private static final String Q = "\""; + private static final String C = ","; + private static final String Op = "{"; + private static final String Cp = "}"; + private static final String Ob = "["; + private static final String Cb = "]"; + + public static Class getClass( Object object ){ + + Class clazz = object.getClass(); + + if( (object instanceof RealmObject && mockingDetails(object).isSpy()) || object instanceof RealmListStubbed ){ + return clazz.getSuperclass(); + } + + return clazz; + } + + /** + * This is a cheap way to save the state of an object. Unfortunately, @Ignore variables cannot be + * tracked due to their nature of their retention policy which is not of Runtime policy type. + * http://stackoverflow.com/questions/4453159/how-to-get-annotations-of-a-member-variable + * @param realmModel object to check variables and values + * @return a json string + */ + public static String getState(Object realmModel){ + + if( realmModel == null ) + return ""; + + String jsonString = ""; + + if( realmModel instanceof AbstractList ){ + + AbstractList abstractList = (AbstractList)realmModel; + + if( abstractList.isEmpty() ){ + return Ob+Cb; + } + + jsonString += Ob; + try{ + for( RealmModel m: abstractList ){ + jsonString += getState(m)+C; + } + } + catch( Exception e ){ + System.err.println( e.getMessage() ); + } + + + jsonString = jsonString.substring(0,jsonString.length()-1); + jsonString+= Cb; + } + else{ + Set fieldSet = Whitebox.getAllInstanceFields(realmModel); + jsonString += Op; + Object currentObject; + + for (Field field: fieldSet) { + + if( !RealmAnnotationUtil.isIgnored(field) ){ + currentObject = Whitebox.getInternalState(realmModel, field.getName() ); + + if(AbstractList.class.isAssignableFrom(field.getType())){ + jsonString+= Q + field.getName() + Q + ":" + getState(currentObject) + C; + } + else + if(RealmModel.class.isAssignableFrom(field.getType())){ + jsonString+= Q + field.getName() + Q + ":" + getState( (RealmModel) currentObject ) + C; + } + else + if (currentObject != null) + { + jsonString+= Q + field.getName() + Q + ":" + Q + currentObject.toString() + Q + C; + } + } + } + + jsonString = jsonString.substring(0,jsonString.length()-1); + jsonString += Cp; + } + + + return jsonString; + } + + + /** + * Make originalRealmModel have the same attribute values from copyRealmModel + * @param originalRealmModel + * @param copyRealmModel + */ + public static RealmModel extend( RealmModel originalRealmModel, RealmModel copyRealmModel ){ + + Set fieldSet = Whitebox.getAllInstanceFields(copyRealmModel); + Object currentObject; + AbstractList copyList, originalList; + + for (Field field: fieldSet) { + + currentObject = Whitebox.getInternalState(copyRealmModel, field.getName() ); + + if( currentObject instanceof AbstractList ){ + copyList = (AbstractList) currentObject; + originalList = (AbstractList) Whitebox.getInternalState(originalRealmModel, field.getName() ); + originalList.clear(); + originalList.addAll( copyList ); + }else{ + Whitebox.setInternalState( originalRealmModel, field.getName(), currentObject ); + } + } + + return originalRealmModel; + } + + public static RealmModel tryToUpdate( RealmModel newRealmModel ){ + + HashMap> realmMap = RealmStorage.getRealmMap(); + Class clazz = RealmModelUtil.getClass(newRealmModel); + + Object newKey, storedKey; + RealmList realmList = realmMap.get(clazz); + + if( !realmList.contains( newRealmModel )){ + + newKey = RealmAnnotationUtil.findPrimaryKey( newRealmModel ); + + if( newKey != null ){ + + for( RealmModel realmModel: realmList ){ + storedKey = RealmAnnotationUtil.findPrimaryKey( realmModel ); + + if( storedKey != null && storedKey.equals( newKey ) ){ + return RealmModelUtil.extend( realmModel, newRealmModel ); + } + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/info/juanmendez/mockrealm/utils/SubscriptionsUtil.java b/src/main/java/info/juanmendez/mockrealm/utils/SubscriptionsUtil.java new file mode 100644 index 0000000..fba2bcf --- /dev/null +++ b/src/main/java/info/juanmendez/mockrealm/utils/SubscriptionsUtil.java @@ -0,0 +1,119 @@ +package info.juanmendez.mockrealm.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import rx.Subscription; +import rx.subscriptions.CompositeSubscription; + +/** + * Created by Juan Mendez on 3/15/2017. + * www.juanmendez.info + * contact@juanmendez.info + * + * A subject has several observers (subjectToObservers) + * An observer is associated with one subject (observerToSubject) + * The subject has a hold of a CompositeSubscription (subjectComposite) + * Each observer added to the subjectComposite has a subscription map associated. (observerSubscriptions) + */ + +public class SubscriptionsUtil { + + private HashMap> subjectToObservers = new HashMap>(); + private HashMap observerToSubject = new HashMap(); + private HashMap subjectComposite = new HashMap<>(); + private HashMap observerSubscriptions = new HashMap<>(); + + private CompositeSubscription getSubjectComposite(S subject ){ + + if( !subjectComposite.containsKey( subject )) + subjectComposite.put( subject, new CompositeSubscription()); + + return subjectComposite.get( subject ); + } + + //get all observers associated with a subject + private ArrayList getSubjectObservers(S subject ){ + + if( !subjectToObservers.containsKey( subject) ){ + subjectToObservers.put( subject, new ArrayList()); + } + + return subjectToObservers.get( subject ); + } + + /** + * add observers' subscription to subjectComposite, and also observerComposite + * also associate the observer with its subscription (observerSubscriptions) + * @param subject + * @param observer + * @param subscription + */ + public void add(S subject, O observer, Subscription subscription ){ + + add( subject, subscription); + observerSubscriptions.put( observer, subscription ); + getSubjectObservers(subject).add( observer ); + observerToSubject.put(observer, subject); + } + + public void add(S subject, Subscription subscription ){ + + CompositeSubscription compositeSubscription = getSubjectComposite( subject ); + compositeSubscription.add( subscription ); + } + + /** + * when you remove the observer from observerComposite, also remove association at observersSubscription + * @param observer + */ + public void remove( O observer ){ + + if( observerToSubject.containsKey( observer ) && observerSubscriptions.containsKey( observer ) ){ + + S subject = observerToSubject.get(observer); + CompositeSubscription compositeSubscription = getSubjectComposite( subject ); + Subscription subscription = observerSubscriptions.get( observer ); + compositeSubscription.remove( subscription ); + + observerSubscriptions.remove(observer); + getSubjectObservers(subject).remove(observer); + observerToSubject.remove(observer); + } + } + + + public void remove(O observer, Subscription subscription ){ + + S subject = observerToSubject.get(observer); + CompositeSubscription compositeSubscription = getSubjectComposite( subject ); + compositeSubscription.remove( subscription ); + } + + public void removeAll( S subject ){ + CompositeSubscription compositeSubscription = getSubjectComposite( subject ); + ArrayList observers = getSubjectObservers(subject); + + compositeSubscription.clear(); + subjectComposite.remove( subject ); + + for (O observer: observers) { + observerSubscriptions.remove(observer); + observerToSubject.remove(observer); + } + + observers.clear(); + subjectToObservers.remove( subject ); + } + + public void removeAll(){ + + subjectToObservers.size(); + for(Iterator>> it = subjectToObservers.entrySet().iterator(); it.hasNext(); ){ + removeAll( it.next().getKey() ); + } + subjectToObservers.size(); + } +} \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml new file mode 100644 index 0000000..49fc91e --- /dev/null +++ b/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + library +