├── annotations ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ ├── annotations │ ├── Body.java │ ├── Parameter.java │ ├── ResultType.java │ ├── DescribeClass.java │ ├── DescribeTypes.java │ ├── ConversionStrategy.java │ ├── NotBindable.java │ ├── RPCMethod.java │ ├── Describe.java │ ├── DefaultParameters.java │ ├── DefaultParameter.java │ └── RPCMethodScope.java │ └── clearnet │ └── annotations │ ├── Body.java │ ├── ResultType.java │ ├── Parameter.java │ ├── ConversionStrategy.java │ ├── NotBindable.java │ ├── RPCMethod.java │ ├── DefaultParameters.java │ ├── DefaultParameter.java │ └── RPCMethodScope.java ├── clearnet ├── .gitignore ├── src │ ├── test │ │ └── java │ │ │ └── clearnet │ │ │ ├── help │ │ │ ├── TestObject.kt │ │ │ ├── TestRequestsForSingleScope.java │ │ │ ├── TestConverterExecutor.kt │ │ │ ├── executors.kt │ │ │ ├── TestCacheProvider.kt │ │ │ ├── TestRequestCallback.kt │ │ │ ├── TrampolineExecutor.kt │ │ │ ├── CoreBlocksTest.kt │ │ │ ├── GsonTestSerializer.kt │ │ │ ├── BatchTestRequestExecutor.kt │ │ │ ├── stubs.kt │ │ │ ├── TestCoreBlocks.kt │ │ │ └── TestRequests.java │ │ │ ├── StrategyMergerTest.kt │ │ │ ├── SubscribeOnRequestsTest.kt │ │ │ ├── TasksAutoBindSyncTest.kt │ │ │ ├── SimpleHeaderObserverTest.kt │ │ │ ├── TasksAutoBindTest.kt │ │ │ ├── RPCRequestBuildingTest.kt │ │ │ ├── InvocationStrategyTest.kt │ │ │ ├── ConversionStrategiesTest.kt │ │ │ └── BatchRequestTest.kt │ └── main │ │ └── java │ │ └── clearnet │ │ ├── error │ │ ├── InterruptFlowRequest.kt │ │ ├── HTTPCodeError.kt │ │ ├── UnknownExternalException.kt │ │ ├── NetworkException.java │ │ ├── ResponseErrorException.java │ │ ├── ConversionException.java │ │ ├── ValidationException.java │ │ └── ClearNetworkException.java │ │ ├── blocks │ │ ├── InitialBlock.kt │ │ ├── DeliverErrorBlock.kt │ │ ├── DeliverResultBlock.kt │ │ ├── SaveToCacheBlock.kt │ │ ├── GetFromCacheBlock.kt │ │ └── GetFromNetBlock.kt │ │ ├── annotations │ │ ├── NoBatch.java │ │ └── InvocationStrategy.java │ │ ├── InvocationBlockType.kt │ │ ├── known_conversion_strategies.kt │ │ ├── RPCRequest.kt │ │ ├── utils.kt │ │ ├── SimpleHeadersObserver.kt │ │ ├── InvocationStrategy.kt │ │ ├── Wrapper.java │ │ ├── models.kt │ │ ├── CoreTask.kt │ │ ├── Core.kt │ │ ├── interfaces.kt │ │ └── ExecutorWrapper.kt └── build.gradle ├── processors ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── processors │ ├── BaseProcessor.java │ └── RpcResourcesGenerator.java ├── clearnetandroid ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── clearnet │ │ │ └── android │ │ │ ├── kutils.kt │ │ │ ├── SqliteCacheProvider.kt │ │ │ ├── CallbackHolder.kt │ │ │ └── NotNullFieldsValidator.kt │ ├── test │ │ └── java │ │ │ └── clearnet │ │ │ └── android │ │ │ ├── help │ │ │ ├── JavaModel.java │ │ │ ├── ImmediateExecutor.kt │ │ │ ├── TestConverterExecutor.kt │ │ │ ├── TestRequests.java │ │ │ ├── GsonTestSerializer.kt │ │ │ └── stubs.kt │ │ │ ├── NotNullKotlinFieldsValidatorTest.kt │ │ │ └── CallbackHolderTest.kt │ └── androidTest │ │ └── java │ │ └── clearnet │ │ └── android │ │ └── SqliteCacheProviderTest.kt ├── CHANGELOG.md ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── README.MD ├── local.properties ├── gradle.properties ├── CHANGELOG.md ├── gradlew.bat ├── gradlew └── LICENCE /annotations/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /clearnet/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /processors/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /clearnetandroid/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':clearnet', ':annotations', ':processors', ':clearnetandroid' 2 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/TestObject.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | data class TestObject(var test: Int) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailru/clearnet/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /clearnetandroid/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Clearnet Android 3 | 4 | -------------------------------------------------------------------------------- /annotations/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | sourceCompatibility = JavaVersion.VERSION_1_7 4 | targetCompatibility = JavaVersion.VERSION_1_7 -------------------------------------------------------------------------------- /clearnetandroid/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Описание 2 | Сетевой пакет clearnet служит для работы приложений с API, в т.ч. по стандарту RPC, поддерживают кеширование и сериализацию данных, работу с потоками. -------------------------------------------------------------------------------- /clearnetandroid/src/test/java/clearnet/android/help/JavaModel.java: -------------------------------------------------------------------------------- 1 | package clearnet.android.help; 2 | 3 | public class JavaModel { 4 | public String required; 5 | public String optional; 6 | } 7 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/TestRequestsForSingleScope.java: -------------------------------------------------------------------------------- 1 | package clearnet.help; 2 | 3 | import annotations.RPCMethodScope; 4 | 5 | @RPCMethodScope("test") 6 | public interface TestRequestsForSingleScope { 7 | void tryIt(); 8 | } 9 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/error/InterruptFlowRequest.kt: -------------------------------------------------------------------------------- 1 | package clearnet.error 2 | 3 | class InterruptFlowRequest(message: String) : ClearNetworkException(message) { 4 | 5 | init { 6 | kind = KIND.INTERRUPT_FLOW_REQUESTED 7 | } 8 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/error/HTTPCodeError.kt: -------------------------------------------------------------------------------- 1 | package clearnet.error 2 | 3 | class HTTPCodeError(val code: Int, val response: String?) : ClearNetworkException("Http code error: $code") { 4 | init { 5 | kind = KIND.HTTP_CODE 6 | } 7 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /clearnetandroid/src/test/java/clearnet/android/help/ImmediateExecutor.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android.help 2 | 3 | import java.util.concurrent.Executor 4 | 5 | object ImmediateExecutor : Executor { 6 | override fun execute(task: Runnable) { 7 | task.run() 8 | } 9 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/error/UnknownExternalException.kt: -------------------------------------------------------------------------------- 1 | package clearnet.error 2 | 3 | class UnknownExternalException (message: String?) : ClearNetworkException(message) { 4 | 5 | init { 6 | kind = ClearNetworkException.KIND.UNKNOWN_EXTERNAL_ERROR 7 | } 8 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/blocks/InitialBlock.kt: -------------------------------------------------------------------------------- 1 | package clearnet.blocks 2 | 3 | import clearnet.InvocationBlockType 4 | import clearnet.interfaces.IInvocationBlock 5 | 6 | object InitialBlock : IInvocationBlock { 7 | override val invocationBlockType = InvocationBlockType.INITIAL 8 | } -------------------------------------------------------------------------------- /processors/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | sourceCompatibility = JavaVersion.VERSION_1_7 3 | targetCompatibility = JavaVersion.VERSION_1_7 4 | 5 | dependencies { 6 | compile project(':annotations') 7 | compile 'com.squareup:javapoet:1.8.0' 8 | compile 'com.google.auto.service:auto-service:1.0-rc3' 9 | } 10 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/error/NetworkException.java: -------------------------------------------------------------------------------- 1 | package clearnet.error; 2 | 3 | import java.io.IOException; 4 | 5 | public class NetworkException extends ClearNetworkException { 6 | { 7 | kind = KIND.NETWORK; 8 | } 9 | 10 | public NetworkException(IOException cause){ 11 | super(cause); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/Body.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.PARAMETER) 10 | public @interface Body { 11 | } 12 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/error/ResponseErrorException.java: -------------------------------------------------------------------------------- 1 | package clearnet.error; 2 | 3 | public class ResponseErrorException extends ClearNetworkException { 4 | public final Object error; 5 | 6 | { 7 | kind = KIND.RESPONSE_ERROR; 8 | } 9 | 10 | public ResponseErrorException(Object error){ 11 | this.error = error; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/Body.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.PARAMETER) 10 | public @interface Body { 11 | } 12 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/annotations/NoBatch.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface NoBatch { 11 | } 12 | -------------------------------------------------------------------------------- /clearnetandroid/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.1.1 2 | - Удаление retrolambda 3 | 4 | # v1.1 5 | - Поддержка clearnet 2.0 6 | 7 | # v1.0.3 8 | - Добавлено inline `wrap` расширение для еще большего сахара 9 | 10 | # v1.0.2 11 | - Добавлены экстеншены на `Callbackholder` для сахарного wrap-инга на котлиновские лямбды 12 | - Добавлены экстеншены `hold` на `Disposable` и `Subscriber` для небольшого сахара -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/TestConverterExecutor.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import clearnet.interfaces.IConverterExecutor 4 | import clearnet.model.PostParams 5 | 6 | class TestConverterExecutor : IConverterExecutor { 7 | var lastParams: PostParams? = null 8 | 9 | override fun executePost(postParams: PostParams) { 10 | lastParams = postParams 11 | } 12 | } -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/Parameter.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.PARAMETER) 10 | public @interface Parameter { 11 | String value(); 12 | } 13 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/ResultType.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface ResultType { 11 | Class value(); 12 | } 13 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/executors.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import java.util.concurrent.Executor 4 | 5 | object ImmediateExecutor : Executor { 6 | override fun execute(task: Runnable) { 7 | task.run() 8 | } 9 | } 10 | 11 | object MultiThreadExecutor : Executor { 12 | override fun execute(command: Runnable) { 13 | Thread(command).start() 14 | } 15 | } -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/DescribeClass.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target(ElementType.TYPE) 10 | @Retention(RetentionPolicy.SOURCE) 11 | public @interface DescribeClass { 12 | Class types(); 13 | } 14 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/DescribeTypes.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target(ElementType.FIELD) 10 | @Retention(RetentionPolicy.SOURCE) 11 | public @interface DescribeTypes { 12 | String[] value(); 13 | } 14 | -------------------------------------------------------------------------------- /clearnetandroid/src/test/java/clearnet/android/help/TestConverterExecutor.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android.help 2 | 3 | import clearnet.interfaces.IConverterExecutor 4 | import clearnet.model.PostParams 5 | 6 | class TestConverterExecutor : IConverterExecutor { 7 | var lastParams: PostParams? = null 8 | 9 | override fun executePost(postParams: PostParams) { 10 | lastParams = postParams 11 | } 12 | } -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/ResultType.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface ResultType { 11 | Class value(); 12 | } 13 | -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/Parameter.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.PARAMETER) 10 | public @interface Parameter { 11 | String value(); 12 | } 13 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/error/ConversionException.java: -------------------------------------------------------------------------------- 1 | package clearnet.error; 2 | 3 | public class ConversionException extends ClearNetworkException { 4 | { 5 | kind = KIND.CONVERSION; 6 | } 7 | 8 | public ConversionException(Throwable cause){ 9 | super(cause); 10 | } 11 | 12 | public ConversionException(String message, Throwable cause){ 13 | super(message, cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Wed Jan 30 12:07:38 MSK 2019 8 | ndk.dir=/Users/t.aliev/Library/Android/sdk/ndk-bundle 9 | sdk.dir=/Users/t.aliev/Library/Android/sdk 10 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/ConversionStrategy.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface ConversionStrategy { 11 | Class value(); 12 | 13 | String parameter() default ""; 14 | } 15 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/NotBindable.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Disallows callbacks to auto or manual binding to running tasks 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | public @interface NotBindable { 14 | } 15 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/RPCMethod.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Represents the full name of an rpc method 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | public @interface RPCMethod { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /clearnetandroid/src/test/java/clearnet/android/help/TestRequests.java: -------------------------------------------------------------------------------- 1 | package clearnet.android.help; 2 | 3 | import annotations.RPCMethodScope; 4 | import clearnet.interfaces.RequestCallback; 5 | import io.reactivex.Observable; 6 | 7 | public interface TestRequests { 8 | @RPCMethodScope("test") 9 | void requestWithCallback(RequestCallback requestCallback); 10 | 11 | @RPCMethodScope("test") 12 | Observable reactiveRequest(); 13 | } 14 | -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/ConversionStrategy.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface ConversionStrategy { 11 | Class value(); 12 | 13 | String parameter() default ""; 14 | } 15 | -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/NotBindable.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Disallows callbacks to auto or manual binding to running tasks 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | public @interface NotBindable { 14 | } 15 | -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/RPCMethod.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Represents the full name of an rpc method 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | public @interface RPCMethod { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/Describe.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.FIELD, ElementType.METHOD}) 9 | @Retention(RetentionPolicy.SOURCE) 10 | public @interface Describe { 11 | /** 12 | * String res name 13 | */ 14 | String value(); 15 | int order(); 16 | } 17 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/error/ValidationException.java: -------------------------------------------------------------------------------- 1 | package clearnet.error; 2 | 3 | public class ValidationException extends ClearNetworkException { 4 | private final Object model; 5 | 6 | { 7 | kind = KIND.VALIDATION; 8 | } 9 | 10 | public ValidationException(String message, Object model) { 11 | super(message); 12 | this.model = model; 13 | } 14 | 15 | public Object getModel() { 16 | return model; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/DefaultParameters.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Container for all {@link DefaultParameter} annotations 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | public @interface DefaultParameters { 14 | DefaultParameter[] value(); 15 | } 16 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/blocks/DeliverErrorBlock.kt: -------------------------------------------------------------------------------- 1 | package clearnet.blocks 2 | 3 | import clearnet.CoreTask 4 | import clearnet.InvocationBlockType 5 | import clearnet.interfaces.* 6 | 7 | object DeliverErrorBlock: IInvocationBlock { 8 | override val invocationBlockType = InvocationBlockType.DELIVER_ERROR 9 | 10 | override fun onEntity(promise: CoreTask.Promise) = with(promise) { 11 | taskRef.deliver(taskRef.getLastErrorResult()) 12 | super.onEntity(promise) 13 | } 14 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/blocks/DeliverResultBlock.kt: -------------------------------------------------------------------------------- 1 | package clearnet.blocks 2 | 3 | import clearnet.CoreTask 4 | import clearnet.InvocationBlockType 5 | import clearnet.interfaces.IInvocationBlock 6 | 7 | object DeliverResultBlock : IInvocationBlock { 8 | override val invocationBlockType = InvocationBlockType.DELIVER_RESULT 9 | 10 | override fun onEntity(promise: CoreTask.Promise) = with(promise) { 11 | taskRef.deliver(taskRef.getLastResult()) 12 | super.onEntity(promise) 13 | } 14 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/TestCacheProvider.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import clearnet.interfaces.ICacheProvider 4 | 5 | class TestCacheProvider : ICacheProvider { 6 | var state: Int = 0 7 | var returnObject = false 8 | 9 | override fun store(key: String, value: String, expiresAfter: Long) { 10 | state += 10 11 | } 12 | 13 | override fun obtain(key: String): String? { 14 | state++ 15 | return if (returnObject) "{\"result\":\"test\"}" else null 16 | } 17 | } -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/DefaultParameters.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Container for all {@link DefaultParameter} annotations 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | public @interface DefaultParameters { 14 | DefaultParameter[] value(); 15 | } 16 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/DefaultParameter.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * It's means object in field result can be error or data depends of success field value 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | public @interface DefaultParameter { 14 | String key(); 15 | String value(); 16 | } 17 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/InvocationBlockType.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | enum class InvocationBlockType { 4 | INITIAL, GET_FROM_CACHE, GET_FROM_NET, SAVE_TO_CACHE, DELIVER_RESULT, DELIVER_ERROR, RESOLVE_ERROR, CHECK_AUTH_TOKEN; 5 | 6 | object Comparator : java.util.Comparator { 7 | override fun compare(p0: InvocationBlockType?, p1: InvocationBlockType?): Int { 8 | return InvocationBlockType.values().indexOf(p0).compareTo(InvocationBlockType.values().indexOf(p1)) 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/DefaultParameter.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * It's means object in field result can be error or data depends of success field value 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target(ElementType.METHOD) 13 | public @interface DefaultParameter { 14 | String key(); 15 | String value(); 16 | } 17 | -------------------------------------------------------------------------------- /annotations/src/main/java/annotations/RPCMethodScope.java: -------------------------------------------------------------------------------- 1 | package annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * The annotation means rpc method name before the dot. 10 | * Name after the dot will be taken from the name of the java interface method. 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({ElementType.METHOD, ElementType.TYPE}) 14 | public @interface RPCMethodScope { 15 | String value(); 16 | 17 | String NO_SCOPE = ""; 18 | } 19 | -------------------------------------------------------------------------------- /annotations/src/main/java/clearnet/annotations/RPCMethodScope.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * The annotation means rpc method name before the dot. 10 | * Name after the dot will be taken from the name of the java interface method. 11 | */ 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Target({ElementType.METHOD, ElementType.TYPE}) 14 | public @interface RPCMethodScope { 15 | String value(); 16 | 17 | String NO_SCOPE = ""; 18 | } 19 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/TestRequestCallback.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import clearnet.error.ClearNetworkException 4 | import clearnet.interfaces.RequestCallback 5 | import java.util.* 6 | import kotlin.collections.ArrayList 7 | 8 | class TestRequestCallback: RequestCallback { 9 | val successes = Collections.synchronizedList(ArrayList()) 10 | val errors = Collections.synchronizedList(ArrayList()) 11 | 12 | override fun onFailure(exception: ClearNetworkException) { 13 | errors += exception 14 | } 15 | 16 | override fun onSuccess(response: T) { 17 | successes += response 18 | } 19 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/TrampolineExecutor.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import java.util.* 4 | import java.util.concurrent.Executor 5 | 6 | class TrampolineExecutor : Executor { 7 | private val queue = LinkedList() 8 | private var execution = false 9 | 10 | @Synchronized 11 | override tailrec fun execute(command: Runnable) { 12 | if (execution) { 13 | queue.add(command) 14 | } else { 15 | execution = true 16 | command.run() 17 | execution = false 18 | val next = queue.poll() 19 | if (next != null) execute(next) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/annotations/InvocationStrategy.java: -------------------------------------------------------------------------------- 1 | package clearnet.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.METHOD) 10 | public @interface InvocationStrategy { 11 | clearnet.InvocationStrategy[] value(); 12 | 13 | long cacheExpiresAfter() default NEVER; 14 | 15 | long MINUTE = 1000 * 60; 16 | long HOUR = MINUTE * 60; 17 | long DAY = HOUR * 24; 18 | long WEEK = DAY * 7; 19 | long NEVER = Long.MAX_VALUE; 20 | } 21 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/CoreBlocksTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import io.reactivex.schedulers.TestScheduler 4 | import org.junit.Before 5 | import java.util.concurrent.TimeUnit 6 | 7 | abstract class CoreBlocksTest { 8 | protected lateinit var coreBlocks: TestCoreBlocks 9 | protected lateinit var testScheduler: TestScheduler 10 | protected var timeT = 0L 11 | 12 | @Before 13 | fun baseSetUp() { 14 | coreBlocks = TestCoreBlocks() 15 | timeT = coreBlocks.getFromNetTimeThreshold 16 | testScheduler = TestScheduler() 17 | } 18 | 19 | protected fun forwardScheduler(){ 20 | testScheduler.advanceTimeBy(timeT, TimeUnit.MILLISECONDS) 21 | } 22 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/error/ClearNetworkException.java: -------------------------------------------------------------------------------- 1 | package clearnet.error; 2 | 3 | public abstract class ClearNetworkException extends Exception { 4 | protected KIND kind; 5 | 6 | public ClearNetworkException(){ 7 | 8 | } 9 | 10 | public ClearNetworkException(String message){ 11 | super(message); 12 | } 13 | 14 | public ClearNetworkException(Throwable cause){ 15 | super(cause); 16 | } 17 | 18 | public ClearNetworkException(String message, Throwable cause){ 19 | super(message, cause); 20 | } 21 | 22 | public KIND getKind(){ 23 | return kind; 24 | } 25 | 26 | public enum KIND { 27 | NETWORK, VALIDATION, CONVERSION, RESPONSE_ERROR, HTTP_CODE, INTERRUPT_FLOW_REQUESTED, UNKNOWN_EXTERNAL_ERROR 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/known_conversion_strategies.kt: -------------------------------------------------------------------------------- 1 | package clearnet.conversion 2 | 3 | import clearnet.interfaces.ConversionStrategy 4 | import clearnet.interfaces.ConversionStrategy.ConversionStrategyError 5 | import clearnet.model.RpcErrorResponse 6 | import clearnet.model.RpcInnerError 7 | import clearnet.model.RpcInnerErrorResponse 8 | import org.json.JSONObject 9 | 10 | open class DefaultConversionStrategy : ConversionStrategy { 11 | override fun checkErrorOrResult(response: JSONObject): String? { 12 | checkOuterError(response) 13 | return response.optString("result") 14 | } 15 | } 16 | 17 | fun ConversionStrategy.checkOuterError(response: JSONObject) { 18 | if (response.has("error")) { 19 | throw ConversionStrategyError(response.optString("error"), RpcErrorResponse::class.java) 20 | } 21 | } -------------------------------------------------------------------------------- /clearnetandroid/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /clearnet/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'kotlin' 3 | 4 | sourceCompatibility = JavaVersion.VERSION_1_7 5 | targetCompatibility = JavaVersion.VERSION_1_7 6 | 7 | buildscript { 8 | 9 | repositories { 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | } 16 | } 17 | 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | dependencies { 23 | compile 'org.json:json:20140107' 24 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 25 | compile project (":annotations") 26 | compile 'io.reactivex.rxjava2:rxkotlin:2.0.3' 27 | 28 | testCompile 'com.google.code.gson:gson:2.8.2' 29 | testCompile 'junit:junit:4.12' 30 | testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 31 | } 32 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/GsonTestSerializer.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import clearnet.interfaces.ISerializer 4 | import com.google.gson.Gson 5 | import java.lang.reflect.Type 6 | 7 | class GsonTestSerializer : ISerializer { 8 | private val gson = Gson() 9 | 10 | override fun serialize(obj: Any?): String = gson.toJson(obj) 11 | 12 | override fun deserialize(body: String?, objectType: Type): Any? { 13 | try { 14 | return chooseBodyDestiny(body, objectType) 15 | } catch (e: Exception) { 16 | throw clearnet.error.ConversionException(e) 17 | } 18 | } 19 | 20 | private fun chooseBodyDestiny(body: String?, objectType: Type): Any? { 21 | if (objectType in listOf(CharSequence::class.java, String::class.java)) { 22 | return body 23 | } else { 24 | return gson.fromJson(body, objectType) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /clearnetandroid/src/test/java/clearnet/android/help/GsonTestSerializer.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android.help 2 | 3 | import clearnet.interfaces.ISerializer 4 | import com.google.gson.Gson 5 | import java.lang.reflect.Type 6 | 7 | class GsonTestSerializer : ISerializer { 8 | private val gson = Gson() 9 | 10 | override fun serialize(obj: Any?): String = gson.toJson(obj) 11 | 12 | override fun deserialize(body: String?, objectType: Type): Any? { 13 | try { 14 | return chooseBodyDestiny(body, objectType) 15 | } catch (e: Exception) { 16 | throw clearnet.error.ConversionException(e) 17 | } 18 | } 19 | 20 | private fun chooseBodyDestiny(body: String?, objectType: Type): Any? { 21 | if (objectType in listOf(CharSequence::class.java, String::class.java)) { 22 | return body 23 | } else { 24 | return gson.fromJson(body, objectType) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | org.gradle.parallel=true 19 | android.enableD8=true 20 | android.enableD8.desugaring=true -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/blocks/SaveToCacheBlock.kt: -------------------------------------------------------------------------------- 1 | package clearnet.blocks 2 | 3 | import clearnet.CoreTask 4 | import clearnet.InvocationBlockType 5 | import clearnet.error.ConversionException 6 | import clearnet.interfaces.ICacheProvider 7 | import clearnet.interfaces.IInvocationBlock 8 | 9 | class SaveToCacheBlock( 10 | private val cacheProvider: ICacheProvider 11 | ) : IInvocationBlock { 12 | override val invocationBlockType = InvocationBlockType.SAVE_TO_CACHE 13 | 14 | override fun onEntity(promise: CoreTask.Promise) = with(promise) { 15 | try { 16 | taskRef.getLastResult().plainResult?.let { 17 | cacheProvider.store( 18 | taskRef.cacheKey, 19 | it, 20 | taskRef.postParams.expiresAfter 21 | ) 22 | } 23 | } catch (e: ConversionException) { 24 | // todo log error 25 | e.printStackTrace() 26 | } 27 | promise.next(invocationBlockType) 28 | } 29 | } -------------------------------------------------------------------------------- /processors/src/main/java/processors/BaseProcessor.java: -------------------------------------------------------------------------------- 1 | package processors; 2 | 3 | import com.squareup.javapoet.JavaFile; 4 | import com.squareup.javapoet.TypeSpec; 5 | 6 | import java.io.IOException; 7 | 8 | import javax.annotation.processing.AbstractProcessor; 9 | import javax.lang.model.element.Element; 10 | import javax.tools.Diagnostic; 11 | 12 | public abstract class BaseProcessor extends AbstractProcessor { 13 | void error(Element e, String msg) { 14 | processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, e); 15 | } 16 | 17 | 18 | void warning(Element e, String msg) { 19 | processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, msg, e); 20 | } 21 | 22 | protected void writeFile(String pack, TypeSpec typeSpec) { 23 | try { 24 | JavaFile javaFile = JavaFile.builder(pack, typeSpec).build(); 25 | javaFile.writeTo(processingEnv.getFiler()); 26 | } catch (IOException e) { 27 | throw new RuntimeException(e); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /clearnetandroid/src/test/java/clearnet/android/help/stubs.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android.help 2 | 3 | import clearnet.interfaces.* 4 | import java.util.* 5 | 6 | object BodyValidatorStub : IBodyValidator { 7 | override fun validate(body: Any?) {} 8 | } 9 | 10 | object CacheProviderStub : ICacheProvider { 11 | override fun store(key: String, value: String, expiresAfter: Long) {} 12 | override fun obtain(key: String): String? = null 13 | } 14 | 15 | object HeadersProviderStub : HeaderProvider { 16 | override fun obtainHeadersList(): Map = emptyMap() 17 | } 18 | 19 | open class RequestExecutorStub : IRequestExecutor { 20 | override fun executeGet(headers: Map, queryParams: Map): Pair> = "" to emptyMap() 21 | 22 | override fun executePost(body: String, headers: Map, queryParams: Map): Pair> { 23 | return Pair("{\"id\":1, \"result\":\"test\"}", Collections.emptyMap()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /clearnetandroid/src/main/java/clearnet/android/kutils.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android 2 | 3 | import android.content.ContentValues 4 | import java.lang.reflect.AccessibleObject 5 | import kotlin.reflect.KProperty 6 | import kotlin.reflect.jvm.isAccessible 7 | 8 | inline fun T.synchronized(block: T.() -> R): R = kotlin.synchronized(this) { block() } 9 | 10 | fun KProperty.Getter.callWithAccessibility(vararg args: Any?): T { 11 | val isAccessible = property.isAccessible 12 | if (!isAccessible) property.isAccessible = true 13 | val isGetterAccessible = this.isAccessible 14 | if (!isGetterAccessible) this.isAccessible = true 15 | val value = this.call(*args) 16 | this.isAccessible = isGetterAccessible 17 | property.isAccessible = isAccessible 18 | return value 19 | } 20 | 21 | inline fun T.doWithAccessibility(action: T.() -> R): R { 22 | val old = isAccessible 23 | isAccessible = true 24 | val result = action() 25 | isAccessible = old 26 | return result 27 | } 28 | 29 | operator fun ContentValues.set(key: String, value: String?) = this.put(key, value) 30 | operator fun ContentValues.set(key: String, value: Long?) = this.put(key, value) 31 | operator fun ContentValues.set(key: String, value: Int) = this.put(key, value) -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/blocks/GetFromCacheBlock.kt: -------------------------------------------------------------------------------- 1 | package clearnet.blocks 2 | 3 | import clearnet.CoreTask 4 | import clearnet.InvocationBlockType 5 | import clearnet.error.ConversionException 6 | import clearnet.interfaces.ICacheProvider 7 | import clearnet.interfaces.IInvocationBlock 8 | import clearnet.interfaces.ISerializer 9 | 10 | class GetFromCacheBlock( 11 | private val cacheProvider: ICacheProvider, 12 | private val converter: ISerializer 13 | ) : IInvocationBlock { 14 | override val invocationBlockType = InvocationBlockType.GET_FROM_CACHE 15 | 16 | override fun onEntity(promise: CoreTask.Promise) = with(promise) { 17 | val responseString = cacheProvider.obtain(taskRef.cacheKey) 18 | 19 | if (responseString != null) { 20 | try { 21 | setResult( 22 | converter.deserialize(responseString, taskRef.postParams.resultType), 23 | responseString, 24 | invocationBlockType 25 | ) 26 | return 27 | } catch (e: ConversionException) { 28 | // todo remove cache item 29 | // todo log exception 30 | e.printStackTrace() 31 | } 32 | } 33 | 34 | pass(invocationBlockType) 35 | } 36 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.1.1 2 | - Последнии актуальные изменения. 3 | 4 | # v2.1 5 | - Добавлена стратегия `REQUEST_EXCLUDE_CACHE`, исключающая получения данных из кэша. 6 | 7 | # v2.0 8 | - Интерфейсы api теперь имеют поддержку rx. `RequestCallback` поддерживается, но помечена устаревшей 9 | - Ядро полностью переписано с использованием rx 10 | - Резул****ьтат выполнения `InvocationBlock`'а теперь выставляется не в задачу, а в promise на эту задачу. Сама задача неизменяема 11 | - В CoreTask больше нет методов `sleep` и `wakeUp`. Каждый блок теперь работает с задачами асинхронно и обязан вызвать один из методов `Promise` для сообщения результата выполнения 12 | - В `InvocationBlock` теперь вызывается только один из методов `onEntity` или `onQueueConsumed` в зависимости от выбранного алгоритма выполнения блока 13 | - Из интерфейса `IConverterExecutor` удалён метод `executePostSequence`. Возможность вручную собрать связку теперь окончательно убрана. `RequestMerger` также помечен как устаревший 14 | - Задачи теперь поддерживают признак завершения 15 | 16 | # v1.1 17 | 18 | - Ядро пакета и `RequestExecutor` больше не привязаны к стандарту RPC 19 | - Текущий `ExecutorWrapper` теперь предназначен только для RPC, для других стандартов требуется описать свой и свои `PostParams` 20 | - Максимальное количество запросов в batch'е теперь устанавливается для каждой сущности сервер апи 21 | - С помощью аннотации `@NoBatch` можно указать, чтоб запрос не попадал в связки -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/RPCRequest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import java.util.HashMap 4 | import java.util.concurrent.atomic.AtomicLong 5 | 6 | class RPCRequest(val method: String) { 7 | var id: Long = idCounter.incrementAndGet() 8 | private val jsonrpc = "2.0" 9 | var params: Any? = null 10 | private set 11 | 12 | @Transient private var bodyState: Boolean = false 13 | 14 | 15 | fun addParameter(name: String, value: Any?): RPCRequest { 16 | if (bodyState) throw IllegalStateException("The body already has been set") 17 | if (params == null) params = HashMap() 18 | 19 | @Suppress("UNCHECKED_CAST") 20 | (params as MutableMap).put(name, value) 21 | return this 22 | } 23 | 24 | fun setParamsBody(paramsBody: Any?) { 25 | if (params != null) throw IllegalStateException("The params already have been set") 26 | params = paramsBody 27 | bodyState = true 28 | } 29 | 30 | override fun equals(other: Any?): Boolean { 31 | if (this === other) return true 32 | if (other !is RPCRequest) return false 33 | 34 | if (method != other.method) return false 35 | if (params != other.params) return false 36 | 37 | return true 38 | } 39 | 40 | override fun hashCode() = method.hashCode() 41 | 42 | companion object { 43 | private val idCounter = AtomicLong() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/BatchTestRequestExecutor.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import clearnet.interfaces.IRequestExecutor 4 | import com.google.gson.Gson 5 | import org.json.JSONArray 6 | import org.json.JSONObject 7 | 8 | open class BatchTestRequestExecutor : IRequestExecutor { 9 | override fun executeGet(headers: Map, queryParams: Map): Pair> { 10 | throw UnsupportedOperationException("No implementation") 11 | } 12 | 13 | override fun executePost(body: String, headers: Map, queryParams: Map): Pair> { 14 | val result: Any 15 | if(body.startsWith("{")){ 16 | val requestObject = JSONObject(body) 17 | result = mapOf( 18 | "result" to "test0", 19 | "id" to requestObject.getInt("id") 20 | ) 21 | } else { 22 | val array = JSONArray(body) 23 | 24 | result = mutableListOf>() 25 | for (i in 0 until array.length()) { 26 | result += mapOf( 27 | "result" to "test$i", 28 | "id" to array.getJSONObject(i).getInt("id") 29 | ) 30 | } 31 | } 32 | 33 | return Pair( 34 | Gson().toJson(result), 35 | mapOf("testHeader" to "test", "testHeader2" to "test2") 36 | ) 37 | } 38 | } -------------------------------------------------------------------------------- /clearnetandroid/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 7 | } 8 | } 9 | 10 | apply plugin: 'com.android.library' 11 | apply plugin: 'kotlin-android' 12 | apply plugin: 'kotlin-kapt' 13 | 14 | android { 15 | compileSdkVersion sdk_version 16 | buildToolsVersion build_tools_version 17 | 18 | defaultConfig { 19 | minSdkVersion 17 20 | targetSdkVersion sdk_version 21 | versionCode 1 22 | versionName "1.0" 23 | 24 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | } 34 | 35 | dependencies { 36 | 37 | implementation project(":clearnet") 38 | 39 | api "com.android.support:appcompat-v7:27.1.1" 40 | 41 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 42 | compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 43 | 44 | compile 'io.reactivex.rxjava2:rxkotlin:2.0.3' 45 | 46 | testImplementation 'junit:junit:4.12' 47 | testCompile 'com.google.code.gson:gson:2.8.2' 48 | testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 49 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 50 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 51 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/utils.kt: -------------------------------------------------------------------------------- 1 | package clearnet.utils 2 | 3 | import clearnet.interfaces.ICallbackStorage 4 | import clearnet.interfaces.RequestCallback 5 | import clearnet.interfaces.Subscription 6 | import java.util.* 7 | 8 | 9 | class Subscriber( 10 | private val callbackStorage: ICallbackStorage, 11 | private val method: String 12 | ) { 13 | private val others: MutableList> = LinkedList() 14 | 15 | fun subscribe(callback: RequestCallback) = subscribe(callback, false) 16 | 17 | fun subscribeOnce(callback: RequestCallback): Subscription = subscribe(callback, true) 18 | 19 | fun mergeWith(another: Subscriber) = apply { 20 | others += another 21 | others += another.others 22 | } 23 | 24 | operator fun plusAssign(another: Subscriber) { 25 | mergeWith(another) 26 | } 27 | 28 | private fun subscribe(callback: RequestCallback, flag: Boolean): Subscription { 29 | val subscription = CompoundSubscription() 30 | subscription += callbackStorage.subscribe(method, callback, flag) 31 | others.forEach { 32 | subscription += callbackStorage.subscribe(it.method, callback, flag) 33 | } 34 | return subscription 35 | } 36 | } 37 | 38 | class CompoundSubscription : Subscription { 39 | val subscriptions = LinkedList() 40 | 41 | @Synchronized 42 | override fun unsubscribe() { 43 | subscriptions.forEach { 44 | it.unsubscribe() 45 | } 46 | 47 | subscriptions.clear() 48 | } 49 | 50 | @Synchronized 51 | operator fun plusAssign(subscription: Subscription) { 52 | subscriptions += subscription 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /clearnetandroid/src/androidTest/java/clearnet/android/SqliteCacheProviderTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | import junit.framework.TestCase.assertNull 6 | import org.junit.After 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class SqliteCacheProviderTest { 14 | lateinit var cacheProvider: SqliteCacheProvider 15 | 16 | @Before 17 | fun setUp(){ 18 | cacheProvider = SqliteCacheProvider(InstrumentationRegistry.getTargetContext(), "test_cache" + System.currentTimeMillis(), 1) 19 | } 20 | 21 | @After 22 | fun tearDown(){ 23 | cacheProvider.clear() 24 | } 25 | 26 | @Test 27 | fun storeAndGetTest() { 28 | cacheProvider.store("1", "val1", 1000000) 29 | cacheProvider.store("2", "val2", 1000000) 30 | assertEquals("val1", cacheProvider.obtain("1")) 31 | assertEquals("val2", cacheProvider.obtain("2")) 32 | assertNull(cacheProvider.obtain("3")) 33 | } 34 | 35 | @Test 36 | fun replaceTest(){ 37 | cacheProvider.store("1", "val1", 1000000) 38 | cacheProvider.store("1", "val2", 1000000) 39 | assertEquals("val2", cacheProvider.obtain("1")) 40 | } 41 | 42 | @Test 43 | fun expirationTest(){ 44 | cacheProvider.store("1", "val1", 10) 45 | Thread.sleep(15) 46 | assertNull(cacheProvider.obtain("1")) 47 | } 48 | 49 | @Test 50 | fun clearTest(){ 51 | cacheProvider.store("1", "val1", 1000000) 52 | cacheProvider.clear() 53 | assertNull(cacheProvider.obtain("1")) 54 | } 55 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/SimpleHeadersObserver.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.interfaces.HeaderListener 4 | import clearnet.interfaces.HeaderObserver 5 | import clearnet.interfaces.Subscription 6 | 7 | class SimpleHeadersObserver : HeaderObserver { 8 | private val headerListeners = ArrayList() 9 | 10 | override fun register(method: String, listener: HeaderListener, vararg header: String): Subscription { 11 | val subscription = HeaderSubscription(method, header.asList(), listener) 12 | synchronized(headerListeners){ 13 | headerListeners.add(subscription) 14 | } 15 | 16 | return object : Subscription { 17 | override fun unsubscribe() { 18 | synchronized(headerListeners){ 19 | headerListeners.remove(subscription) 20 | } 21 | } 22 | } 23 | } 24 | 25 | fun propagateHeaders(method: String, headers: Map){ 26 | synchronized(headerListeners){ 27 | headerListeners.filter { 28 | method == it.method 29 | }.forEach { subscription -> 30 | val headersList = if (!subscription.allHeaders()) { 31 | headers.entries.filter { subscription.names.contains(it.key) } 32 | } else { 33 | headers.entries 34 | } 35 | 36 | headersList.forEach { 37 | subscription.listener.onNewHeader(method, it.key, it.value) 38 | } 39 | } 40 | } 41 | } 42 | 43 | private data class HeaderSubscription(val method: String, val names: Collection, val listener: HeaderListener){ 44 | fun allHeaders() = names.isEmpty() 45 | } 46 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/InvocationStrategy.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.InvocationBlockType.* 4 | import clearnet.interfaces.IInvocationStrategy 5 | import clearnet.interfaces.IInvocationStrategy.Decision 6 | 7 | 8 | enum class InvocationStrategy( 9 | override val algorithm: Map, 10 | override val metaData: Map = emptyMap() 11 | ): IInvocationStrategy { 12 | NO_CACHE(mapOf( 13 | INITIAL to Decision(GET_FROM_NET), 14 | CHECK_AUTH_TOKEN to Decision(GET_FROM_NET, DELIVER_ERROR), 15 | GET_FROM_NET to Decision(DELIVER_RESULT, RESOLVE_ERROR) 16 | )), 17 | 18 | REQUEST_EXCLUDE_CACHE(mapOf( 19 | INITIAL to Decision(GET_FROM_NET), 20 | CHECK_AUTH_TOKEN to Decision(GET_FROM_NET, DELIVER_ERROR), 21 | GET_FROM_NET to Decision(arrayOf(DELIVER_RESULT, SAVE_TO_CACHE), RESOLVE_ERROR) 22 | )), 23 | 24 | PRIORITY_REQUEST(mapOf( 25 | INITIAL to Decision(GET_FROM_NET), 26 | CHECK_AUTH_TOKEN to Decision(GET_FROM_NET, DELIVER_ERROR), 27 | GET_FROM_NET to Decision(arrayOf(DELIVER_RESULT, SAVE_TO_CACHE), GET_FROM_CACHE), 28 | GET_FROM_CACHE to Decision(DELIVER_RESULT, RESOLVE_ERROR) 29 | )), 30 | 31 | PRIORITY_CACHE(mapOf( 32 | INITIAL to Decision(GET_FROM_CACHE), 33 | CHECK_AUTH_TOKEN to Decision(GET_FROM_CACHE, DELIVER_ERROR), 34 | GET_FROM_CACHE to Decision(DELIVER_RESULT, GET_FROM_NET), 35 | GET_FROM_NET to Decision(arrayOf(DELIVER_RESULT, SAVE_TO_CACHE), RESOLVE_ERROR) 36 | )), 37 | 38 | RETRY_IF_NO_NETWORK(emptyMap(), mapOf("retry_network_error" to "true")), 39 | 40 | AUTHORIZED_REQUEST(mapOf( 41 | INITIAL to Decision(CHECK_AUTH_TOKEN) 42 | )); 43 | } 44 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/stubs.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import clearnet.Wrapper 4 | import clearnet.error.ClearNetworkException 5 | import clearnet.interfaces.* 6 | import io.reactivex.disposables.Disposable 7 | import junit.framework.Assert 8 | import java.util.* 9 | 10 | object CallbackHolderStub : ICallbackHolder { 11 | override fun hold(disposable: Disposable) {} 12 | 13 | override fun init() {} 14 | override fun clear() {} 15 | 16 | override fun createEmpty(type: Class): I = Wrapper.stub(type) as I 17 | override fun wrap(source: I, interfaceType: Class): I = source 18 | } 19 | 20 | object BodyValidatorStub : IBodyValidator { 21 | override fun validate(body: Any?) {} 22 | } 23 | 24 | object CacheProviderStub : ICacheProvider { 25 | override fun store(key: String, value: String, expiresAfter: Long) {} 26 | override fun obtain(key: String): String? = null 27 | } 28 | 29 | object HeadersProviderStub : HeaderProvider { 30 | override fun obtainHeadersList(): Map = emptyMap() 31 | } 32 | 33 | open class RequestCallbackStub : RequestCallback { 34 | override fun onSuccess(response: T) {} 35 | 36 | override fun onFailure(exception: ClearNetworkException) { 37 | Assert.fail("onFailure: " + exception.message) 38 | } 39 | } 40 | 41 | open class RequestExecutorStub : IRequestExecutor { 42 | override fun executeGet(headers: Map, queryParams: Map): Pair> = Pair("", emptyMap()) 43 | 44 | override fun executePost(body: String, headers: Map, queryParams: Map): Pair> { 45 | return Pair("{\"id\":1, \"result\":\"test\"}", Collections.emptyMap()) 46 | } 47 | } 48 | 49 | object SubscriptionStub : Subscription { 50 | override fun unsubscribe() {} 51 | } 52 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/StrategyMergerTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.InvocationBlockType.* 4 | import clearnet.help.* 5 | import clearnet.interfaces.IConverterExecutor 6 | import clearnet.interfaces.Subscription 7 | import clearnet.model.PostParams 8 | import org.junit.Test 9 | import java.util.* 10 | import java.util.concurrent.atomic.AtomicReference 11 | import kotlin.test.assertEquals 12 | import kotlin.test.assertNotNull 13 | import kotlin.test.assertTrue 14 | 15 | class StrategyMergerTest { 16 | @Test 17 | fun mergeTest() { 18 | val lastPostParams = AtomicReference() 19 | 20 | val converterExecutor = object : IConverterExecutor { 21 | override fun executePost(postParams: PostParams) { 22 | lastPostParams.set(postParams) 23 | } 24 | } 25 | 26 | val executorWrapper = ExecutorWrapper(converterExecutor, HeadersProviderStub, GsonTestSerializer()) 27 | val testRequests = executorWrapper.create(TestRequests::class.java, BatchTestRequestExecutor(), Int.MAX_VALUE) 28 | 29 | testRequests.mergeStrategiesTest() 30 | 31 | assertNotNull(lastPostParams.get()) 32 | 33 | val strategy = lastPostParams.get().invocationStrategy 34 | 35 | assertTrue(Arrays.deepEquals(strategy[INITIAL][true], arrayOf(CHECK_AUTH_TOKEN))) 36 | assertTrue(Arrays.deepEquals(strategy[INITIAL][false], emptyArray())) 37 | assertTrue(Arrays.deepEquals(strategy[CHECK_AUTH_TOKEN][true], arrayOf(GET_FROM_CACHE))) 38 | assertTrue(Arrays.deepEquals(strategy[CHECK_AUTH_TOKEN][false], arrayOf(DELIVER_ERROR))) 39 | assertTrue(Arrays.deepEquals(strategy[GET_FROM_CACHE][true], arrayOf(DELIVER_RESULT))) 40 | assertTrue(Arrays.deepEquals(strategy[GET_FROM_CACHE][false], arrayOf(GET_FROM_NET))) 41 | 42 | assertEquals("true", strategy["retry_network_error"]) 43 | } 44 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/SubscribeOnRequestsTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.help.* 4 | import org.junit.Before 5 | import org.junit.Test 6 | import java.util.concurrent.TimeUnit 7 | import java.util.concurrent.atomic.AtomicInteger 8 | import kotlin.test.assertEquals 9 | 10 | class SubscribeOnRequestsTest : CoreBlocksTest() { 11 | private lateinit var core: Core 12 | private lateinit var testRequests: TestRequests 13 | 14 | @Before 15 | fun setup() { 16 | core = Core( 17 | ioExecutor = ImmediateExecutor, 18 | worker = testScheduler, 19 | blocks = *coreBlocks.getAll() 20 | ) 21 | testRequests = ExecutorWrapper(core, HeadersProviderStub, GsonTestSerializer()) 22 | .create(TestRequests::class.java, BatchTestRequestExecutor(), 1, CallbackHolderStub) 23 | } 24 | 25 | @Test 26 | fun subscribe() { 27 | val successes = AtomicInteger() 28 | val subscriptionCalled = AtomicInteger() 29 | 30 | val callback = object : RequestCallbackStub() { 31 | override fun onSuccess(response: String) { 32 | successes.incrementAndGet() 33 | } 34 | } 35 | 36 | val subscription = core.subscribe("test.bindableTask", object : RequestCallbackStub() { 37 | override fun onSuccess(response: String) { 38 | subscriptionCalled.incrementAndGet() 39 | } 40 | }) 41 | 42 | testRequests.bindableTask(1, callback) 43 | forwardScheduler() 44 | assertEquals(1, successes.get()) 45 | assertEquals(1, subscriptionCalled.get()) 46 | 47 | testRequests.notBindableTask(callback) 48 | forwardScheduler() 49 | assertEquals(2, successes.get()) 50 | assertEquals(1, subscriptionCalled.get()) 51 | 52 | testRequests.bindableTask(2, callback) 53 | forwardScheduler() 54 | assertEquals(3, successes.get()) 55 | assertEquals(2, subscriptionCalled.get()) 56 | 57 | subscription.unsubscribe() 58 | 59 | testRequests.bindableTask(3, callback) 60 | forwardScheduler() 61 | assertEquals(4, successes.get()) 62 | assertEquals(2, subscriptionCalled.get()) 63 | } 64 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/TasksAutoBindSyncTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.help.* 4 | import org.junit.Before 5 | import org.junit.Test 6 | import java.util.concurrent.atomic.AtomicInteger 7 | import kotlin.test.assertEquals 8 | 9 | class TasksAutoBindSyncTest : CoreBlocksTest() { 10 | private lateinit var core: Core 11 | private lateinit var testRequests: TestRequests 12 | 13 | @Before 14 | fun setup() { 15 | core = Core( 16 | ioExecutor = ImmediateExecutor, 17 | worker = testScheduler, 18 | blocks = *coreBlocks.getAll() 19 | ) 20 | testRequests = ExecutorWrapper(core, HeadersProviderStub, GsonTestSerializer()) 21 | .create(TestRequests::class.java, BatchTestRequestExecutor(), 1, CallbackHolderStub) 22 | } 23 | 24 | @Test 25 | fun bindAfterSuccess() { 26 | val successes = AtomicInteger() 27 | 28 | val callback = object : RequestCallbackStub() { 29 | override fun onSuccess(response: String) { 30 | successes.incrementAndGet() 31 | } 32 | } 33 | testRequests.bindableTask(1, callback) 34 | forwardScheduler() 35 | assertEquals(1, successes.get()) 36 | 37 | testRequests.bindableTask(1, callback) 38 | forwardScheduler() 39 | assertEquals(2, successes.get()) 40 | } 41 | 42 | @Test 43 | fun bindAfterDeliverResult() { // what happens if new callback is subscribed after result is delivered 44 | val successes = AtomicInteger() 45 | 46 | val callback = object : RequestCallbackStub() { 47 | override fun onSuccess(response: String) { 48 | if (successes.getAndIncrement() == 0) { 49 | testRequests.withCacheBindableTask(this) 50 | } 51 | } 52 | } 53 | 54 | testRequests.withCacheBindableTask(callback) 55 | forwardScheduler() 56 | forwardScheduler() 57 | 58 | assertEquals(2, successes.get()) 59 | } 60 | 61 | @Test 62 | fun reactive() { 63 | val successes = AtomicInteger() 64 | 65 | testRequests.reactiveRequest(1).subscribe { 66 | successes.incrementAndGet() 67 | } 68 | 69 | forwardScheduler() 70 | 71 | assertEquals(1, successes.get()) 72 | } 73 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/TestCoreBlocks.kt: -------------------------------------------------------------------------------- 1 | package clearnet.help 2 | 3 | import clearnet.CoreTask 4 | import clearnet.InvocationBlockType 5 | import clearnet.InvocationBlockType.* 6 | import clearnet.blocks.* 7 | import clearnet.interfaces.* 8 | 9 | open class TestCoreBlocks( 10 | private val converter: ISerializer = GsonTestSerializer(), 11 | private val validator: IBodyValidator = BodyValidatorStub, 12 | private val cacheProvider: ICacheProvider = CacheProviderStub) { 13 | 14 | private val getFromNetBlock by lazy { GetFromNetBlock(validator, converter) } 15 | private val getFromCacheBlock by lazy { GetFromCacheBlock(cacheProvider, converter) } 16 | private val saveToCacheBlock by lazy { SaveToCacheBlock(cacheProvider) } 17 | private val errorsResolverBlock by lazy { 18 | createErrorsResolverBlock() 19 | } 20 | 21 | private val checkAuthTokenBlock by lazy { 22 | createAuthTokenBlock() 23 | } 24 | 25 | val getFromNetTimeThreshold = getFromNetBlock.queueTimeThreshold 26 | 27 | fun getAll(): Array { 28 | return InvocationBlockType.values().map { 29 | getBlock(it) 30 | }.toTypedArray() 31 | } 32 | 33 | fun getHeadersObserver() = getFromNetBlock.getHeadersObserver() 34 | 35 | protected open fun createErrorsResolverBlock(): IInvocationBlock = EmptyErrorsResolverBlock 36 | 37 | protected open fun createAuthTokenBlock(): IInvocationBlock = EmptyAuthTokenBlock 38 | 39 | private fun getBlock(type: InvocationBlockType) = when (type) { 40 | INITIAL -> InitialBlock 41 | GET_FROM_CACHE -> getFromCacheBlock 42 | GET_FROM_NET -> getFromNetBlock 43 | SAVE_TO_CACHE -> saveToCacheBlock 44 | DELIVER_RESULT -> DeliverResultBlock 45 | DELIVER_ERROR -> DeliverErrorBlock 46 | RESOLVE_ERROR -> errorsResolverBlock 47 | CHECK_AUTH_TOKEN -> checkAuthTokenBlock 48 | } 49 | 50 | object EmptyErrorsResolverBlock : IInvocationBlock { 51 | override val invocationBlockType: InvocationBlockType 52 | get() = RESOLVE_ERROR 53 | 54 | override fun onEntity(promise: CoreTask.Promise) { 55 | promise.setNextIndex(DELIVER_ERROR) 56 | } 57 | } 58 | 59 | object EmptyAuthTokenBlock : IInvocationBlock { 60 | override val invocationBlockType = CHECK_AUTH_TOKEN 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/Wrapper.java: -------------------------------------------------------------------------------- 1 | package clearnet; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.lang.reflect.InvocationHandler; 6 | import java.lang.reflect.Method; 7 | import java.lang.reflect.Proxy; 8 | import java.util.concurrent.Executor; 9 | 10 | public class Wrapper { 11 | private final Object lock; 12 | private final Executor executor; 13 | private I source; 14 | 15 | @NotNull 16 | @SuppressWarnings("unchecked") 17 | public static R stub(Class clazz) { 18 | return (R) Proxy.newProxyInstance( 19 | clazz.getClassLoader(), 20 | new Class[]{clazz}, 21 | new InvocationHandler() { 22 | @Override 23 | public Object invoke(Object o, Method method, Object[] objects) throws Throwable { 24 | return null; 25 | } 26 | } 27 | ); 28 | } 29 | 30 | public Wrapper(@NotNull Object lock, @NotNull Executor executor, @NotNull I source) { 31 | this.lock = lock; 32 | this.executor = executor; 33 | this.source = source; 34 | } 35 | 36 | @NotNull 37 | @SuppressWarnings("unchecked") 38 | public I create(Class interfaceType) { 39 | return (I) Proxy.newProxyInstance( 40 | this.getClass().getClassLoader(), 41 | new Class[]{interfaceType}, 42 | handler 43 | ); 44 | } 45 | 46 | public void stop() { 47 | source = null; 48 | } 49 | 50 | private InvocationHandler handler = new InvocationHandler() { 51 | @Override 52 | public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { 53 | synchronized (lock) { 54 | if (source == null) return null; 55 | } 56 | 57 | executor.execute(new Runnable() { 58 | @Override 59 | public void run() { 60 | synchronized (lock) { 61 | if (source == null) return; 62 | try { 63 | method.invoke(source, args); 64 | } catch (Exception e) { 65 | throw new RuntimeException(e); 66 | } 67 | } 68 | } 69 | }); 70 | return null; 71 | } 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/SimpleHeaderObserverTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.interfaces.HeaderListener 4 | import org.junit.Before 5 | import org.junit.Test 6 | import java.util.concurrent.atomic.AtomicInteger 7 | import kotlin.test.* 8 | 9 | class SimpleHeaderObserverTest { 10 | 11 | private lateinit var headerObserver: SimpleHeadersObserver 12 | private val testHeaders = mapOf( 13 | "n1" to "v1", 14 | "n2" to "v2" 15 | ) 16 | 17 | 18 | @Before 19 | fun setup(){ 20 | headerObserver = SimpleHeadersObserver() 21 | } 22 | 23 | 24 | @Test 25 | fun headersDelivered(){ 26 | val called = AtomicInteger() 27 | headerObserver.register("test", object : HeaderListener { 28 | override fun onNewHeader(method: String, name: String, value: String) { 29 | called.incrementAndGet() 30 | assertEquals("test", method) 31 | } 32 | }) 33 | 34 | headerObserver.propagateHeaders("test", testHeaders) 35 | headerObserver.propagateHeaders("test2", testHeaders) 36 | 37 | assertEquals(2, called.get()) 38 | 39 | headerObserver.propagateHeaders("test", testHeaders) 40 | 41 | assertEquals(4, called.get()) 42 | } 43 | 44 | @Test 45 | fun headerFiltering(){ 46 | val called = AtomicInteger() 47 | headerObserver.register("test", object : HeaderListener { 48 | override fun onNewHeader(method: String, name: String, value: String) { 49 | called.incrementAndGet() 50 | assertEquals("test", method) 51 | assertEquals("n1", name) 52 | } 53 | }, "n1") 54 | 55 | headerObserver.propagateHeaders("test", testHeaders) 56 | headerObserver.propagateHeaders("test2", testHeaders) 57 | 58 | assertEquals(1, called.get()) 59 | } 60 | 61 | @Test 62 | fun unsubscription(){ 63 | val called = AtomicInteger() 64 | val subscription = headerObserver.register("test", object : HeaderListener { 65 | override fun onNewHeader(method: String, name: String, value: String) { 66 | called.incrementAndGet() 67 | } 68 | }) 69 | headerObserver.propagateHeaders("test", testHeaders) 70 | 71 | subscription.unsubscribe() 72 | 73 | headerObserver.propagateHeaders("test", testHeaders) 74 | 75 | assertEquals(2, called.get()) 76 | } 77 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /clearnetandroid/src/main/java/clearnet/android/SqliteCacheProvider.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android 2 | 3 | import android.content.ContentValues 4 | import android.content.Context 5 | import android.database.sqlite.SQLiteDatabase 6 | import android.database.sqlite.SQLiteException 7 | import android.database.sqlite.SQLiteOpenHelper 8 | import clearnet.interfaces.ICacheProvider 9 | 10 | open class SqliteCacheProvider(context: Context?, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version), ICacheProvider { 11 | companion object { 12 | private const val T_CACHE = "cache" 13 | private const val C_KEY = "c_key" 14 | private const val C_VALUE = "c_value" 15 | private const val C_EXPIRES = "c_expires" 16 | } 17 | override fun onCreate(db: SQLiteDatabase) { 18 | db.execSQL("CREATE TABLE $T_CACHE ($C_KEY TEXT PRIMARY KEY, $C_VALUE TEXT, $C_EXPIRES INTEGER);") 19 | } 20 | 21 | override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 22 | db.execSQL("DROP TABLE IF EXISTS $T_CACHE") 23 | onCreate(db) 24 | } 25 | 26 | override fun store(key: String, value: String, expiresAfter: Long) { 27 | val values = ContentValues(3) 28 | values[C_KEY] = key 29 | values[C_VALUE] = value 30 | 31 | var expires = System.currentTimeMillis() + expiresAfter 32 | if(expires < expiresAfter) expires = Long.MAX_VALUE 33 | values[C_EXPIRES] = expires 34 | writableDatabase.insertWithOnConflict(T_CACHE, "", values, SQLiteDatabase.CONFLICT_REPLACE) 35 | } 36 | 37 | override fun obtain(key: String): String? { 38 | try { 39 | val cursor = readableDatabase.rawQuery( 40 | "SELECT $C_VALUE FROM $T_CACHE WHERE $C_KEY = ? AND $C_EXPIRES > ${System.currentTimeMillis()} LIMIT 1", 41 | arrayOf(key) 42 | ) 43 | 44 | cursor.use { 45 | return if (cursor.count == 0) { 46 | null 47 | } else { 48 | cursor.moveToFirst() 49 | cursor.getString(cursor.getColumnIndex(C_VALUE)) 50 | } 51 | } 52 | } catch (e: SQLiteException) { 53 | return null 54 | } 55 | } 56 | 57 | fun clean(){ 58 | writableDatabase.execSQL("DELETE FROM $T_CACHE WHERE $C_EXPIRES < ${System.currentTimeMillis()}") 59 | } 60 | 61 | fun clear(){ 62 | writableDatabase.execSQL("DELETE FROM $T_CACHE") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /clearnetandroid/src/main/java/clearnet/android/CallbackHolder.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android 2 | 3 | import android.arch.lifecycle.Lifecycle.Event.ON_CREATE 4 | import android.arch.lifecycle.Lifecycle.Event.ON_DESTROY 5 | import android.arch.lifecycle.LifecycleObserver 6 | import android.arch.lifecycle.OnLifecycleEvent 7 | import clearnet.Wrapper 8 | import clearnet.interfaces.ICallbackHolder 9 | import clearnet.interfaces.Subscription 10 | import io.reactivex.disposables.CompositeDisposable 11 | import io.reactivex.disposables.Disposable 12 | import io.reactivex.schedulers.Schedulers 13 | import java.util.concurrent.Executor 14 | 15 | open class CallbackHolder(val callbackExecutor: Executor) : LifecycleObserver, ICallbackHolder { 16 | override val scheduler = Schedulers.from(callbackExecutor) 17 | 18 | internal val callbackList: MutableList> = ArrayList() //todo temporal visibility until tests will be moved 19 | internal var disposables = CompositeDisposable() 20 | private val subscriptions = ArrayList() 21 | 22 | @OnLifecycleEvent(ON_DESTROY) 23 | override fun clear() { 24 | callbackList.synchronized { 25 | forEach { it.stop() } 26 | clear() 27 | } 28 | 29 | disposables.dispose() 30 | 31 | subscriptions.synchronized { 32 | forEach { it.unsubscribe() } 33 | clear() 34 | } 35 | } 36 | 37 | @OnLifecycleEvent(ON_CREATE) 38 | override fun init() { 39 | if (disposables.isDisposed) disposables = CompositeDisposable() 40 | } 41 | 42 | override fun createEmpty(type: Class): I = Wrapper.stub(type) as I 43 | 44 | override fun wrap(source: I, interfaceType: Class): I { 45 | val wrapper = Wrapper(callbackList, callbackExecutor, source) 46 | callbackList.synchronized { 47 | add(wrapper) 48 | } 49 | return wrapper.create(interfaceType) 50 | } 51 | 52 | override fun hold(disposable: Disposable) { 53 | disposables.add(disposable) 54 | } 55 | 56 | fun hold(subscription: Subscription) { 57 | subscriptions.synchronized { 58 | add(subscription) 59 | } 60 | } 61 | } 62 | 63 | fun Disposable.hold(callbackHolder: CallbackHolder) = apply { 64 | callbackHolder.hold(this) 65 | } 66 | 67 | fun Subscription.hold(callbackHolder: CallbackHolder) = apply { 68 | callbackHolder.hold(this) 69 | } 70 | 71 | inline fun CallbackHolder.wrapBlock(crossinline block: () -> Unit): () -> Unit { 72 | val wrapped = wrap(Runnable { block() }, Runnable::class.java) 73 | return { wrapped.run() } 74 | } 75 | 76 | inline fun CallbackHolder.wrap(source: T) { 77 | wrap(source, T::class.java) 78 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/help/TestRequests.java: -------------------------------------------------------------------------------- 1 | package clearnet.help; 2 | 3 | import annotations.Body; 4 | import annotations.ConversionStrategy; 5 | import annotations.NotBindable; 6 | import annotations.Parameter; 7 | import annotations.RPCMethod; 8 | import annotations.RPCMethodScope; 9 | import clearnet.annotations.InvocationStrategy; 10 | import clearnet.annotations.NoBatch; 11 | import clearnet.conversion.InnerErrorConversionStrategy; 12 | import clearnet.conversion.InnerResultConversionStrategy; 13 | import clearnet.interfaces.RequestCallback; 14 | import io.reactivex.Observable; 15 | 16 | import static clearnet.InvocationStrategy.AUTHORIZED_REQUEST; 17 | import static clearnet.InvocationStrategy.PRIORITY_CACHE; 18 | import static clearnet.InvocationStrategy.PRIORITY_REQUEST; 19 | import static clearnet.InvocationStrategy.RETRY_IF_NO_NETWORK; 20 | 21 | public interface TestRequests { 22 | 23 | // ---- RPCRequestBuildingTest ---- 24 | 25 | @RPCMethodScope("testScope") 26 | void test(); 27 | 28 | @RPCMethod("testScope.test") 29 | void test2(); 30 | 31 | void withoutRPCAnnotation(); 32 | 33 | @RPCMethodScope("test") 34 | void testParams(@Parameter("p1") String p1, @Parameter("p2") int p2, @Parameter("p3") int[] p3); 35 | 36 | @RPCMethodScope("test") 37 | void testBody(@Body int[] values); 38 | 39 | @RPCMethodScope("test") 40 | void multipleBody(@Body String p1, @Body String p2); 41 | 42 | @RPCMethodScope("test") 43 | void unknownArgs(String arg); 44 | 45 | @RPCMethodScope("test") 46 | void paramsAndBodyMixing(@Parameter("p1") int p1, @Body String p2); 47 | 48 | @RPCMethodScope("test") 49 | void paramsAndBodyMixingOnSingleArgument(@Parameter("p1") @Body int p1); 50 | 51 | // ---- BatchRequestTest ---- 52 | 53 | @NotBindable 54 | @RPCMethodScope("test") 55 | void firstOfBatch(RequestCallback callback); 56 | 57 | @RPCMethodScope("test") 58 | void secondOfBatch(RequestCallback callback); 59 | 60 | @RPCMethodScope("test") 61 | @InvocationStrategy(PRIORITY_CACHE) 62 | void forBatchWithPriorityCache(RequestCallback callback); 63 | 64 | @NotBindable 65 | @NoBatch 66 | @RPCMethodScope("test") 67 | void batchNoBatch(RequestCallback callback); 68 | 69 | // ---- CacheStrategyTest ---- 70 | 71 | @RPCMethodScope("test") 72 | void noCache(RequestCallback requestCallback); 73 | 74 | @InvocationStrategy(PRIORITY_REQUEST) 75 | @RPCMethodScope("test") 76 | void priorityRequest(RequestCallback requestCallback); 77 | 78 | @InvocationStrategy(PRIORITY_CACHE) 79 | @RPCMethodScope("test") 80 | void priorityCache(RequestCallback requestCallback); 81 | 82 | 83 | 84 | // ---- SuccessOrErrorResponsesVariantsTest ---- 85 | @RPCMethodScope("test") 86 | void commonResponse(RequestCallback callback); 87 | 88 | @ConversionStrategy(InnerResultConversionStrategy.class) 89 | @RPCMethodScope("test") 90 | void innerResponse(RequestCallback callback); 91 | 92 | @ConversionStrategy(InnerErrorConversionStrategy.class) 93 | @RPCMethodScope("test") 94 | void innerErrorResponse(RequestCallback callback); 95 | 96 | 97 | // ---- TasksSubscriptionTest ---- 98 | @RPCMethodScope("test") 99 | void bindableTask(@Parameter("param") int parameter, RequestCallback callback); 100 | 101 | @NotBindable 102 | @RPCMethodScope("test") 103 | void notBindableTask(RequestCallback callback); 104 | 105 | @RPCMethodScope("test") 106 | @InvocationStrategy(PRIORITY_REQUEST) 107 | void withCacheBindableTask(RequestCallback callback); 108 | 109 | @RPCMethodScope("test") 110 | @InvocationStrategy({PRIORITY_CACHE, AUTHORIZED_REQUEST, RETRY_IF_NO_NETWORK}) 111 | void mergeStrategiesTest(); 112 | 113 | // ---- Reactive ---- 114 | @RPCMethodScope("test") 115 | Observable reactiveRequest(@Parameter("p1") int p1); 116 | } 117 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/TasksAutoBindTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.help.* 4 | import org.junit.Before 5 | import org.junit.Test 6 | import java.util.concurrent.Semaphore 7 | import java.util.concurrent.TimeUnit 8 | import java.util.concurrent.atomic.AtomicInteger 9 | import kotlin.test.assertEquals 10 | import kotlin.test.assertTrue 11 | 12 | class TasksAutoBindTest : CoreBlocksTest() { 13 | private lateinit var core: Core 14 | private lateinit var testRequests: TestRequests 15 | private lateinit var requestExecutor: TestRequestExecutor 16 | 17 | @Before 18 | fun setup() { 19 | core = Core( 20 | ioExecutor = MultiThreadExecutor, 21 | blocks = *coreBlocks.getAll() 22 | ) 23 | requestExecutor = TestRequestExecutor() 24 | testRequests = ExecutorWrapper(core, HeadersProviderStub, GsonTestSerializer()) 25 | .create(TestRequests::class.java, requestExecutor, 1, CallbackHolderStub) 26 | } 27 | 28 | @Test() 29 | fun autoBind() { 30 | val semaphore = Semaphore(3) 31 | semaphore.acquire(3) 32 | 33 | val callback = object : RequestCallbackStub() { 34 | override fun onSuccess(response: String) { 35 | semaphore.release() 36 | } 37 | } 38 | 39 | testRequests.bindableTask(1, callback) 40 | testRequests.bindableTask(1, callback) 41 | testRequests.bindableTask(1, callback) 42 | 43 | waitForTasksOnTestRequestExecutor() 44 | 45 | requestExecutor.semaphore.release(3) 46 | 47 | 48 | assertTrue(semaphore.tryAcquire(3, 100, TimeUnit.MILLISECONDS), "Callback called ${semaphore.availablePermits()} times") 49 | assertEquals(1, requestExecutor.counter.get()) 50 | } 51 | 52 | @Test 53 | fun noBindWithDifferentParams() { 54 | val semaphore = Semaphore(2) 55 | semaphore.acquire(2) 56 | 57 | val callback = object : RequestCallbackStub() { 58 | override fun onSuccess(response: String) { 59 | semaphore.release() 60 | } 61 | } 62 | testRequests.bindableTask(1, callback) 63 | testRequests.bindableTask(2, callback) 64 | 65 | waitForTasksOnTestRequestExecutor() 66 | 67 | requestExecutor.semaphore.release(2) 68 | 69 | assertTrue(semaphore.tryAcquire(2, 100, TimeUnit.MILLISECONDS)) 70 | 71 | assertEquals(2, requestExecutor.counter.get()) 72 | } 73 | 74 | @Test 75 | fun noBindNotBindable() { 76 | val semaphore = Semaphore(2) 77 | semaphore.acquire(2) 78 | 79 | val callback = object : RequestCallbackStub() { 80 | override fun onSuccess(response: String) { 81 | semaphore.release() 82 | } 83 | } 84 | 85 | testRequests.notBindableTask(callback) 86 | testRequests.notBindableTask(callback) 87 | 88 | waitForTasksOnTestRequestExecutor() 89 | 90 | requestExecutor.semaphore.release(2) 91 | 92 | assertTrue(semaphore.tryAcquire(2, 100, TimeUnit.MILLISECONDS)) 93 | assertEquals(2, requestExecutor.counter.get()) 94 | } 95 | 96 | 97 | private fun waitForTasksOnTestRequestExecutor() { 98 | var time = 0 99 | do { 100 | Thread.sleep(10L) 101 | time += 10 102 | } while (!requestExecutor.semaphore.hasQueuedThreads()) 103 | System.out.println("Waiting finished for $time milliseconds") 104 | } 105 | 106 | private open class TestRequestExecutor : BatchTestRequestExecutor() { 107 | var counter = AtomicInteger() 108 | var semaphore = Semaphore(3).apply { 109 | acquire(3) 110 | } 111 | 112 | override fun executePost(body: String, headers: Map, queryParams: Map): Pair> { 113 | semaphore.acquire() 114 | val result = super.executePost(body, headers, queryParams) 115 | counter.incrementAndGet() 116 | return result 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/RPCRequestBuildingTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.help.* 4 | import org.junit.Before 5 | import org.junit.Test 6 | import java.util.* 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertNotNull 9 | import kotlin.test.fail 10 | 11 | class RPCRequestBuildingTest { 12 | 13 | val testConverterExecutor = TestConverterExecutor() 14 | val testRequests: TestRequests = ExecutorWrapper(testConverterExecutor, HeadersProviderStub, GsonTestSerializer()) 15 | .create(TestRequests::class.java, RequestExecutorStub(), Int.MAX_VALUE) 16 | 17 | 18 | @Before 19 | fun prepare() { 20 | testConverterExecutor.lastParams = null 21 | } 22 | 23 | 24 | @Test 25 | fun rpcMethodNaming() { 26 | try { 27 | testRequests.withoutRPCAnnotation() 28 | fail("Exception must be thrown without any RPC annotation") 29 | } catch (ignored: IllegalArgumentException) { 30 | } 31 | 32 | testRequests.test() 33 | assertNotNull(testConverterExecutor.lastParams) 34 | assertNotNull(testConverterExecutor.lastParams!!.requestBody) 35 | var body = testConverterExecutor.lastParams!!.requestBody as RPCRequest 36 | assertEquals("testScope.test", body.method) 37 | 38 | testRequests.test2() 39 | assertNotNull(testConverterExecutor.lastParams) 40 | assertNotNull(testConverterExecutor.lastParams!!.requestBody) 41 | body = testConverterExecutor.lastParams!!.requestBody as RPCRequest 42 | assertEquals("testScope.test", body.method) 43 | } 44 | 45 | @Test 46 | fun rpcRequestWrongArguments() { 47 | try { 48 | testRequests.multipleBody("b1", "b2") 49 | fail("Exception must be thrown if multiple body") 50 | } catch (ignored: IllegalStateException) {} 51 | 52 | try { 53 | testRequests.unknownArgs("arg") 54 | fail("Exception must be thrown if parameter was annotated") 55 | } catch (ignored: IllegalArgumentException) {} 56 | 57 | try { 58 | testRequests.paramsAndBodyMixing(1, "p2") 59 | fail("Exception must be thrown if body and parameter were mixed") 60 | } catch (ignored: IllegalStateException) {} 61 | 62 | try { 63 | testRequests.paramsAndBodyMixingOnSingleArgument(1) 64 | fail("Exception must be thrown if body and parameter were mixed") 65 | } catch (ignored: IllegalStateException) {} 66 | } 67 | 68 | @Test 69 | fun rpcRequestRightBuilding() { 70 | val p3 = intArrayOf(1, 2) 71 | testRequests.testParams("t1", 1, p3) 72 | assertNotNull(testConverterExecutor.lastParams) 73 | assertNotNull(testConverterExecutor.lastParams?.requestBody) 74 | assertNotNull((testConverterExecutor.lastParams!!.requestBody as? RPCRequest)?.params) 75 | val paramsMap: HashMap = (testConverterExecutor.lastParams!!.requestBody as RPCRequest).params as HashMap 76 | assertEquals("t1", paramsMap["p1"]) 77 | assertEquals(1, paramsMap["p2"]) 78 | assertEquals(p3, paramsMap["p3"]) 79 | 80 | testConverterExecutor.lastParams = null 81 | 82 | testRequests.testBody(p3) 83 | assertNotNull(testConverterExecutor.lastParams) 84 | assertNotNull(testConverterExecutor.lastParams?.requestBody) 85 | assertEquals(p3, (testConverterExecutor.lastParams!!.requestBody as RPCRequest).params) 86 | } 87 | 88 | @Test 89 | fun rpcScopeOnFile() { 90 | val testRequest: TestRequestsForSingleScope = ExecutorWrapper(testConverterExecutor, HeadersProviderStub, GsonTestSerializer()) 91 | .create(TestRequestsForSingleScope::class.java, RequestExecutorStub(), Int.MAX_VALUE) 92 | 93 | 94 | testRequest.tryIt() 95 | 96 | assertNotNull(testConverterExecutor.lastParams) 97 | assertNotNull(testConverterExecutor.lastParams!!.requestBody) 98 | val body = testConverterExecutor.lastParams!!.requestBody as RPCRequest 99 | assertEquals("test.tryIt", body.method) 100 | } 101 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/models.kt: -------------------------------------------------------------------------------- 1 | package clearnet.model 2 | 3 | import clearnet.InvocationBlockType 4 | import clearnet.InvocationStrategy 5 | import clearnet.RPCRequest 6 | import clearnet.interfaces.ConversionStrategy 7 | import clearnet.interfaces.IInvocationStrategy 8 | import clearnet.interfaces.IRequestExecutor 9 | import clearnet.interfaces.ISerializer 10 | import io.reactivex.subjects.ReplaySubject 11 | import io.reactivex.subjects.Subject 12 | import java.lang.reflect.Type 13 | 14 | 15 | abstract class PostParams( 16 | val httpRequestType: String, 17 | val requestParams: Map, 18 | open val requestBody: Any?, 19 | val resultType: Type, 20 | val requestExecutor: IRequestExecutor, 21 | val invocationStrategy: MergedInvocationStrategy, 22 | val expiresAfter: Long, 23 | val conversionStrategy: ConversionStrategy, 24 | var headers: Map, 25 | val bindable: Boolean = true, 26 | val maxBatchSize: Int = 1, 27 | val subject: Subject = ReplaySubject.create().toSerialized() 28 | ) { 29 | abstract val requestTypeIdentifier: String 30 | abstract val flatRequest: String 31 | abstract val cacheKey: String 32 | } 33 | 34 | class RpcPostParams( 35 | requestParams: Map, 36 | override val requestBody: RPCRequest, 37 | resultType: Type, 38 | requestExecutor: IRequestExecutor, 39 | invocationStrategy: MergedInvocationStrategy, 40 | expiresAfter: Long, 41 | conversionStrategy: ConversionStrategy, 42 | headers: Map, 43 | bindable: Boolean = true, 44 | maxBatchSize: Int, 45 | serializer: ISerializer) 46 | : PostParams( 47 | "POST", 48 | requestParams, 49 | requestBody, 50 | resultType, 51 | requestExecutor, 52 | invocationStrategy, 53 | expiresAfter, 54 | conversionStrategy, 55 | headers, 56 | bindable, 57 | maxBatchSize 58 | ) { 59 | 60 | override val requestTypeIdentifier = requestBody.method 61 | override val flatRequest: String 62 | override val cacheKey: String 63 | 64 | init { 65 | flatRequest = serializer.serialize(requestBody) 66 | 67 | val id = requestBody.id 68 | requestBody.id = 0 // we have to make this hook for making cache not store actual request id as key 69 | val ccacheKey = serializer.serialize(requestBody) 70 | requestBody.id = id 71 | cacheKey = ccacheKey 72 | } 73 | } 74 | 75 | 76 | class MergedInvocationStrategy(strategies: Array) { 77 | private val algorithm: Map 78 | private val metaData: MutableMap 79 | 80 | 81 | operator fun get(index: InvocationBlockType): IInvocationStrategy.Decision = algorithm[index] 82 | ?: IInvocationStrategy.Decision(emptyArray(), emptyArray()) 83 | 84 | @Synchronized 85 | operator fun get(key: String) = metaData[key] 86 | 87 | @Synchronized 88 | operator fun set(key: String, value: String?) { 89 | if (value == null) metaData.remove(key) 90 | else metaData[key] = value 91 | } 92 | 93 | init { 94 | val mergedAlgorithm = mutableMapOf() 95 | val mergedMeta = mutableMapOf() 96 | strategies.forEach { 97 | mergedAlgorithm.putAll(it.algorithm) 98 | mergedMeta.putAll(it.metaData) 99 | } 100 | algorithm = mergedAlgorithm 101 | metaData = mergedMeta 102 | } 103 | } 104 | 105 | data class RpcErrorResponse(val code: Long?, val message: String?, val data: Any?) { 106 | override fun toString() = "Code: $code, $message. \n $data" 107 | } 108 | 109 | data class RpcInnerError(var message: String?, var error: String?) { 110 | override fun toString() = "$error\n$message" 111 | } 112 | 113 | data class RpcInnerErrorResponse(val error: String?, val error_description: String?) { 114 | override fun toString() = "$error\n$error_description" 115 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/CoreTask.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.error.ClearNetworkException 4 | import clearnet.model.PostParams 5 | import io.reactivex.subjects.ReplaySubject 6 | import java.util.* 7 | import java.util.concurrent.atomic.AtomicLong 8 | import kotlin.Comparator 9 | 10 | class CoreTask internal constructor( 11 | val postParams: PostParams 12 | ) { 13 | val id = idIterator.incrementAndGet() 14 | val cacheKey: String by lazy { postParams.cacheKey } 15 | val requestKey: String by lazy { postParams.flatRequest } 16 | val startTime = System.currentTimeMillis() 17 | 18 | private val inQueues: MutableList = ArrayList(2) 19 | private val results = ReplaySubject.create() 20 | private val delivered = ReplaySubject.create().toSerialized() 21 | 22 | fun getLastResult(): SuccessResult = results.values 23 | .map { it as Result } 24 | .last { !it.isAncillary && it is SuccessResult } as SuccessResult 25 | 26 | // Warning not reactive transformations 27 | fun getLastErrorResult(): ErrorResult = results.values 28 | .map { it as Result } 29 | .last { !it.isAncillary && it is ErrorResult} as ErrorResult 30 | 31 | fun deliver(result: Result) = delivered.onNext(result) 32 | 33 | fun getRequestIdentifier() = postParams.requestTypeIdentifier 34 | 35 | // todo no queues 36 | fun move(from: InvocationBlockType?, to: Array) = synchronized(inQueues) { 37 | if (from != null) inQueues.remove(from) 38 | inQueues.addAll(to) 39 | if (inQueues.isEmpty()) results.onComplete() 40 | } 41 | 42 | @Deprecated("") 43 | fun isFinished() = results.hasComplete() 44 | 45 | private fun resolveNextIndexes(index: InvocationBlockType, isSuccess: Boolean) = postParams.invocationStrategy[index][isSuccess] 46 | 47 | fun respond(method: String, params: String?): Boolean { 48 | return postParams.requestTypeIdentifier == method && (params == null || cacheKey == params) 49 | } 50 | 51 | internal fun observe() = delivered.hide() 52 | 53 | internal fun promise() = Promise().apply { 54 | observe().subscribe(results::onNext) // only elements 55 | } 56 | 57 | companion object { 58 | private val idIterator = AtomicLong() 59 | } 60 | 61 | 62 | open class Result(val nextIndexes: Array, internal val isAncillary: Boolean = true) 63 | 64 | class ErrorResult(val error: ClearNetworkException, nextIndexes: Array) : Result(nextIndexes, false) 65 | 66 | class SuccessResult(val result: kotlin.Any?, val plainResult: kotlin.String?, nextIndexes: kotlin.Array) : Result(nextIndexes, false) 67 | 68 | 69 | 70 | inner class Promise { 71 | private val resultSubject = ReplaySubject.create() 72 | val taskRef = this@CoreTask 73 | internal fun observe() = resultSubject.hide() 74 | 75 | // Unfortunately we must handle null responses 76 | fun setResult(result: Any?, plainResult: String?, from: InvocationBlockType) { 77 | dispatch(SuccessResult(result, plainResult, resolveNextIndexes(from, true))) 78 | } 79 | 80 | fun setError(exception: ClearNetworkException, from: InvocationBlockType) { 81 | dispatch(ErrorResult(exception, resolveNextIndexes(from, false))) 82 | } 83 | 84 | fun next(from: InvocationBlockType, success: Boolean = true) = setNextIndexes(resolveNextIndexes(from, success)) 85 | 86 | fun pass(from: InvocationBlockType) = next(from, false) 87 | 88 | /** 89 | * Edit the InvocationStrategy flow: 90 | * a manual set index will be used instead of InvocationStrategy's indexes 91 | */ 92 | fun setNextIndex(nextIndex: InvocationBlockType) = setNextIndexes(arrayOf(nextIndex)) 93 | 94 | /** 95 | * Edit the InvocationStrategy flow: 96 | * manual set indexes will be used instead of InvocationStrategy's indexes 97 | */ 98 | fun setNextIndexes(nextIndexes: Array) { 99 | dispatch(Result(nextIndexes)) 100 | } 101 | 102 | private fun dispatch(result: Result) { 103 | resultSubject.onNext(result) 104 | resultSubject.onComplete() 105 | } 106 | } 107 | 108 | object ResultsCountComparator : Comparator { 109 | override fun compare(p0: CoreTask, p1: CoreTask) = p0.results.values.size.compareTo(p1.results.values.size) 110 | } 111 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/InvocationStrategyTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.InvocationStrategy.* 4 | import clearnet.error.ClearNetworkException 5 | import clearnet.interfaces.RequestCallback 6 | import clearnet.help.* 7 | import io.reactivex.schedulers.TestScheduler 8 | import org.junit.Before 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | import org.junit.runners.Parameterized 12 | import org.junit.runners.Parameterized.Parameters 13 | import java.io.IOException 14 | import java.util.* 15 | import java.util.concurrent.TimeUnit 16 | import kotlin.test.assertEquals 17 | 18 | @RunWith(Parameterized::class) 19 | class InvocationStrategyTest( 20 | var throwError: Boolean, 21 | var requestExecutorStateExpectation: Int, 22 | var returnObject: Boolean, 23 | var cacheProviderStateExpectation: Int, 24 | var callbackStateExpectation: Int, 25 | var invocationStrategy: clearnet.InvocationStrategy 26 | ) { 27 | 28 | val testScheduler = TestScheduler() 29 | val testCacheProvider = TestCacheProvider() 30 | val testRequestExecutor = TestRequestExecutor() 31 | val invocationBlocks = TestCoreBlocks(cacheProvider = testCacheProvider) 32 | val converterExecutor = Core(ImmediateExecutor, testScheduler, blocks = *invocationBlocks.getAll()) 33 | val testRequests: TestRequests = ExecutorWrapper(converterExecutor, HeadersProviderStub, GsonTestSerializer()) 34 | .create(TestRequests::class.java, testRequestExecutor, Int.MAX_VALUE) 35 | val testCallback = TestCallback() 36 | val timeT = invocationBlocks.getFromNetTimeThreshold 37 | 38 | companion object { 39 | @JvmStatic 40 | @Parameters 41 | fun data(): Collection> { 42 | return listOf( 43 | arrayOf(false, 1, false, 0, 10, NO_CACHE), 44 | arrayOf(false, 1, true, 0, 10, NO_CACHE), 45 | arrayOf(true, 1, false, 0, 1, NO_CACHE), 46 | arrayOf(true, 1, true, 0, 1, NO_CACHE), 47 | 48 | arrayOf(false, 1, false, 10, 10, PRIORITY_REQUEST), 49 | arrayOf(false, 1, true, 10, 10, PRIORITY_REQUEST), 50 | arrayOf(true, 1, false, 1, 1, PRIORITY_REQUEST), 51 | arrayOf(true, 1, true, 1, 10, PRIORITY_REQUEST), 52 | 53 | arrayOf(false, 1, false, 11, 10, PRIORITY_CACHE), 54 | arrayOf(false, 0, true, 1, 10, PRIORITY_CACHE), 55 | arrayOf(true, 1, false, 1, 1, PRIORITY_CACHE), 56 | arrayOf(true, 0, true, 1, 10, PRIORITY_CACHE) 57 | ) 58 | } 59 | } 60 | 61 | @Before 62 | fun setup() { 63 | testRequestExecutor.state = 0 64 | testRequestExecutor.throwError = false 65 | testCacheProvider.state = 0 66 | testCallback.state = 0 67 | } 68 | 69 | @Test 70 | fun test() { 71 | testRequestExecutor.throwError = throwError 72 | testCacheProvider.returnObject = returnObject 73 | 74 | when (invocationStrategy) { 75 | NO_CACHE -> testRequests.noCache(testCallback) 76 | PRIORITY_REQUEST -> testRequests.priorityRequest(testCallback) 77 | PRIORITY_CACHE -> testRequests.priorityCache(testCallback) 78 | } 79 | 80 | testScheduler.advanceTimeBy(timeT, TimeUnit.MILLISECONDS) 81 | testScheduler.advanceTimeBy(timeT, TimeUnit.MILLISECONDS) 82 | 83 | assertEquals(requestExecutorStateExpectation, testRequestExecutor.state, "Strategy: $invocationStrategy") 84 | assertEquals(cacheProviderStateExpectation, testCacheProvider.state, "Strategy: + $invocationStrategy") 85 | assertEquals(callbackStateExpectation, testCallback.state, "Strategy: + $invocationStrategy Exception: ${testCallback.lastException}") 86 | } 87 | 88 | class TestRequestExecutor : RequestExecutorStub() { 89 | var throwError = false 90 | var state = 0 91 | 92 | override fun executePost(body: String, headers: Map, queryParams: Map): Pair> { 93 | state++ 94 | if (throwError) throw IOException() 95 | return Pair("{\"result\":\"test\"}", Collections.emptyMap()) 96 | } 97 | } 98 | 99 | class TestCallback : RequestCallback { 100 | var state = 0; 101 | var lastException: ClearNetworkException? = null 102 | override fun onSuccess(response: String) { 103 | state += 10 104 | } 105 | 106 | override fun onFailure(exception: ClearNetworkException) { 107 | if (exception.kind != ClearNetworkException.KIND.NETWORK) throw exception 108 | state++ 109 | lastException = exception 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /clearnetandroid/src/test/java/clearnet/android/NotNullKotlinFieldsValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android 2 | 3 | import clearnet.android.help.JavaModel 4 | import clearnet.error.ValidationException 5 | import com.google.gson.Gson 6 | import com.google.gson.reflect.TypeToken 7 | import org.junit.Assert.* 8 | import org.junit.Before 9 | import org.junit.Test 10 | 11 | class NotNullKotlinFieldsValidatorTest { 12 | private lateinit var validator: NotNullFieldsValidator 13 | private val gson = Gson() 14 | 15 | @Before 16 | fun setUp() { 17 | validator = NotNullFieldsValidator(true) 18 | } 19 | 20 | @Test 21 | fun testDeserialize() { 22 | var item = convert("right", Item::class.java) 23 | 24 | assertNotNull(item.getPrivateRequired()) 25 | assertNotNull(item.optional) 26 | 27 | item = convert("required", Item::class.java) 28 | assertNotNull(item.getPrivateRequired()) 29 | 30 | val list: ArrayList = convert("arrayRight", ItemsArrayList::class.java) 31 | 32 | assertEquals(list.size.toLong(), 1) 33 | 34 | 35 | val array: Array = convert("arrayRight", object : TypeToken>() {}.rawType as Class>) 36 | assertEquals(array.size.toLong(), 1) 37 | 38 | val map: Map = convert("mapRight", StringToItemMap::class.java) 39 | 40 | assertNull(map["one"]) 41 | item = map["two"] 42 | assertNotNull(item!!.getPrivateRequired()) 43 | assertNotNull(item.optional) 44 | item = map["three"] 45 | assertNotNull(item!!.getPrivateRequired()) 46 | 47 | 48 | var extended = convert("wrong", Included::class.java) 49 | assertNull(extended.item) 50 | 51 | extended = convert("includedRight", Included::class.java) 52 | assertNotNull(extended.item) 53 | assertNotNull(extended.item!!.getPrivateRequired()) 54 | 55 | 56 | try { 57 | convert("wrong", Item::class.java) 58 | fail() 59 | } catch (e: ValidationException) { 60 | 61 | } 62 | 63 | try { 64 | convert("arrayWrong", ItemsArrayList::class.java) 65 | fail() 66 | } catch (e: ValidationException) { 67 | 68 | } 69 | 70 | try { 71 | convert("arrayWrong", object : TypeToken>() {}.rawType as Class>) 72 | fail() 73 | } catch (e: ValidationException) { 74 | 75 | } 76 | 77 | try { 78 | convert("mapWrong", StringToItemMap::class.java) 79 | fail() 80 | } catch (e: ValidationException) { 81 | 82 | } 83 | 84 | try { 85 | convert("includedWrong", Included::class.java) 86 | fail() 87 | } catch (e: ValidationException) { 88 | 89 | } 90 | 91 | try { 92 | convert("wrong", ItemExtended::class.java) 93 | fail() 94 | } catch (e: ValidationException) { 95 | 96 | } 97 | } 98 | 99 | 100 | @Test 101 | fun testPackages() { 102 | validator = NotNullFieldsValidator(true, "another.package") 103 | try { 104 | convert("wrong", Item::class.java, validator) 105 | } catch (e: ValidationException) { 106 | fail() 107 | } 108 | 109 | validator = NotNullFieldsValidator(true, Item::class.java.`package`.name) 110 | try { 111 | convert("wrong", Item::class.java, validator) 112 | fail() 113 | } catch (e: ValidationException) { 114 | } 115 | } 116 | 117 | @Test 118 | fun ignoreJavaClasses() { 119 | try { 120 | convert("wrong", JavaModel::class.java) 121 | } catch (e: ValidationException){ 122 | fail() 123 | } 124 | } 125 | 126 | 127 | @Throws(ValidationException::class) 128 | private fun convert(request: String, type: Class, validator: NotNullFieldsValidator = this.validator) = gson.fromJson(getStringForResponse(request), type).apply { 129 | validator.validate(this) 130 | } 131 | 132 | 133 | private fun getStringForResponse(request: String) = when (request) { 134 | "right" -> "{\"required\":\"yes\", \"optional\":\"no\"}" 135 | "required" -> "{\"required\":\"yes\"}" 136 | "arrayRight" -> "[{\"required\":\"yes\"}]" 137 | "arrayWrong" -> "[{}]" 138 | "includedRight" -> "{\"item\":{\"required\":\"yes\"}}" 139 | "includedWrong" -> "{\"item\":{}}" 140 | "mapRight" -> "{\"one\":null,\"two\":{\"required\":\"yes\", \"optional\":\"no\"},\"three\":{\"required\":\"yes\"}}" 141 | "mapWrong" -> "{\"one\":null,\"two\":{\"optional\":\"no\"},\"three\":{\"required\":\"yes\"}}" 142 | else -> "{}" 143 | } 144 | 145 | 146 | open class Item( 147 | private val required: String, 148 | var optional: String?, 149 | var requiredPrimitive: Int = 0 // must ignore annotation 150 | ) { 151 | fun getPrivateRequired() = required 152 | } 153 | 154 | class ItemExtended( 155 | one: String, two: String?, three: Int, 156 | private val rr: String? 157 | ) : Item(one, two, three) 158 | 159 | class Included { 160 | var item: Item? = null 161 | } 162 | 163 | 164 | class ItemsArrayList : ArrayList() 165 | class StringToItemMap : HashMap() 166 | } -------------------------------------------------------------------------------- /clearnetandroid/src/main/java/clearnet/android/NotNullFieldsValidator.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android 2 | 3 | import clearnet.error.ValidationException 4 | import clearnet.interfaces.IBodyValidator 5 | import java.util.* 6 | import java.util.concurrent.ConcurrentHashMap 7 | import kotlin.reflect.KClass 8 | import kotlin.reflect.KProperty 9 | import kotlin.reflect.full.declaredMemberProperties 10 | import kotlin.reflect.full.isSubclassOf 11 | import kotlin.reflect.full.superclasses 12 | import kotlin.reflect.jvm.javaField 13 | 14 | 15 | /** 16 | * @param throwFirst If true the converter throws an exception when the first error was found 17 | * (recommended for production builds). Otherwise the converter will collect 18 | * error messages and will throw an exceptions with a message that contains 19 | * full list of errors (recommended for debug) 20 | * @param checkingPackages List of packages of objects that the converter must check. 21 | * If it isn't set the converter will check all the objects 22 | * Recommended for optimisation. 23 | */ 24 | class NotNullFieldsValidator(private val throwFirst: Boolean, vararg checkingPackages: String) : IBodyValidator { 25 | private val checkingPackages = Collections.newSetFromMap(ConcurrentHashMap()) 26 | 27 | init { 28 | this.checkingPackages.addAll(Arrays.asList(*checkingPackages)) 29 | } 30 | 31 | @Throws(ValidationException::class) 32 | override fun validate(body: Any?) { 33 | if (body == null) return 34 | 35 | val errors = LinkedList() 36 | 37 | check(body, HashSet(), "|", errors) 38 | 39 | if (!errors.isEmpty()) { 40 | throw ValidationException(errors.joinToString("\n"), body) 41 | } 42 | } 43 | 44 | @Throws(ValidationException::class) 45 | private fun check(obj: Any?, checked: MutableSet, path: String, errors: MutableList) { 46 | if (obj == null) return 47 | checked.add(obj) 48 | 49 | when (obj) { 50 | is Iterable<*> -> { 51 | obj.filter { it != null && !containsPointer(checked, it) } 52 | .forEachIndexed { key, item -> check(item, checked, "$path[$key]", errors) } 53 | } 54 | is Array<*> -> { 55 | obj.filter { it != null && !containsPointer(checked, it) } 56 | .forEachIndexed { key, item -> check(item, checked, "$path[$key]", errors) } 57 | } 58 | is Map<*, *> -> for ((key, value) in obj) { 59 | if (value != null && !containsPointer(checked, value)) 60 | check(value, checked, "$path[$key]", errors) 61 | } 62 | else -> { 63 | val kClass = obj::class 64 | 65 | try { 66 | if (!kClass.isKotlinClass() || checkIsEnum(kClass) || !checkPackage(kClass.java.`package`?.name)) return 67 | checkFields(kClass.declaredMemberProperties, obj, checked, path, errors) 68 | 69 | kClass.superclasses 70 | .filter { kClass.isKotlinClass() && !checkIsEnum(kClass) && checkPackage(kClass.java.`package`?.name) } 71 | .forEach { checkFields(it.declaredMemberProperties, obj, checked, path, errors) } 72 | } catch (e: ValidationException) { 73 | throw e 74 | } catch (e: Throwable) { 75 | throw ValidationException(e.toString(), obj) 76 | } 77 | } 78 | } 79 | } 80 | 81 | @Throws(ValidationException::class) 82 | private fun checkFields(fields: Iterable>, obj: Any, checked: MutableSet, path: String, errors: MutableList) { 83 | for (field in fields) { 84 | var value: Any? = null 85 | val javaField = field.javaField 86 | 87 | value = if (javaField == null) { 88 | field.getter.callWithAccessibility(obj) 89 | } else { // for example, field not exists if the getter is overridden 90 | javaField.doWithAccessibility { get(obj) } 91 | } 92 | 93 | if (!field.returnType.isMarkedNullable) { 94 | if (value == null) { 95 | val message = "Field " + path + PATH_SEPARATOR + field.name + " in " + obj.javaClass.simpleName + " is required" 96 | if (throwFirst) throw ValidationException(message, obj) 97 | else errors.add(message) 98 | } 99 | } 100 | if (value != null && !containsPointer(checked, value!!)) { 101 | check(value, checked, path + PATH_SEPARATOR + field.name, errors) 102 | } 103 | } 104 | } 105 | 106 | private fun checkIsEnum(kClass: KClass<*>) = kClass.isSubclassOf(kotlin.Enum::class) 107 | 108 | private fun checkPackage(p: String?): Boolean { 109 | if (p == null) return false 110 | return !p.startsWith("java") && !p.startsWith("android") && !p.startsWith("kotlin") && (checkingPackages.isEmpty() || checkingPackages.contains(p)) 111 | } 112 | 113 | private fun KClass<*>.isKotlinClass(): Boolean { 114 | return this.java.declaredAnnotations.any { 115 | it.annotationClass.qualifiedName == "kotlin.Metadata" 116 | } 117 | } 118 | 119 | companion object { 120 | private const val PATH_SEPARATOR = "." 121 | 122 | private fun containsPointer(objects: Set, target: Any) = objects.any { it === target } 123 | } 124 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/ConversionStrategiesTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.ConversionStrategiesTest.CallbackState.* 4 | import clearnet.interfaces.ISerializer 5 | import clearnet.help.* 6 | import org.junit.Before 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | import org.junit.runners.Parameterized 10 | import clearnet.interfaces.RequestCallback 11 | import clearnet.model.RpcErrorResponse 12 | import clearnet.model.RpcInnerError 13 | import clearnet.model.RpcInnerErrorResponse 14 | import io.reactivex.schedulers.TestScheduler 15 | import java.util.* 16 | import java.util.concurrent.TimeUnit 17 | import kotlin.test.* 18 | 19 | @RunWith(Parameterized::class) 20 | class ConversionStrategiesTest( 21 | var callbackState: CallbackState 22 | ) : CoreBlocksTest() { 23 | 24 | private lateinit var testRequestExecutor: TestRequestExecutor 25 | private lateinit var converterExecutor: Core 26 | private lateinit var testRequests: TestRequests 27 | private lateinit var callback: TestCallback 28 | 29 | companion object { 30 | @JvmStatic 31 | @Parameterized.Parameters 32 | fun data(): Collection> { 33 | val d: ArrayList> = ArrayList() // I was confused with generic typing problems =( 34 | d.add(arrayOf(COMMON_OBJECT)) 35 | d.add(arrayOf(INNER_OBJECT)) 36 | d.add(arrayOf(COMMON_ERROR)) 37 | d.add(arrayOf(INNER_ERROR_ARRAY)) 38 | d.add(arrayOf(INNER_ERROR)) 39 | return d 40 | } 41 | } 42 | 43 | @Before 44 | fun setup(){ 45 | testRequestExecutor = TestRequestExecutor() 46 | converterExecutor = Core(ImmediateExecutor, testScheduler, blocks = *coreBlocks.getAll()) 47 | testRequests = ExecutorWrapper(converterExecutor, HeadersProviderStub, GsonTestSerializer()) 48 | .create(TestRequests::class.java, testRequestExecutor, Int.MAX_VALUE) 49 | callback = TestCallback() 50 | } 51 | 52 | 53 | @Before 54 | fun clean() { 55 | callback.testObject = null 56 | callback.exception = null 57 | testRequestExecutor.callbackState = callbackState 58 | } 59 | 60 | @Test 61 | fun test() { 62 | 63 | when (callbackState) { 64 | COMMON_OBJECT, COMMON_ERROR -> testRequests.commonResponse(callback) 65 | INNER_OBJECT, INNER_ERROR_ARRAY -> testRequests.innerResponse(callback) 66 | INNER_ERROR -> testRequests.innerErrorResponse(callback) 67 | } 68 | 69 | forwardScheduler() 70 | 71 | when (callbackState) { 72 | COMMON_OBJECT, INNER_OBJECT -> { 73 | assertNotNull(callback.testObject) 74 | assertEquals(1, callback.testObject!!.test) 75 | assertNull(callback.exception) 76 | } 77 | else -> { 78 | assertNull(callback.testObject) 79 | assertNotNull(callback.exception) 80 | 81 | val responseError = callback.exception as clearnet.error.ResponseErrorException 82 | assertNotNull(responseError.error) 83 | when (callbackState) { 84 | COMMON_ERROR -> { 85 | val rpcErrorResponse = responseError.error as RpcErrorResponse 86 | assertEquals(1, rpcErrorResponse.code) 87 | assertEquals("test", rpcErrorResponse.message) 88 | } 89 | INNER_ERROR_ARRAY -> { 90 | val errorArray = responseError.error as Array 91 | assertEquals(1, errorArray.size) 92 | val rpcInnerError = errorArray[0] 93 | assertEquals("test", rpcInnerError.error) 94 | assertEquals("test", rpcInnerError.message) 95 | } 96 | INNER_ERROR -> { 97 | val innerError = responseError.error as RpcInnerErrorResponse 98 | assertEquals("test", innerError.error) 99 | } 100 | else -> { 101 | fail("Unexpected branch") 102 | } 103 | } 104 | } 105 | } 106 | } 107 | 108 | 109 | class TestRequestExecutor : RequestExecutorStub() { 110 | private val converter: ISerializer by lazy { GsonTestSerializer() } 111 | lateinit var callbackState: CallbackState 112 | 113 | override fun executePost(body: String, headers: Map, queryParams: Map): Pair> { 114 | val response = when (callbackState) { 115 | COMMON_OBJECT -> "{\"result\":{\"test\":1}}" 116 | INNER_OBJECT -> "{\"result\":{\"success\":true,\"data\":{\"test\":1}}}" 117 | COMMON_ERROR -> "{\"error\":" + converter.serialize(RpcErrorResponse(1, "test", null)) + "}" 118 | INNER_ERROR_ARRAY -> "{\"result\":{\"success\":false,\"errors\":[" + converter.serialize(RpcInnerError("test", "test")) + "]}}" 119 | INNER_ERROR -> "{\"result\":" + converter.serialize(RpcInnerErrorResponse("test", "Test description")) + "}" 120 | else -> "ERROR" 121 | } 122 | return Pair(response, Collections.emptyMap()) 123 | } 124 | } 125 | 126 | class TestCallback : RequestCallback { 127 | var testObject: TestObject? = null 128 | var exception: clearnet.error.ClearNetworkException? = null 129 | 130 | override fun onSuccess(response: TestObject) { 131 | testObject = response 132 | } 133 | 134 | override fun onFailure(exception: clearnet.error.ClearNetworkException) { 135 | if (exception !is clearnet.error.ResponseErrorException) throw exception 136 | this.exception = exception 137 | } 138 | } 139 | 140 | enum class CallbackState { 141 | COMMON_OBJECT, INNER_OBJECT, COMMON_ERROR, INNER_ERROR, INNER_ERROR_ARRAY 142 | } 143 | } -------------------------------------------------------------------------------- /clearnetandroid/src/test/java/clearnet/android/CallbackHolderTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet.android 2 | 3 | import clearnet.ExecutorWrapper 4 | import clearnet.android.help.* 5 | import clearnet.interfaces.RequestCallback 6 | import io.reactivex.Observer 7 | import io.reactivex.disposables.Disposable 8 | 9 | import junit.framework.Assert.assertNull 10 | import org.junit.Before 11 | import org.junit.Test 12 | import ru.am.kutils.consume 13 | import java.lang.ref.WeakReference 14 | import java.util.* 15 | import java.util.concurrent.Executor 16 | import java.util.concurrent.atomic.AtomicInteger 17 | import kotlin.test.assertEquals 18 | import kotlin.test.assertFalse 19 | import kotlin.test.assertTrue 20 | 21 | class CallbackHolderTest { 22 | val testConverterExecutor = TestConverterExecutor() 23 | var callbackHolder = CallbackHolder(ImmediateExecutor) 24 | val testRequests = ExecutorWrapper(testConverterExecutor, HeadersProviderStub, GsonTestSerializer()) 25 | .create(TestRequests::class.java, RequestExecutorStub(), 1, callbackHolder) 26 | 27 | @Before 28 | fun prepare() { 29 | testConverterExecutor.lastParams = null 30 | callbackHolder.callbackList.clear() 31 | } 32 | 33 | @Test 34 | fun testCallbackManualClear() { 35 | val callback = TestCallback() 36 | 37 | testRequests.requestWithCallback(callback) 38 | assertEquals(1, callbackHolder.callbackList.size) 39 | val params = testConverterExecutor.lastParams!! 40 | 41 | callbackHolder.clear() 42 | assertTrue(callbackHolder.callbackList.isEmpty()) 43 | 44 | params.subject.onNext(1) 45 | assertFalse(callback.called) 46 | assertTrue(callbackHolder.callbackList.isEmpty()) 47 | } 48 | 49 | @Test 50 | fun testDisposablesManualClear() { 51 | val testObserver = TestObserver() 52 | 53 | testRequests.reactiveRequest().subscribe(testObserver) 54 | 55 | assertEquals(1, callbackHolder.disposables.size()) 56 | val params = testConverterExecutor.lastParams!! 57 | 58 | callbackHolder.clear() 59 | assertEquals(0, callbackHolder.disposables.size()) 60 | 61 | params.subject.onNext(1) 62 | assertFalse(testObserver.called) 63 | assertEquals(0, callbackHolder.disposables.size()) 64 | } 65 | 66 | @Test 67 | fun testWrapperCast(){ 68 | 69 | open class T1 : RequestCallback { 70 | override fun onSuccess(response: String) { 71 | } 72 | 73 | override fun onFailure(exception: clearnet.error.ClearNetworkException) { 74 | } 75 | } 76 | 77 | var rc: RequestCallback = callbackHolder.wrap(T1(), RequestCallback::class.java) 78 | 79 | rc.onSuccess("ignored") 80 | rc.onFailure(clearnet.error.ResponseErrorException("ignored")) 81 | 82 | class T2 : T1(), Runnable { 83 | override fun run() {} 84 | } 85 | 86 | rc = callbackHolder.wrap(T2(), RequestCallback::class.java) 87 | 88 | val r: Runnable = callbackHolder.wrap(T2(), Runnable::class.java) 89 | 90 | rc.onSuccess("ignored") 91 | rc.onFailure(clearnet.error.ResponseErrorException("ignored")) 92 | 93 | rc = callbackHolder.createEmpty(RequestCallback::class.java) 94 | 95 | rc.onSuccess("ignored") 96 | rc.onFailure(clearnet.error.ResponseErrorException("ignored")) 97 | } 98 | 99 | @Test 100 | fun testExecuteOnCorrectExecutor(){ 101 | val callFlag = AtomicInteger() 102 | val runFlag = AtomicInteger() 103 | 104 | val callbackHolder = clearnet.android.CallbackHolder(Executor { 105 | runFlag.incrementAndGet() 106 | it.run() 107 | }) 108 | 109 | var testClass: Runnable? = Runnable { callFlag.incrementAndGet() } 110 | 111 | val testWrapped = callbackHolder.wrap(testClass!!, Runnable::class.java) 112 | 113 | 114 | testWrapped.run() 115 | 116 | assertEquals(1, callFlag.get()) 117 | assertEquals(1, runFlag.get()) 118 | 119 | callFlag.set(0) 120 | runFlag.set(0) 121 | 122 | val reference = WeakReference(testClass) 123 | 124 | testClass = null 125 | callbackHolder.clear() 126 | 127 | val started = System.currentTimeMillis() 128 | 129 | while (reference.get() != null && System.currentTimeMillis() - started < 2000) { 130 | System.gc() 131 | } 132 | 133 | testWrapped.run() 134 | 135 | assertEquals(0, callFlag.get()) 136 | assertEquals(0, runFlag.get()) 137 | 138 | assertNull(reference.get()) 139 | } 140 | 141 | @Test 142 | fun testExecuteOnCorrectScheduler() { 143 | val testExecutor = TestExecutor() 144 | val specialCallbackHolder = CallbackHolder(testExecutor) 145 | val specialTestRequests = ExecutorWrapper(testConverterExecutor, HeadersProviderStub, GsonTestSerializer()) 146 | .create(TestRequests::class.java, RequestExecutorStub(), 1, specialCallbackHolder) 147 | 148 | val testObserver = TestObserver() 149 | 150 | specialTestRequests.reactiveRequest().subscribe(testObserver) 151 | val params = testConverterExecutor.lastParams!! 152 | 153 | params.subject.onNext(1) 154 | 155 | assertFalse(testObserver.called) 156 | 157 | testExecutor.run() 158 | 159 | assertTrue(testObserver.called) 160 | } 161 | 162 | internal class TestCallback : RequestCallback { 163 | var called = false 164 | var errorCalled = false 165 | 166 | override fun onSuccess(response: T) { 167 | called = true 168 | } 169 | 170 | override fun onFailure(exception: clearnet.error.ClearNetworkException) { 171 | errorCalled = true 172 | } 173 | } 174 | 175 | internal class TestObserver : Observer { 176 | var called = false 177 | var errorCalled = false 178 | 179 | override fun onNext(t: T) { 180 | called = true 181 | } 182 | 183 | override fun onError(e: Throwable?) { 184 | errorCalled = true 185 | } 186 | 187 | override fun onComplete() {} 188 | override fun onSubscribe(d: Disposable?) {} 189 | } 190 | 191 | private class TestExecutor : Executor { 192 | private val tasks: Queue = LinkedList() 193 | 194 | override fun execute(command: Runnable?) { 195 | tasks += command 196 | } 197 | 198 | fun run() = tasks.consume(Runnable::run) 199 | } 200 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/Core.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.error.UnknownExternalException 4 | import clearnet.interfaces.* 5 | import clearnet.interfaces.IInvocationBlock.QueueAlgorithm.IMMEDIATE 6 | import clearnet.interfaces.IInvocationBlock.QueueAlgorithm.TIME_THRESHOLD 7 | import clearnet.model.PostParams 8 | import io.reactivex.Observable 9 | import io.reactivex.Scheduler 10 | import io.reactivex.schedulers.Schedulers 11 | import io.reactivex.subjects.PublishSubject 12 | import io.reactivex.subjects.Subject 13 | import java.util.concurrent.CopyOnWriteArrayList 14 | import java.util.concurrent.Executor 15 | import java.util.concurrent.TimeUnit 16 | 17 | /** 18 | * Default realization of [IConverterExecutor] with the validation models feature 19 | */ 20 | class Core( 21 | private val ioExecutor: Executor, 22 | private val worker: Scheduler = Schedulers.single(), 23 | private val timeTracker: TaskTimeTracker? = null, 24 | vararg blocks: IInvocationBlock 25 | ) : IConverterExecutor, ICallbackStorage { 26 | private val flow: Map> 27 | private val taskStorage: MutableList = CopyOnWriteArrayList() 28 | private val ioScheduler = Schedulers.from(ioExecutor) 29 | 30 | // todo support nulls 31 | private val collector = PublishSubject.create>().toSerialized() 32 | 33 | init { 34 | flow = blocks.associate { block -> 35 | val subject = PublishSubject.create().toSerialized() 36 | 37 | when (block.queueAlgorithm) { 38 | IMMEDIATE -> subject.subscribeImmediate(block) 39 | TIME_THRESHOLD -> subject.subscribeWithTimeThreshold(block) 40 | } 41 | 42 | block.invocationBlockType to subject 43 | } 44 | } 45 | 46 | 47 | override fun executePost(postParams: PostParams) { 48 | Observable.just(postParams.bindable).subscribeOn(worker).flatMap { bindable -> 49 | if (bindable) Observable.fromIterable(taskStorage) 50 | else Observable.empty() 51 | }.filter { taskItem -> 52 | taskItem.respond(postParams.requestTypeIdentifier, postParams.cacheKey) 53 | }.sorted(CoreTask.ResultsCountComparator).switchIfEmpty { 54 | val task = CoreTask(postParams) 55 | taskStorage += task 56 | placeToQueue(task, InvocationBlockType.INITIAL) 57 | task.observe().subscribe { collector.onNext(task to it) } 58 | it.onNext(task) 59 | }.take(1).flatMap { 60 | it.observe() 61 | }.flatMap { 62 | if (it is CoreTask.ErrorResult) Observable.error(it.error) 63 | else Observable.just((it as CoreTask.SuccessResult).result) 64 | }.observeOn(ioScheduler).subscribe(postParams.subject) 65 | } 66 | 67 | 68 | override fun subscribe(method: String, callback: RequestCallback<*>, once: Boolean): Subscription { 69 | val disposable = collector.filter { (task, _) -> task.respond(method, null) } 70 | .compose { if (once) it.take(1) else it } 71 | .map { it.second }.subscribe { 72 | if (it is CoreTask.SuccessResult) { 73 | (callback as RequestCallback).onSuccess(it.result) 74 | } else if (it is CoreTask.ErrorResult) { 75 | callback.onFailure(it.error) 76 | } 77 | } 78 | 79 | return object : Subscription { 80 | override fun unsubscribe() = disposable.dispose() 81 | } 82 | } 83 | 84 | override fun observe(method: String): Observable { 85 | return collector.filter { (task, _) -> task.respond(method, null) } 86 | .map { it.second } 87 | .filter { it is CoreTask.SuccessResult } 88 | .map { (it as CoreTask.SuccessResult).result as T } 89 | } 90 | 91 | 92 | private fun placeToQueue(task: CoreTask, index: InvocationBlockType) { 93 | flow[index]!!.onNext(task) 94 | } 95 | 96 | private fun placeToQueues(from: InvocationBlockType?, task: CoreTask, indexes: Array) { 97 | task.move(from, indexes) 98 | indexes.forEach { placeToQueue(task, it) } 99 | if (task.isFinished()) { 100 | taskStorage.remove(task) 101 | timeTracker?.onTaskFinished( 102 | task.postParams.invocationStrategy, 103 | task.postParams.requestTypeIdentifier, 104 | System.currentTimeMillis() - task.startTime 105 | ) 106 | } 107 | } 108 | 109 | 110 | private fun handleTaskResult(block: IInvocationBlock, task: CoreTask, result: CoreTask.Result) { 111 | placeToQueues(block.invocationBlockType, task, result.nextIndexes) 112 | } 113 | 114 | private fun Observable.subscribeImmediate(block: IInvocationBlock) { 115 | this.observeOn(worker).subscribe { task -> 116 | val promise = task.promise().apply { 117 | observe().observeOn(Schedulers.trampoline()).subscribe { result -> 118 | handleTaskResult(block, task, result) 119 | } 120 | } 121 | 122 | // todo need test this 123 | ioExecutor.execute { 124 | try{ 125 | block.onEntity(promise) 126 | }catch (e: Throwable){ 127 | promise.setError(UnknownExternalException(e.message), block.invocationBlockType) 128 | } 129 | } 130 | } 131 | } 132 | 133 | private fun Observable.subscribeWithTimeThreshold(block: IInvocationBlock) { 134 | this.buffer(block.queueTimeThreshold, TimeUnit.MILLISECONDS, worker).filter { 135 | !it.isEmpty() 136 | }.subscribe { taskList -> 137 | val promises = taskList.map { task -> 138 | task.promise().apply { 139 | observe().observeOn(Schedulers.trampoline()).subscribe { result -> 140 | handleTaskResult(block, task, result) 141 | } 142 | } 143 | } 144 | 145 | // todo need test this 146 | ioExecutor.execute { 147 | try { 148 | block.onQueueConsumed(promises) 149 | }catch (e: Throwable){ 150 | promises.forEach { 151 | it.setError(UnknownExternalException(e.message), block.invocationBlockType) 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/interfaces.kt: -------------------------------------------------------------------------------- 1 | package clearnet.interfaces 2 | 3 | import clearnet.CoreTask 4 | import clearnet.InvocationBlockType 5 | import clearnet.error.ClearNetworkException 6 | import clearnet.error.ConversionException 7 | import clearnet.error.HTTPCodeError 8 | import clearnet.model.MergedInvocationStrategy 9 | import clearnet.model.PostParams 10 | import io.reactivex.Observable 11 | import io.reactivex.Scheduler 12 | import io.reactivex.disposables.Disposable 13 | import io.reactivex.internal.schedulers.ImmediateThinScheduler 14 | import io.reactivex.schedulers.Schedulers 15 | import org.json.JSONException 16 | import org.json.JSONObject 17 | import java.io.IOException 18 | import java.lang.reflect.Type 19 | import java.util.* 20 | 21 | interface ConversionStrategy { 22 | @Throws(JSONException::class, ConversionStrategyError::class) 23 | fun checkErrorOrResult(response: JSONObject): String? 24 | 25 | fun init(parameter: String){ 26 | // nop 27 | } 28 | 29 | class ConversionStrategyError(val serializedError: String?, val errorType: Type) : Exception() 30 | 31 | object SmartConverter { 32 | @Throws(ClearNetworkException::class) 33 | fun convert(converter: ISerializer, body: String, type: Type, strategy: ConversionStrategy): Any? { 34 | return converter.deserialize(getStringResultOrThrow(converter, body, strategy), type) 35 | } 36 | 37 | @Throws(ClearNetworkException::class) 38 | fun getStringResultOrThrow(converter: ISerializer, body: String, strategy: ConversionStrategy): String? { 39 | try { 40 | return getStringResultOrThrow(converter, JSONObject(body), strategy) 41 | } catch (e: JSONException) { 42 | throw ConversionException(e) 43 | } 44 | } 45 | 46 | @Throws(ClearNetworkException::class) 47 | fun getStringResultOrThrow(converter: ISerializer, body: JSONObject, strategy: ConversionStrategy): String? { 48 | try { 49 | return strategy.checkErrorOrResult(body) 50 | } catch (e: JSONException) { 51 | throw ConversionException(e) 52 | } catch (e: ConversionStrategyError) { 53 | throw clearnet.error.ResponseErrorException(converter.deserialize(e.serializedError, e.errorType)) 54 | } 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * Validates fields of model instances which has been deserialized from the server response. 61 | * The validator must check required fields and can contains some additional checking rules. 62 | */ 63 | interface IBodyValidator { 64 | @Throws(clearnet.error.ValidationException::class) 65 | fun validate(body: Any?) 66 | } 67 | 68 | interface ICacheProvider { 69 | fun store(key: String, value: String, expiresAfter: Long) 70 | 71 | fun obtain(key: String): String? 72 | } 73 | 74 | /** 75 | * The upper level abstraction of [IRequestExecutor]. 76 | * It should serialize the object, send the request and deserialize the response to the model. 77 | */ 78 | interface IConverterExecutor { 79 | fun executePost(postParams: PostParams) 80 | } 81 | 82 | /** 83 | * Request executor which should just push data to server and return response as String 84 | */ 85 | interface IRequestExecutor { 86 | @Throws(IOException::class, HTTPCodeError::class) 87 | fun executeGet(headers: Map, queryParams: Map = emptyMap()): Pair> 88 | 89 | @Throws(IOException::class, HTTPCodeError::class) 90 | fun executePost(body: String, headers: Map, queryParams: Map = emptyMap()): Pair> 91 | } 92 | 93 | /** 94 | * Serializes and deserializes models to/from String 95 | */ 96 | interface ISerializer { 97 | @Throws(ConversionException::class) 98 | fun serialize(obj: Any?): String 99 | 100 | @Throws(ConversionException::class) 101 | fun deserialize(body: String?, objectType: Type): Any? 102 | } 103 | 104 | interface ISmartConverter { 105 | @Throws(ClearNetworkException::class) 106 | fun convert(body: String, type: Type, strategy: ConversionStrategy): Any? 107 | } 108 | 109 | /** 110 | * Request executor non typed callback for inner use 111 | */ 112 | @Deprecated("Use reactive streams") 113 | interface RequestCallback { 114 | 115 | /** 116 | * @param response – the deserialized model 117 | */ 118 | fun onSuccess(response: T) 119 | 120 | /** 121 | * @param exception – the exception, which can be caught during request, conversation or validation processes 122 | */ 123 | fun onFailure(exception: ClearNetworkException) 124 | } 125 | 126 | interface ICallbackHolder { 127 | val scheduler: Scheduler 128 | get() = ImmediateThinScheduler.INSTANCE 129 | 130 | fun init() 131 | fun hold(disposable: Disposable) 132 | fun clear() 133 | 134 | @Deprecated("") 135 | fun createEmpty(type: Class): I 136 | 137 | @Deprecated("") 138 | fun wrap(source: I, interfaceType: Class): I 139 | } 140 | 141 | interface HeaderProvider { 142 | fun obtainHeadersList(): Map 143 | } 144 | 145 | interface HeaderListener { 146 | fun onNewHeader(method: String, name: String, value: String) 147 | } 148 | 149 | interface HeaderObserver { 150 | fun register(method: String, listener: HeaderListener, vararg header: String): Subscription 151 | } 152 | 153 | interface ICallbackStorage { 154 | fun observe(method: String): Observable 155 | 156 | @Deprecated("") 157 | fun subscribe(method: String, callback: RequestCallback<*>, once: Boolean = false): Subscription 158 | } 159 | 160 | @Deprecated("") 161 | interface Subscription { 162 | fun unsubscribe() 163 | } 164 | 165 | interface IInvocationBlock { 166 | val invocationBlockType: InvocationBlockType 167 | val queueAlgorithm: QueueAlgorithm 168 | get() = QueueAlgorithm.IMMEDIATE 169 | val queueTimeThreshold: Long 170 | get() = 100L 171 | 172 | fun onEntity(promise: CoreTask.Promise) { 173 | promise.next(invocationBlockType) 174 | } 175 | fun onQueueConsumed(promises: List) {} 176 | 177 | enum class QueueAlgorithm { 178 | IMMEDIATE, TIME_THRESHOLD 179 | } 180 | } 181 | 182 | interface TaskTimeTracker { 183 | fun onTaskFinished(invocationStrategy: MergedInvocationStrategy, method: String, time: Long) 184 | } 185 | 186 | interface IInvocationStrategy { 187 | val algorithm: Map 188 | val metaData: Map 189 | 190 | class Decision(private val onResult: Array, private val onError: Array = emptyArray()) { 191 | constructor(onResult: InvocationBlockType) : this(arrayOf(onResult)) 192 | constructor(onResult: InvocationBlockType, onError: InvocationBlockType) : this(arrayOf(onResult), arrayOf(onError)) 193 | constructor(onResult: Array, onError: InvocationBlockType) : this(onResult, arrayOf(onError)) 194 | 195 | operator fun get(hasResult: Boolean) = if (hasResult) onResult else onError 196 | } 197 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/blocks/GetFromNetBlock.kt: -------------------------------------------------------------------------------- 1 | package clearnet.blocks 2 | 3 | import clearnet.* 4 | import clearnet.error.ClearNetworkException 5 | import clearnet.error.ConversionException 6 | import clearnet.error.NetworkException 7 | import clearnet.interfaces.IBodyValidator 8 | import clearnet.interfaces.IInvocationBlock 9 | import clearnet.interfaces.ISerializer 10 | import clearnet.interfaces.ConversionStrategy.SmartConverter 11 | import clearnet.interfaces.HeaderObserver 12 | import org.json.JSONArray 13 | import org.json.JSONException 14 | import org.json.JSONObject 15 | import java.io.IOException 16 | import java.util.NoSuchElementException 17 | 18 | class GetFromNetBlock( 19 | private val validator: IBodyValidator, 20 | private val converter: ISerializer 21 | ) : IInvocationBlock { 22 | override val invocationBlockType = InvocationBlockType.GET_FROM_NET 23 | override val queueAlgorithm = IInvocationBlock.QueueAlgorithm.TIME_THRESHOLD 24 | private val headersObserver = SimpleHeadersObserver() 25 | 26 | fun getHeadersObserver(): HeaderObserver = headersObserver 27 | 28 | override fun onQueueConsumed(promises: List) { 29 | when { 30 | promises.isEmpty() -> return 31 | promises.size == 1 -> obtainFromNet(promises[0]) 32 | checkExecutors(promises) -> groupByBatchSize(promises) 33 | else -> { 34 | val runningTasks = ArrayList() 35 | promises.forEach { 36 | if (it.taskRef.postParams.requestExecutor === promises[0].taskRef.postParams.requestExecutor) runningTasks.add(it) 37 | else it.setNextIndex(InvocationBlockType.GET_FROM_NET) 38 | } 39 | 40 | if (runningTasks.size == 1) { 41 | obtainFromNet(runningTasks[0]) 42 | } else { 43 | groupByBatchSize(runningTasks) 44 | } 45 | } 46 | } 47 | } 48 | 49 | private fun checkExecutors(promises: List): Boolean { 50 | return (1 until promises.size).none { promises[it - 1].taskRef.postParams.requestExecutor !== promises[it].taskRef.postParams.requestExecutor } 51 | } 52 | 53 | private fun obtainFromNet(promise: CoreTask.Promise) = with(promise.taskRef.postParams) { 54 | try { 55 | val responseString: String 56 | try { 57 | val result = if (httpRequestType == "POST") { 58 | requestExecutor.executePost(promise.taskRef.requestKey, headers, requestParams) 59 | } else { 60 | requestExecutor.executeGet(requestParams, headers) 61 | } 62 | headersObserver.propagateHeaders(requestTypeIdentifier, result.second) 63 | responseString = result.first 64 | } catch (e: IOException) { 65 | throw NetworkException(e) 66 | } 67 | 68 | val stringResult = SmartConverter.getStringResultOrThrow(converter, responseString, conversionStrategy) 69 | val result = converter.deserialize(stringResult, resultType) 70 | validator.validate(result) 71 | 72 | promise.setResult(result, stringResult, invocationBlockType) 73 | } catch (e: ClearNetworkException) { 74 | promise.setError(e, invocationBlockType) 75 | } 76 | } 77 | 78 | private fun groupByBatchSize(promises: List) { 79 | val executingList = mutableListOf() 80 | var max = promises[0].taskRef.postParams.maxBatchSize 81 | 82 | promises.forEach { 83 | if (executingList.size < max && it.taskRef.postParams.maxBatchSize > executingList.size) { 84 | executingList += it 85 | if (it.taskRef.postParams.maxBatchSize < max) max = it.taskRef.postParams.maxBatchSize 86 | } else { 87 | it.setNextIndex(InvocationBlockType.GET_FROM_NET) 88 | } 89 | } 90 | 91 | trimAndExecuteOnSingleExecutor(executingList) 92 | } 93 | 94 | private fun trimAndExecuteOnSingleExecutor(promises: List) { 95 | val maxBatchSize = promises[0].taskRef.postParams.maxBatchSize 96 | if (promises.size > maxBatchSize) { 97 | val runningList = promises.subList(0, maxBatchSize) 98 | val overflowList = promises.subList(maxBatchSize, promises.size) 99 | overflowList.forEach { it.setNextIndex(InvocationBlockType.GET_FROM_NET) } 100 | executeSequenceOnSingleExecutor(runningList) 101 | } else { 102 | executeSequenceOnSingleExecutor(promises) 103 | } 104 | } 105 | 106 | private fun executeSequenceOnSingleExecutor(promises: List) { 107 | if (promises.size == 1) { // in case of maxBatchSize == 1 108 | obtainFromNet(promises[0]) 109 | return 110 | } 111 | try { 112 | val result: String 113 | val tasksToWork = promises.toMutableList() 114 | try { 115 | val conflictedHeadersTasks = mutableListOf() 116 | val combinedHeaders = combineHeaders(promises, conflictedHeadersTasks) 117 | conflictedHeadersTasks.forEach { it -> 118 | it.setNextIndex(InvocationBlockType.GET_FROM_NET) 119 | } 120 | tasksToWork.removeAll(conflictedHeadersTasks) 121 | 122 | val responseWithHeaders = tasksToWork[0].taskRef.postParams.requestExecutor.executePost( 123 | createBatchString(tasksToWork), 124 | combinedHeaders, 125 | mapOf("applicationMethod" to combineRpcMethods(promises)) 126 | ) 127 | result = responseWithHeaders.first 128 | tasksToWork.forEach { headersObserver.propagateHeaders(it.taskRef.getRequestIdentifier(), responseWithHeaders.second) } 129 | } catch (e: IOException) { 130 | throw NetworkException(e) 131 | } 132 | getRequestResponseList(tasksToWork, result).forEach { 133 | try { 134 | val stringResult = SmartConverter.getStringResultOrThrow(converter, it.second, it.first.taskRef.postParams.conversionStrategy) 135 | val convertedResult = converter.deserialize(stringResult, it.first.taskRef.postParams.resultType) 136 | 137 | validator.validate(convertedResult) 138 | it.first.setResult(convertedResult, stringResult, invocationBlockType) 139 | } catch (e: ClearNetworkException) { 140 | it.first.setError(e, invocationBlockType) 141 | } 142 | } 143 | } catch (e: ClearNetworkException) { 144 | promises.forEach { task -> 145 | task.setError(e, invocationBlockType) 146 | } 147 | } 148 | } 149 | 150 | private fun combineHeaders( 151 | promises: List, 152 | conflictedHeadersTasks: MutableList 153 | ): Map = mutableMapOf().apply { 154 | promises.forEach { promise -> 155 | promise.taskRef.postParams.headers.entries.forEach { 156 | if (it.key in keys && this[it.key] != it.value) { 157 | conflictedHeadersTasks.add(promise) 158 | } else { 159 | this[it.key] = it.value 160 | } 161 | } 162 | } 163 | } 164 | 165 | @Deprecated("") 166 | // todo move this logic to Post params 167 | // It's difficult because it uses strange protocol with comma instead of HTTP params array 168 | private fun combineRpcMethods(promises: List) = promises.joinToString(",") { it.taskRef.getRequestIdentifier() } 169 | 170 | @Throws(ConversionException::class) 171 | private fun createBatchString(promises: List): String { 172 | return converter.serialize(promises.map { it.taskRef.postParams.requestBody }) 173 | } 174 | 175 | @Throws(ConversionException::class) 176 | private fun getRequestResponseList(promises: List, source: String): List> { 177 | try { 178 | val array = JSONArray(source) 179 | return (0 until array.length()) 180 | .map { array.getJSONObject(it) } 181 | .map { getTaskPromiseById(promises, it.getLong("id")) to it } 182 | } catch (e: JSONException) { 183 | throw ConversionException("Incorrect batch response: $source", e) 184 | } 185 | 186 | } 187 | 188 | @Throws(ConversionException::class) 189 | private fun getTaskPromiseById(promises: List, id: Long): CoreTask.Promise { 190 | try { 191 | return promises.first { 192 | // todo remove manual casting 193 | (it.taskRef.postParams.requestBody as RPCRequest).id == id 194 | } 195 | } catch (e: NoSuchElementException) { 196 | throw ConversionException("Responses ids not comparable with requests ids", e) 197 | } 198 | } 199 | } -------------------------------------------------------------------------------- /clearnet/src/test/java/clearnet/BatchRequestTest.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.error.ClearNetworkException 4 | import clearnet.help.* 5 | import clearnet.interfaces.* 6 | import com.google.gson.Gson 7 | import io.reactivex.schedulers.TestScheduler 8 | import org.json.JSONArray 9 | import org.junit.Before 10 | import org.junit.Test 11 | import java.util.concurrent.TimeUnit 12 | import java.util.concurrent.atomic.AtomicInteger 13 | import java.util.concurrent.atomic.AtomicReference 14 | import kotlin.test.assertEquals 15 | 16 | class BatchRequestTest : CoreBlocksTest() { 17 | 18 | companion object { 19 | private const val MAX_BATCH_SIZE = 5 20 | } 21 | 22 | private lateinit var core: Core 23 | private lateinit var invocationBlocks: TestCoreBlocks 24 | 25 | @Before 26 | fun setup() { 27 | invocationBlocks = TestCoreBlocks( 28 | cacheProvider = testCacheProvider 29 | ) 30 | 31 | timeT = invocationBlocks.getFromNetTimeThreshold 32 | 33 | core = Core( 34 | ioExecutor = TrampolineExecutor(), 35 | worker = testScheduler, 36 | blocks = *invocationBlocks.getAll() 37 | ) 38 | } 39 | 40 | @Test 41 | fun creatingBatch() { 42 | val firstResult = AtomicReference() 43 | val secondResult = AtomicReference() 44 | 45 | val testRequests = provideTestRequests(BatchTestRequestExecutor()) 46 | 47 | testRequests.firstOfBatch(object : RequestCallback { 48 | override fun onSuccess(response: String) { 49 | firstResult.set(response) 50 | } 51 | 52 | override fun onFailure(exception: ClearNetworkException) { 53 | throw exception 54 | } 55 | }) 56 | testRequests.secondOfBatch(object : RequestCallback { 57 | override fun onSuccess(response: String) { 58 | secondResult.set(response) 59 | } 60 | 61 | override fun onFailure(exception: ClearNetworkException) { 62 | throw exception 63 | } 64 | }) 65 | 66 | forwardScheduler() 67 | 68 | assertEquals("test0", firstResult.get()) 69 | assertEquals("test1", secondResult.get()) 70 | } 71 | 72 | @Test 73 | fun differentExecutors() { 74 | val firstRequestExecutor = TestSingleRequestsExecutor("test1") 75 | val secondRequestExecutor = TestSingleRequestsExecutor("test2") 76 | 77 | val firstRequests = provideTestRequests(firstRequestExecutor) 78 | val secondRequests = provideTestRequests(secondRequestExecutor) 79 | 80 | val firstResult = AtomicReference() 81 | val secondResult = AtomicReference() 82 | 83 | firstRequests.firstOfBatch(object : RequestCallback { 84 | override fun onSuccess(response: String) { 85 | firstResult.set(response) 86 | } 87 | 88 | override fun onFailure(exception: ClearNetworkException) { 89 | throw exception 90 | } 91 | }) 92 | 93 | secondRequests.secondOfBatch(object : RequestCallback { 94 | override fun onSuccess(response: String) { 95 | secondResult.set(response) 96 | } 97 | 98 | override fun onFailure(exception: ClearNetworkException) { 99 | throw exception 100 | } 101 | }) 102 | 103 | forwardScheduler() 104 | forwardScheduler() 105 | 106 | assertEquals("test1", firstResult.get()) 107 | assertEquals("test2", secondResult.get()) 108 | 109 | assertEquals(1, firstRequestExecutor.called) 110 | assertEquals(1, secondRequestExecutor.called) 111 | } 112 | 113 | 114 | @Test 115 | fun headers() { 116 | val counter = AtomicInteger() 117 | val header = AtomicReference() 118 | invocationBlocks.getHeadersObserver().register("test.secondOfBatch", object : HeaderListener { 119 | override fun onNewHeader(method: String, name: String, value: String) { 120 | counter.incrementAndGet() 121 | header.set(value) 122 | } 123 | }, "testHeader") 124 | 125 | val testRequests = provideTestRequests(BatchTestRequestExecutor()) 126 | testRequests.firstOfBatch(RequestCallbackStub()) 127 | testRequests.secondOfBatch(RequestCallbackStub()) 128 | 129 | forwardScheduler() 130 | 131 | assertEquals(1, counter.get()) 132 | assertEquals("test", header.get()) 133 | } 134 | 135 | @Test 136 | fun testConflictedHeaders() { 137 | val executor = TestCheckBatchSizeRequestExecutor() 138 | val testRequests = provideTestRequests( 139 | requestExecutor = executor, 140 | headerProvider = object : HeaderProvider { 141 | var callsCount = 0 142 | override fun obtainHeadersList(): Map { 143 | callsCount++ 144 | return when (callsCount) { 145 | 1 -> mapOf("Header" to "header-1") 146 | 2 -> mapOf("Header" to "header-2") 147 | else -> emptyMap() 148 | } 149 | } 150 | } 151 | ) 152 | testRequests.firstOfBatch(RequestCallbackStub()) 153 | testRequests.secondOfBatch(RequestCallbackStub()) 154 | 155 | forwardScheduler() 156 | forwardScheduler() 157 | 158 | assertEquals(2, executor.counter.size) 159 | } 160 | 161 | @Test 162 | fun someOfBatchIsFromCache() { 163 | val firstResult = AtomicReference() 164 | val secondResult = AtomicReference() 165 | 166 | val testRequests = provideTestRequests(TestSingleRequestsExecutor("test1")) 167 | testRequests.firstOfBatch(object : RequestCallback { 168 | override fun onSuccess(response: String) { 169 | firstResult.set(response) 170 | } 171 | 172 | override fun onFailure(exception: ClearNetworkException) { 173 | throw exception 174 | } 175 | }) 176 | testRequests.forBatchWithPriorityCache(object : RequestCallback { 177 | override fun onSuccess(response: String) { 178 | secondResult.set(response) 179 | } 180 | 181 | override fun onFailure(exception: ClearNetworkException) { 182 | throw exception 183 | } 184 | }) 185 | 186 | forwardScheduler() 187 | 188 | assertEquals("test1", firstResult.get()) 189 | assertEquals("cache", secondResult.get()) 190 | } 191 | 192 | @Test 193 | fun moreThenMaxBatchSizeRequests() { 194 | val executor = TestCheckBatchSizeRequestExecutor() 195 | val testRequests = provideTestRequests(executor) 196 | 197 | testRequests.firstOfBatch(RequestCallbackStub()) 198 | 199 | for (i in 0 until MAX_BATCH_SIZE) { 200 | testRequests.firstOfBatch(RequestCallbackStub()) 201 | } 202 | 203 | // testScheduler.triggerActions() 204 | testScheduler.advanceTimeBy(201, TimeUnit.MILLISECONDS) 205 | 206 | assertEquals(2, executor.counter.size) 207 | assertEquals(MAX_BATCH_SIZE, executor.counter[0]) 208 | assertEquals(1, executor.counter[1]) 209 | } 210 | 211 | @Test 212 | fun noBatchTest() { // also tests cases with different batch sizes on each task in same request executor 213 | val executor = TestCheckBatchSizeRequestExecutor() 214 | val testRequests = provideTestRequests(executor) 215 | 216 | testRequests.firstOfBatch(RequestCallbackStub()) 217 | testRequests.batchNoBatch(RequestCallbackStub()) 218 | testRequests.secondOfBatch(RequestCallbackStub()) 219 | 220 | testScheduler.triggerActions() 221 | testScheduler.advanceTimeBy(201, TimeUnit.MILLISECONDS) 222 | 223 | assertEquals(2, executor.counter.size) 224 | assertEquals(2, executor.counter[0]) 225 | assertEquals(1, executor.counter[1]) 226 | } 227 | 228 | private fun provideTestRequests(requestExecutor: IRequestExecutor, headerProvider: HeaderProvider = HeadersProviderStub): TestRequests { 229 | return ExecutorWrapper(core, headerProvider, GsonTestSerializer()) 230 | .create(TestRequests::class.java, requestExecutor, MAX_BATCH_SIZE) 231 | } 232 | 233 | 234 | private object testCacheProvider : ICacheProvider { 235 | override fun store(key: String, value: String, expiresAfter: Long) {} 236 | override fun obtain(key: String) = "cache" 237 | } 238 | 239 | private class TestCheckBatchSizeRequestExecutor : BatchTestRequestExecutor() { 240 | var counter = mutableListOf() 241 | override fun executePost(body: String, headers: Map, queryParams: Map): Pair> { 242 | if(body.startsWith("{")) { 243 | counter.add(1) 244 | } else { 245 | val array = JSONArray(body) 246 | counter.add(array.length()) 247 | } 248 | return super.executePost(body, headers, queryParams) 249 | } 250 | } 251 | 252 | private class TestSingleRequestsExecutor(private val result: String) : RequestExecutorStub() { 253 | var called = 0 254 | 255 | override fun executePost(body: String, headers: Map, queryParams: Map): Pair> { 256 | called++ 257 | return Pair(Gson().toJson(mapOf("result" to result)), emptyMap()) 258 | } 259 | } 260 | } -------------------------------------------------------------------------------- /clearnet/src/main/java/clearnet/ExecutorWrapper.kt: -------------------------------------------------------------------------------- 1 | package clearnet 2 | 3 | import clearnet.annotations.* 4 | import clearnet.annotations.Parameter 5 | import clearnet.annotations.NoBatch 6 | import clearnet.annotations.RPCMethodScope 7 | import clearnet.conversion.DefaultConversionStrategy 8 | import clearnet.error.ClearNetworkException 9 | import clearnet.interfaces.* 10 | import clearnet.interfaces.ConversionStrategy 11 | import clearnet.model.MergedInvocationStrategy 12 | import clearnet.model.RpcPostParams 13 | import io.reactivex.Observable 14 | import io.reactivex.disposables.Disposable 15 | import io.reactivex.subjects.Subject 16 | import java.lang.reflect.* 17 | 18 | 19 | class ExecutorWrapper(private val converterExecutor: IConverterExecutor, 20 | private val headerProvider: HeaderProvider, 21 | private val serializer: ISerializer) { 22 | private val defaultCallbackHolder: ICallbackHolder 23 | 24 | fun create(tClass: Class, requestExecutor: IRequestExecutor, maxBatchSize: Int, callbackHolder: ICallbackHolder = defaultCallbackHolder): T { 25 | return Proxy.newProxyInstance(tClass.classLoader, arrayOf>(tClass), ApiInvocationHandler( 26 | requestExecutor, 27 | callbackHolder, 28 | tClass.getAnnotation(RPCMethodScope::class.java)?.value, 29 | maxBatchSize 30 | )) as T 31 | } 32 | 33 | 34 | private inner class ApiInvocationHandler( 35 | private val requestExecutor: IRequestExecutor, 36 | private val callbackHolder: ICallbackHolder, 37 | private val rpcMethodScope: String?, 38 | private val maxBatchSize: Int 39 | ) : InvocationHandler { 40 | override fun invoke(proxy: Any?, method: Method, args: Array?): Any? { 41 | val requestBody = RPCRequest(retrieveRemoteMethod(method)) 42 | 43 | addDefaultParameterIfExists(method, requestBody) 44 | val (invocationStrategy: MergedInvocationStrategy, expiresAfter: Long) = retrieveInvocationStrategyAndExpiration(method) 45 | val (requestCallback, type) = fillRequestBodyAndFindListeners(args, method, requestBody) 46 | 47 | val postParams = RpcPostParams( 48 | generateAmruRequestParams(requestBody), 49 | requestBody, 50 | type, 51 | requestExecutor, 52 | invocationStrategy, 53 | expiresAfter, 54 | retrieveConversionStrategy(method), 55 | headerProvider.obtainHeadersList(), 56 | method.getAnnotation(NotBindable::class.java) == null, 57 | getMaxBatchSize(method), 58 | serializer 59 | ) 60 | 61 | 62 | wrapCallback(requestCallback, postParams.subject, callbackHolder) 63 | 64 | // todo check callback and observable 65 | converterExecutor.executePost(postParams) 66 | 67 | return if(isReturnsObservable(method)) postParams.subject.observeOn(callbackHolder.scheduler).doOnSubscribe(callbackHolder::hold) 68 | else null 69 | } 70 | 71 | private fun wrapCallback(requestCallback: RequestCallback?, subject: Subject, callbackHolder: ICallbackHolder): RequestCallback? { 72 | return if (requestCallback == null) { 73 | null 74 | } else { 75 | val result = callbackHolder.wrap(requestCallback, RequestCallback::class.java) 76 | subject.subscribe({ 77 | result.onSuccess(it) 78 | }, { 79 | result.onFailure(it as ClearNetworkException) 80 | }) 81 | result 82 | } 83 | } 84 | 85 | private fun retrieveRemoteMethod(method: Method): String { 86 | val methodAnnotation = method.getAnnotation(RPCMethod::class.java) 87 | if (methodAnnotation != null) { 88 | return methodAnnotation.value 89 | } 90 | 91 | val scope = method.getAnnotation(RPCMethodScope::class.java)?.value ?: rpcMethodScope 92 | 93 | if(scope != null) { 94 | return if(scope.isEmpty()) method.name else "$scope.${method.name}" 95 | } else { 96 | throw IllegalArgumentException("Method " + method.name + " must be annotated with @" + RPCMethod::class.java.name + " or @" + RPCMethodScope::class.java.name + " annotation") 97 | } 98 | } 99 | 100 | private fun isReturnsObservable(method: Method) = method.returnType == Observable::class.java 101 | 102 | private fun getGenericParameterType(method: Method, parameterIndex: Int): Type { 103 | return (method.genericParameterTypes[parameterIndex] as ParameterizedType).actualTypeArguments[0] 104 | } 105 | 106 | private fun getGenericReturnType(method: Method): Type { 107 | return (method.genericReturnType as ParameterizedType).actualTypeArguments[0] 108 | } 109 | 110 | private fun fillRequestBodyAndFindListeners(args: Array?, method: Method, requestBody: RPCRequest): Pair?, Type> { 111 | var requestCallback: RequestCallback? = null 112 | 113 | var callbackIndex = -1 114 | if (args != null) { 115 | val annotations = method.parameterAnnotations 116 | 117 | for (i in args.indices) { 118 | var parameterSet = false 119 | var bodySet = false 120 | for (annotation in annotations[i]) { 121 | if (annotation is Parameter) { 122 | requestBody.addParameter(annotation.value, args[i]) 123 | parameterSet = true 124 | } else if (annotation is Body) { 125 | requestBody.setParamsBody(args[i]) 126 | bodySet = true 127 | } 128 | } 129 | 130 | if (parameterSet || bodySet) continue 131 | 132 | if (args[i] is RequestCallback<*>) { 133 | requestCallback = args[i] as RequestCallback 134 | callbackIndex = i 135 | } else { 136 | throw IllegalArgumentException("All parameters in method " + method.name + " must have the Parameter annotation") 137 | } 138 | } 139 | } 140 | val type = when { 141 | callbackIndex >= 0 -> getGenericParameterType(method, callbackIndex) 142 | isReturnsObservable(method) -> getGenericReturnType(method) 143 | else -> method.getAnnotation(ResultType::class.java)?.value?.java as Type? ?: Any::class.java 144 | } 145 | return Pair(requestCallback, type) 146 | } 147 | 148 | private fun addDefaultParameterIfExists(method: Method, requestBody: RPCRequest) { 149 | val defaultParameters = retrieveDefaultParameters(method) 150 | defaultParameters.forEach { 151 | requestBody.addParameter(it.key, it.value) 152 | } 153 | } 154 | 155 | private fun retrieveDefaultParameters(method: Method): Array { 156 | val defaultParameter = method.getAnnotation(DefaultParameter::class.java) 157 | val defaultParameters = method.getAnnotation(DefaultParameters::class.java) 158 | 159 | if (defaultParameter == null || defaultParameters == null) { 160 | return if (defaultParameter == null) defaultParameters?.value ?: arrayOf() else arrayOf(defaultParameter) 161 | } else { 162 | throw IllegalArgumentException("Method ${method.name} must have either DefaultParameter annotation or DefaultParameters but not both") 163 | } 164 | } 165 | 166 | private fun retrieveConversionStrategy(method: Method): ConversionStrategy { 167 | val conversionStrategyAnnotation = method.getAnnotation(clearnet.annotations.ConversionStrategy::class.java) 168 | return if (conversionStrategyAnnotation == null) { 169 | DefaultConversionStrategy() 170 | } else { 171 | val result: ConversionStrategy = conversionStrategyAnnotation.value.java.newInstance() as ConversionStrategy 172 | result.init(conversionStrategyAnnotation.parameter) 173 | result 174 | } 175 | } 176 | 177 | private fun retrieveInvocationStrategyAndExpiration(method: Method): Pair { 178 | var invocationStrategies: Array = emptyArray() 179 | var expiresAfter: Long = 0 180 | val annotation = method.getAnnotation(clearnet.annotations.InvocationStrategy::class.java) 181 | if (annotation != null) { 182 | invocationStrategies = annotation.value 183 | expiresAfter = annotation.cacheExpiresAfter 184 | } 185 | 186 | return if (invocationStrategies.isEmpty()) { 187 | Pair(MergedInvocationStrategy(arrayOf(InvocationStrategy.NO_CACHE)), expiresAfter) 188 | } else { 189 | Pair(MergedInvocationStrategy(invocationStrategies), expiresAfter) 190 | } 191 | } 192 | 193 | private fun generateAmruRequestParams(requestBody: RPCRequest) = mapOf("applicationMethod" to requestBody.method) 194 | 195 | private fun getMaxBatchSize(method: Method): Int = with(method.getAnnotation(NoBatch::class.java)) { 196 | return if (this == null) maxBatchSize else 1 197 | } 198 | } 199 | 200 | 201 | init { 202 | defaultCallbackHolder = object : ICallbackHolder { 203 | override fun init() {} 204 | override fun clear() {} 205 | override fun hold(disposable: Disposable) {} 206 | override fun createEmpty(type: Class): I = Wrapper.stub(type) as I 207 | override fun wrap(source: I, interfaceType: Class) = source 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2012 The Obvious Corporation and contributors. 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | ``` 17 | ------------------------------------------------------------------------- 18 | Apache License 19 | Version 2.0, January 2004 20 | http://www.apache.org/licenses/ 21 | 22 | 23 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 24 | 25 | 1. Definitions. 26 | 27 | "License" shall mean the terms and conditions for use, reproduction, 28 | and distribution as defined by Sections 1 through 9 of this document. 29 | 30 | "Licensor" shall mean the copyright owner or entity authorized by 31 | the copyright owner that is granting the License. 32 | 33 | "Legal Entity" shall mean the union of the acting entity and all 34 | other entities that control, are controlled by, or are under common 35 | control with that entity. For the purposes of this definition, 36 | "control" means (i) the power, direct or indirect, to cause the 37 | direction or management of such entity, whether by contract or 38 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 39 | outstanding shares, or (iii) beneficial ownership of such entity. 40 | 41 | "You" (or "Your") shall mean an individual or Legal Entity 42 | exercising permissions granted by this License. 43 | 44 | "Source" form shall mean the preferred form for making modifications, 45 | including but not limited to software source code, documentation 46 | source, and configuration files. 47 | 48 | "Object" form shall mean any form resulting from mechanical 49 | transformation or translation of a Source form, including but 50 | not limited to compiled object code, generated documentation, 51 | and conversions to other media types. 52 | 53 | "Work" shall mean the work of authorship, whether in Source or 54 | Object form, made available under the License, as indicated by a 55 | copyright notice that is included in or attached to the work 56 | (an example is provided in the Appendix below). 57 | 58 | "Derivative Works" shall mean any work, whether in Source or Object 59 | form, that is based on (or derived from) the Work and for which the 60 | editorial revisions, annotations, elaborations, or other modifications 61 | represent, as a whole, an original work of authorship. For the purposes 62 | of this License, Derivative Works shall not include works that remain 63 | separable from, or merely link (or bind by name) to the interfaces of, 64 | the Work and Derivative Works thereof. 65 | 66 | "Contribution" shall mean any work of authorship, including 67 | the original version of the Work and any modifications or additions 68 | to that Work or Derivative Works thereof, that is intentionally 69 | submitted to Licensor for inclusion in the Work by the copyright owner 70 | or by an individual or Legal Entity authorized to submit on behalf of 71 | the copyright owner. For the purposes of this definition, "submitted" 72 | means any form of electronic, verbal, or written communication sent 73 | to the Licensor or its representatives, including but not limited to 74 | communication on electronic mailing lists, source code control systems, 75 | and issue tracking systems that are managed by, or on behalf of, the 76 | Licensor for the purpose of discussing and improving the Work, but 77 | excluding communication that is conspicuously marked or otherwise 78 | designated in writing by the copyright owner as "Not a Contribution." 79 | 80 | "Contributor" shall mean Licensor and any individual or Legal Entity 81 | on behalf of whom a Contribution has been received by Licensor and 82 | subsequently incorporated within the Work. 83 | 84 | 2. Grant of Copyright License. Subject to the terms and conditions of 85 | this License, each Contributor hereby grants to You a perpetual, 86 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 87 | copyright license to reproduce, prepare Derivative Works of, 88 | publicly display, publicly perform, sublicense, and distribute the 89 | Work and such Derivative Works in Source or Object form. 90 | 91 | 3. Grant of Patent License. Subject to the terms and conditions of 92 | this License, each Contributor hereby grants to You a perpetual, 93 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 94 | (except as stated in this section) patent license to make, have made, 95 | use, offer to sell, sell, import, and otherwise transfer the Work, 96 | where such license applies only to those patent claims licensable 97 | by such Contributor that are necessarily infringed by their 98 | Contribution(s) alone or by combination of their Contribution(s) 99 | with the Work to which such Contribution(s) was submitted. If You 100 | institute patent litigation against any entity (including a 101 | cross-claim or counterclaim in a lawsuit) alleging that the Work 102 | or a Contribution incorporated within the Work constitutes direct 103 | or contributory patent infringement, then any patent licenses 104 | granted to You under this License for that Work shall terminate 105 | as of the date such litigation is filed. 106 | 107 | 4. Redistribution. You may reproduce and distribute copies of the 108 | Work or Derivative Works thereof in any medium, with or without 109 | modifications, and in Source or Object form, provided that You 110 | meet the following conditions: 111 | 112 | (a) You must give any other recipients of the Work or 113 | Derivative Works a copy of this License; and 114 | 115 | (b) You must cause any modified files to carry prominent notices 116 | stating that You changed the files; and 117 | 118 | (c) You must retain, in the Source form of any Derivative Works 119 | that You distribute, all copyright, patent, trademark, and 120 | attribution notices from the Source form of the Work, 121 | excluding those notices that do not pertain to any part of 122 | the Derivative Works; and 123 | 124 | (d) If the Work includes a "NOTICE" text file as part of its 125 | distribution, then any Derivative Works that You distribute must 126 | include a readable copy of the attribution notices contained 127 | within such NOTICE file, excluding those notices that do not 128 | pertain to any part of the Derivative Works, in at least one 129 | of the following places: within a NOTICE text file distributed 130 | as part of the Derivative Works; within the Source form or 131 | documentation, if provided along with the Derivative Works; or, 132 | within a display generated by the Derivative Works, if and 133 | wherever such third-party notices normally appear. The contents 134 | of the NOTICE file are for informational purposes only and 135 | do not modify the License. You may add Your own attribution 136 | notices within Derivative Works that You distribute, alongside 137 | or as an addendum to the NOTICE text from the Work, provided 138 | that such additional attribution notices cannot be construed 139 | as modifying the License. 140 | 141 | You may add Your own copyright statement to Your modifications and 142 | may provide additional or different license terms and conditions 143 | for use, reproduction, or distribution of Your modifications, or 144 | for any such Derivative Works as a whole, provided Your use, 145 | reproduction, and distribution of the Work otherwise complies with 146 | the conditions stated in this License. 147 | 148 | 5. Submission of Contributions. Unless You explicitly state otherwise, 149 | any Contribution intentionally submitted for inclusion in the Work 150 | by You to the Licensor shall be under the terms and conditions of 151 | this License, without any additional terms or conditions. 152 | Notwithstanding the above, nothing herein shall supersede or modify 153 | the terms of any separate license agreement you may have executed 154 | with Licensor regarding such Contributions. 155 | 156 | 6. Trademarks. This License does not grant permission to use the trade 157 | names, trademarks, service marks, or product names of the Licensor, 158 | except as required for reasonable and customary use in describing the 159 | origin of the Work and reproducing the content of the NOTICE file. 160 | 161 | 7. Disclaimer of Warranty. Unless required by applicable law or 162 | agreed to in writing, Licensor provides the Work (and each 163 | Contributor provides its Contributions) on an "AS IS" BASIS, 164 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 165 | implied, including, without limitation, any warranties or conditions 166 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 167 | PARTICULAR PURPOSE. You are solely responsible for determining the 168 | appropriateness of using or redistributing the Work and assume any 169 | risks associated with Your exercise of permissions under this License. 170 | 171 | 8. Limitation of Liability. In no event and under no legal theory, 172 | whether in tort (including negligence), contract, or otherwise, 173 | unless required by applicable law (such as deliberate and grossly 174 | negligent acts) or agreed to in writing, shall any Contributor be 175 | liable to You for damages, including any direct, indirect, special, 176 | incidental, or consequential damages of any character arising as a 177 | result of this License or out of the use or inability to use the 178 | Work (including but not limited to damages for loss of goodwill, 179 | work stoppage, computer failure or malfunction, or any and all 180 | other commercial damages or losses), even if such Contributor 181 | has been advised of the possibility of such damages. 182 | 183 | 9. Accepting Warranty or Additional Liability. While redistributing 184 | the Work or Derivative Works thereof, You may choose to offer, 185 | and charge a fee for, acceptance of support, warranty, indemnity, 186 | or other liability obligations and/or rights consistent with this 187 | License. However, in accepting such obligations, You may act only 188 | on Your own behalf and on Your sole responsibility, not on behalf 189 | of any other Contributor, and only if You agree to indemnify, 190 | defend, and hold each Contributor harmless for any liability 191 | incurred by, or claims asserted against, such Contributor by reason 192 | of your accepting any such warranty or additional liability. 193 | 194 | END OF TERMS AND CONDITIONS 195 | ``` -------------------------------------------------------------------------------- /processors/src/main/java/processors/RpcResourcesGenerator.java: -------------------------------------------------------------------------------- 1 | package processors; 2 | 3 | import com.google.auto.service.AutoService; 4 | import com.squareup.javapoet.ClassName; 5 | import com.squareup.javapoet.FieldSpec; 6 | import com.squareup.javapoet.JavaFile; 7 | import com.squareup.javapoet.MethodSpec; 8 | import com.squareup.javapoet.ParameterSpec; 9 | import com.squareup.javapoet.ParameterizedTypeName; 10 | import com.squareup.javapoet.TypeName; 11 | import com.squareup.javapoet.TypeSpec; 12 | 13 | import java.io.IOException; 14 | import java.util.ArrayList; 15 | import java.util.Arrays; 16 | import java.util.Collection; 17 | import java.util.Collections; 18 | import java.util.HashMap; 19 | import java.util.HashSet; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | import javax.annotation.processing.AbstractProcessor; 24 | import javax.annotation.processing.Processor; 25 | import javax.annotation.processing.RoundEnvironment; 26 | import javax.annotation.processing.SupportedSourceVersion; 27 | import javax.lang.model.SourceVersion; 28 | import javax.lang.model.element.Element; 29 | import javax.lang.model.element.ExecutableElement; 30 | import javax.lang.model.element.Modifier; 31 | import javax.lang.model.element.TypeElement; 32 | import javax.lang.model.element.VariableElement; 33 | import javax.lang.model.type.DeclaredType; 34 | import javax.lang.model.type.MirroredTypeException; 35 | import javax.tools.Diagnostic; 36 | 37 | import clearnet.annotations.RPCMethod; 38 | import clearnet.annotations.RPCMethodScope; 39 | import clearnet.annotations.ResultType; 40 | 41 | @AutoService(Processor.class) 42 | @SupportedSourceVersion(SourceVersion.RELEASE_7) 43 | public class RpcResourcesGenerator extends BaseProcessor { 44 | 45 | @Override 46 | public Set getSupportedAnnotationTypes() { 47 | return new HashSet<>(Arrays.asList( 48 | RPCMethodScope.class.getCanonicalName(), 49 | RPCMethod.class.getCanonicalName() 50 | )); 51 | } 52 | 53 | @Override 54 | public boolean process(Set set, RoundEnvironment roundEnvironment) { 55 | Map> tree = new HashMap<>(); 56 | for (Element source : roundEnvironment.getElementsAnnotatedWith(RPCMethodScope.class)) { 57 | if (source instanceof TypeElement) { 58 | RPCMethodScope scopeAnnotation = source.getAnnotation(RPCMethodScope.class); 59 | for (Element element : source.getEnclosedElements()) { 60 | if (!(element instanceof ExecutableElement)) continue; 61 | addMethodIfHas(element, tree, scopeAnnotation.value()); 62 | } 63 | } else { 64 | addMethodIfHas(source, tree, null); 65 | } 66 | } 67 | 68 | for (Element source : roundEnvironment.getElementsAnnotatedWith(RPCMethod.class)) { 69 | addMethodIfHas(source, tree, null); 70 | } 71 | 72 | writeResourcesFile(tree); 73 | writeSubscriberClass(tree); 74 | return true; 75 | } 76 | 77 | private void addMethodIfHas(Element element, Map> tree, String scope) { 78 | RPCMethod methodAnnotation = element.getAnnotation(RPCMethod.class); 79 | RPCMethodScope scopeAnnotation = element.getAnnotation(RPCMethodScope.class); 80 | 81 | if (methodAnnotation == null && scopeAnnotation == null && scope == null) 82 | return; // todo error 83 | 84 | if (methodAnnotation != null && scopeAnnotation != null) { 85 | error(element, "The element can have only one of these annotations: " + RPCMethod.class.getSimpleName() + " or " + RPCMethodScope.class.getSimpleName()); 86 | } 87 | 88 | String method; 89 | 90 | 91 | if (scopeAnnotation != null) { 92 | scope = scopeAnnotation.value(); 93 | method = element.getSimpleName().toString(); 94 | } else if (methodAnnotation != null) { 95 | String[] parts = methodAnnotation.value().split("\\."); 96 | if (parts.length != 2) { 97 | error(element, "Invalid method \"" + methodAnnotation.value() + "\". It must looks: scope.method."); 98 | return; 99 | } 100 | scope = parts[0]; 101 | method = parts[1]; 102 | } else { 103 | method = element.getSimpleName().toString(); 104 | } 105 | 106 | if (!scope.isEmpty() && !scope.matches("[a-zA-Z0-9]+")) { 107 | error(element, "Invalid scope"); 108 | return; 109 | } 110 | 111 | if (!method.matches("[a-zA-Z0-9]+")) { 112 | error(element, "Invalid method"); 113 | return; 114 | } 115 | 116 | Set methods = tree.get(scope); 117 | if (methods == null) { 118 | methods = new HashSet<>(); 119 | tree.put(scope, methods); 120 | } 121 | 122 | methods.add(new NameTypePair(method, resolveCallbackType((ExecutableElement) element))); 123 | } 124 | 125 | private TypeName resolveCallbackType(ExecutableElement element) { 126 | ResultType annotation = element.getAnnotation(ResultType.class); 127 | if (annotation != null) { 128 | 129 | // solution from https://stackoverflow.com/questions/7687829/java-6-annotation-processing-getting-a-class-from-an-annotation 130 | try { 131 | return TypeName.get(annotation.value()); 132 | } catch (MirroredTypeException mte) { 133 | return TypeName.get(mte.getTypeMirror()); 134 | } 135 | } 136 | 137 | for (VariableElement parameter : element.getParameters()) { 138 | if (parameter.asType().toString().startsWith("clearnet.interfaces.RequestCallback")) { 139 | DeclaredType dclt = (DeclaredType) parameter.asType(); 140 | return TypeName.get(dclt.getTypeArguments().get(0)); 141 | } 142 | } 143 | warning(element, "Cannot define result type"); 144 | return TypeName.get(Object.class); 145 | } 146 | 147 | private TypeSpec buildScopeClass(String scope, Collection pairs) { 148 | TypeSpec.Builder builder = TypeSpec.classBuilder(scope.isEmpty() ? "NoScope" : scope) 149 | .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL); 150 | 151 | Set methods = new HashSet<>(); 152 | for (NameTypePair pair : pairs) methods.add(pair.name); 153 | for (String method : methods) { 154 | builder.addField( 155 | FieldSpec.builder(String.class, method, Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) 156 | .initializer("$S", scope.isEmpty() ? method : scope + "." + method) 157 | .build() 158 | ); 159 | } 160 | 161 | return builder.build(); 162 | } 163 | 164 | private void writeResourcesFile(Map> tree) { 165 | if (tree.isEmpty()) return; 166 | 167 | TypeSpec.Builder builder = TypeSpec.classBuilder("NR") 168 | .addModifiers(Modifier.PUBLIC, Modifier.FINAL); 169 | 170 | for (Map.Entry> entry : tree.entrySet()) { 171 | builder.addType(buildScopeClass(entry.getKey(), entry.getValue())); 172 | } 173 | 174 | try { 175 | // todo add custom compiler option with package name 176 | JavaFile javaFile = JavaFile.builder(processingEnv.getOptions().get("android.databinding.modulePackage"), builder.build()) 177 | .build(); 178 | javaFile.writeTo(processingEnv.getFiler()); 179 | } catch (IOException e) { 180 | throw new RuntimeException(e); 181 | } 182 | } 183 | 184 | private void writeSubscriberClass(Map> tree) { 185 | if (tree.isEmpty()) return; 186 | 187 | TypeSpec.Builder builder = TypeSpec.classBuilder("ClearNet") 188 | .addModifiers(Modifier.PUBLIC); 189 | 190 | TypeName callbackStorageTypeName = ClassName.bestGuess("clearnet.interfaces.ICallbackStorage"); 191 | 192 | final String callbachStorageFieldName = "callbackStorage"; 193 | builder.addMethod(MethodSpec.constructorBuilder().addParameter( 194 | ParameterSpec.builder(ClassName.bestGuess("clearnet.interfaces.ICallbackStorage"), callbachStorageFieldName).build() 195 | ) 196 | .addModifiers(Modifier.PUBLIC) 197 | .addStatement("this.$L = $L", callbachStorageFieldName, callbachStorageFieldName) 198 | .build()); 199 | 200 | builder.addField(callbackStorageTypeName, "callbackStorage", Modifier.PRIVATE, Modifier.FINAL); 201 | 202 | ArrayList sortedScopes = new ArrayList<>(tree.keySet()); 203 | Collections.sort(sortedScopes); 204 | ArrayList sortedPairs; 205 | for (String scope : sortedScopes) { 206 | sortedPairs = new ArrayList<>(tree.get(scope)); 207 | Collections.sort(sortedPairs); 208 | addClassForScope(builder, scope, sortedPairs); 209 | } 210 | 211 | try { 212 | String packageName = processingEnv.getOptions().get("packageName"); 213 | if(packageName == null || packageName.isEmpty()){ 214 | throw new IllegalArgumentException("Argument 'packageName' not set in your gradle file"); 215 | } 216 | 217 | JavaFile.builder(packageName, builder.build()) 218 | .build() 219 | .writeTo(processingEnv.getFiler()); 220 | } catch (IOException e) { 221 | throw new RuntimeException(e); 222 | } 223 | } 224 | 225 | private void addClassForScope(TypeSpec.Builder typeBuilder, String scope, Collection pairs) { 226 | final String name = scope.isEmpty() ? "NoScope" : scope; 227 | TypeSpec.Builder builder = TypeSpec.classBuilder(scope.isEmpty() ? "NoScope" : capitalize(scope)) 228 | .addModifiers(Modifier.PUBLIC); 229 | 230 | 231 | final Set names = new HashSet<>(); 232 | final Set excepted = new HashSet<>(); 233 | for (NameTypePair pair : pairs) { 234 | if (!names.add(pair.name)) { 235 | warning(null, "Conflicted result type for method " + scope + "." + pair.name + ". Method missed"); 236 | excepted.add(pair.name); 237 | } 238 | } 239 | 240 | for (NameTypePair pair : pairs) { 241 | if(excepted.contains(pair.name)) continue; 242 | TypeName subscriberTypeName = ParameterizedTypeName.get(ClassName.bestGuess("clearnet.utils.Subscriber"), pair.callbackType); 243 | builder.addMethod( 244 | MethodSpec.methodBuilder(pair.name) 245 | .returns(subscriberTypeName) 246 | .addModifiers(Modifier.PUBLIC) 247 | .addStatement("return new $T(callbackStorage, $S)", subscriberTypeName, scope.isEmpty() ? pair.name : scope + "." + pair.name) 248 | .build() 249 | ); 250 | } 251 | 252 | TypeSpec result = builder.build(); 253 | 254 | typeBuilder.addType(result); 255 | typeBuilder.addField( 256 | FieldSpec.builder(ClassName.get("", capitalize(name)), name, Modifier.PUBLIC, Modifier.FINAL) 257 | .initializer("new $L()", capitalize(name)) 258 | .build() 259 | ); 260 | } 261 | 262 | private String capitalize(final String line) { 263 | return Character.toUpperCase(line.charAt(0)) + line.substring(1); 264 | } 265 | 266 | private static class NameTypePair implements Comparable { 267 | final String name; 268 | final TypeName callbackType; 269 | 270 | private NameTypePair(String name, TypeName callbackType) { 271 | this.name = name; 272 | this.callbackType = callbackType; 273 | } 274 | 275 | @Override 276 | public boolean equals(Object o) { 277 | if (this == o) return true; 278 | if (!(o instanceof NameTypePair)) return false; 279 | 280 | NameTypePair that = (NameTypePair) o; 281 | 282 | if (name != null ? !name.equals(that.name) : that.name != null) return false; 283 | return callbackType != null ? callbackType.equals(that.callbackType) : that.callbackType == null; 284 | 285 | } 286 | 287 | @Override 288 | public int hashCode() { 289 | int result = name != null ? name.hashCode() : 0; 290 | result = 31 * result + (callbackType != null ? callbackType.hashCode() : 0); 291 | return result; 292 | } 293 | 294 | @Override 295 | public int compareTo(NameTypePair nameTypePair) { 296 | return name.compareTo(nameTypePair.name); 297 | } 298 | } 299 | } 300 | --------------------------------------------------------------------------------