├── .gitattributes ├── settings.gradle ├── app ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ └── layout │ │ │ │ └── activity_main.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── goodow │ │ │ │ └── realtime │ │ │ │ └── android │ │ │ │ └── demo │ │ │ │ ├── test │ │ │ │ ├── TestBViewModel.java │ │ │ │ ├── TestCActivity.java │ │ │ │ └── TestBActivity.java │ │ │ │ ├── TestAActivity.java │ │ │ │ └── MainActivity.java │ │ ├── proto │ │ │ └── view_model.proto │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── goodow │ │ │ └── realtime │ │ │ └── android │ │ │ └── demo │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── goodow │ │ └── realtime │ │ └── android │ │ └── demo │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro ├── google-services.json └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── realtime-channel ├── src │ ├── main │ │ ├── res │ │ │ └── values │ │ │ │ └── strings.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── goodow │ │ │ │ └── realtime │ │ │ │ ├── android │ │ │ │ ├── mvp │ │ │ │ │ ├── IView.java │ │ │ │ │ ├── IPresenter.java │ │ │ │ │ ├── Router.java │ │ │ │ │ ├── util │ │ │ │ │ │ ├── JsonMapper.java │ │ │ │ │ │ ├── BeanUtils.java │ │ │ │ │ │ └── CustomClassMapper.java │ │ │ │ │ └── RouteBuilder.java │ │ │ │ └── ChannelInitProvider.java │ │ │ │ └── channel │ │ │ │ ├── impl │ │ │ │ ├── AsyncResultImpl.java │ │ │ │ ├── MessageImpl.java │ │ │ │ └── SimpleBus.java │ │ │ │ ├── Handler.java │ │ │ │ ├── util │ │ │ │ ├── BusProvider.java │ │ │ │ ├── MessageHandler.java │ │ │ │ ├── AsyncResultHandler.java │ │ │ │ └── IdGenerator.java │ │ │ │ ├── AsyncResult.java │ │ │ │ ├── Registration.java │ │ │ │ ├── mqtt │ │ │ │ ├── Token.java │ │ │ │ └── Topic.java │ │ │ │ ├── Message.java │ │ │ │ ├── firebase │ │ │ │ └── FirebaseChannel.java │ │ │ │ └── Bus.java │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── goodow │ │ │ └── realtime │ │ │ └── channel │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── goodow │ │ └── realtime │ │ └── channel │ │ └── ExampleInstrumentedTest.java ├── generateProbobuf.sh ├── protos │ ├── goodow_bool.proto │ ├── goodow_channel.proto │ └── goodow_extras_option.proto ├── proguard-rules.pro └── build.gradle ├── .gitignore ├── .travis.yml ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':realtime-channel' 2 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Realtime Demo 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /realtime-channel/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | realtime-channel 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /realtime-channel/generateProbobuf.sh: -------------------------------------------------------------------------------- 1 | protoc --java_out=src/main/java -Iprotos \ 2 | protos/goodow_channel.proto \ 3 | protos/goodow_extras_option.proto -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodow/realtime-android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/android/mvp/IView.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.mvp; 2 | 3 | /** 4 | * Created by larry on 2017/11/7. 5 | */ 6 | 7 | public interface IView { 8 | 9 | IPresenter presenter(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/android/mvp/IPresenter.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.mvp; 2 | 3 | /** 4 | * Created by larry on 2017/11/7. 5 | */ 6 | 7 | public interface IPresenter { 8 | 9 | void update(IView view, T data); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/goodow/realtime/android/demo/test/TestBViewModel.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.demo.test; 2 | 3 | /** 4 | * Created by larry on 2017/11/9. 5 | */ 6 | 7 | public class TestBViewModel { 8 | public String title; 9 | 10 | public int id; 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Nov 08 18:48:05 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-all.zip 7 | -------------------------------------------------------------------------------- /realtime-channel/protos/goodow_bool.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | package goodow.protobuf; 3 | 4 | option objc_class_prefix = 'GDPB'; 5 | option java_package = "com.goodow.realtime.channel.protobuf"; 6 | option java_outer_classname = "BoolProtos"; 7 | 8 | enum Bool { 9 | default = 0; // 默认值 10 | true = 1; 11 | false = 2; 12 | }; -------------------------------------------------------------------------------- /app/src/main/proto/view_model.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package goodow.android.demo; 3 | 4 | option java_package = "com.goodow.realtime.android.demo.test"; 5 | option java_outer_classname = "ViewModelProtos"; 6 | option java_multiple_files = true; 7 | option optimize_for = LITE_RUNTIME; 8 | //option optimize_for = CODE_SIZE; 9 | 10 | message TestBViewModel { 11 | int32 id = 1; 12 | string title = 2; 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /realtime-channel/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/test/java/com/goodow/realtime/android/demo/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.demo; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /realtime-channel/src/test/java/com/goodow/realtime/channel/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.channel; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() throws Exception { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/goodow/realtime/android/demo/TestAActivity.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.demo; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | 7 | import com.goodow.realtime.android.mvp.Router; 8 | 9 | /** 10 | * Created by larry on 2017/11/7. 11 | */ 12 | 13 | public class TestAActivity extends Activity { 14 | 15 | @Override 16 | protected void onCreate(@Nullable Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | 19 | Object data = Router.getInstance().getData(this); 20 | if (data != null) { 21 | 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven build output. 2 | target/ 3 | 4 | # Generated for Eclipse. 5 | .settings/ 6 | .project 7 | .classpath 8 | 9 | # Created by OS. 10 | .DS_Store 11 | 12 | # Generated for IntelliJ IDEA. 13 | .idea/ 14 | *.ipr 15 | *.iws 16 | *.iml 17 | ## Additional for IntelliJ 18 | out/ 19 | # generated by mpeltonen/sbt-idea plugin 20 | .idea_modules/ 21 | # generated by JIRA plugin 22 | atlassian-ide-plugin.xml 23 | # generated by Crashlytics plugin (for Android Studio and Intellij) 24 | com_crashlytics_export_strings.xml 25 | 26 | 27 | 28 | .gradle 29 | /local.properties 30 | /.idea/workspace.xml 31 | /.idea/libraries 32 | build 33 | /captures 34 | .externalNativeBuild 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/goodow/realtime/android/demo/test/TestCActivity.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.demo.test; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | 7 | import com.goodow.realtime.android.mvp.Router; 8 | 9 | /** 10 | * Created by larry on 2017/11/8. 11 | */ 12 | 13 | public class TestCActivity extends Activity { 14 | 15 | public String title; 16 | public int id; 17 | 18 | @Override 19 | protected void onCreate(@Nullable Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | 22 | Router.getInstance().inject(this); 23 | String title = this.title; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | before_install: "wget -P target/travis https://raw.githubusercontent.com/goodow/maven/master/settings.xml" 3 | script: "[ ${TRAVIS_PULL_REQUEST} = 'false' ] && mvn clean deploy -Psonatype-oss-release -Dgpg.skip=true --settings target/travis/settings.xml || mvn clean verify --settings target/travis/settings.xml" 4 | 5 | env: 6 | global: 7 | - secure: fbR8iRrTHQl9QokGPCSLoWBsX+TTdPthmbJf1jRrkQLcDqAnOWEZVizWNJDbTmDfLlcrJgP/8Z1YRe1IiEdJj3c3cKUhADVH8hL0DnJnD83B0ORAX571svc8V+xbK8euWIPFLfrYXPNmZUKArqpjh56RSCjqljl8GuK9TisPYmU= 8 | - secure: b9MQM+SwEc6RAbvoMYeJoFaAHEg47NZa4RuCUkA/4gxkDSTMJtrhO/+Dv+BFdMkWppSnIsqOYt5q3HprsbffiPZILD8jgM6fis0lLkf2gUDlY0qkdIgHAUuCESe1JUWt5+NBg0S9pXZRRKgBHZLvwBKvr3Ft/leITijhqy1rvG8= 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/goodow/realtime/android/demo/test/TestBActivity.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.demo.test; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | 7 | import com.goodow.realtime.android.mvp.Router; 8 | 9 | /** 10 | * Created by larry on 2017/11/8. 11 | */ 12 | 13 | public class TestBActivity extends Activity { 14 | 15 | @Override 16 | protected void onCreate(@Nullable Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | 19 | TestBViewModel data = Router.getInstance().getData(this); 20 | if (data != null) { 21 | int id = data.id; 22 | String title = data.title; 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/impl/AsyncResultImpl.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.channel.impl; 2 | 3 | import com.goodow.realtime.channel.Message; 4 | import com.goodow.realtime.channel.AsyncResult; 5 | 6 | public class AsyncResultImpl implements AsyncResult { 7 | private Throwable cause; 8 | private T result; 9 | 10 | public AsyncResultImpl(Message message) { 11 | if (message.payload() instanceof Throwable) { 12 | this.cause = (Throwable)message.payload(); 13 | } 14 | this.result = (T) message; 15 | } 16 | 17 | @Override 18 | public Throwable cause() { 19 | return cause; 20 | } 21 | 22 | @Override 23 | public boolean failed() { 24 | return cause != null; 25 | } 26 | 27 | @Override 28 | public T result() { 29 | return result; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/goodow/realtime/android/demo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.demo; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.goodow.realtime.android.demo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /realtime-channel/src/androidTest/java/com/goodow/realtime/channel/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.channel; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.goodow.realtime.channel.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/Handler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel; 15 | 16 | /** 17 | * A generic event handler 18 | */ 19 | public interface Handler { 20 | 21 | /** 22 | * Something has happened, so handle it. 23 | */ 24 | void handle(E event); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/util/BusProvider.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.channel.util; 2 | 3 | import com.goodow.realtime.channel.Bus; 4 | import com.goodow.realtime.channel.firebase.FirebaseChannel; 5 | import com.goodow.realtime.channel.impl.SimpleBus; 6 | 7 | /** 8 | * Created by larry on 2017/12/1. 9 | */ 10 | public class BusProvider { 11 | private volatile static Bus instance = null; 12 | private static FirebaseChannel firebaseChannel; 13 | 14 | public static Bus get() { 15 | if (instance != null) { 16 | return instance; 17 | } 18 | synchronized (BusProvider.class) { 19 | if (instance == null) { 20 | instance = new SimpleBus(); 21 | } 22 | } 23 | return instance; 24 | } 25 | 26 | public static Bus enableRemoteBus() { 27 | if (firebaseChannel != null) { 28 | return BusProvider.get(); 29 | } 30 | firebaseChannel = new FirebaseChannel(BusProvider.get()); 31 | return BusProvider.get(); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/util/MessageHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel.util; 15 | 16 | import com.goodow.realtime.channel.Handler; 17 | import com.goodow.realtime.channel.Message; 18 | 19 | /** 20 | * Handler for {@link Message} 21 | */ 22 | public interface MessageHandler extends Handler> { 23 | @Override 24 | public void handle(Message message); 25 | } 26 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/larry/dev/tools/bin/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /realtime-channel/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/larry/dev/tools/bin/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/util/AsyncResultHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel.util; 15 | 16 | import com.goodow.realtime.channel.AsyncResult; 17 | import com.goodow.realtime.channel.Handler; 18 | import com.goodow.realtime.channel.Message; 19 | 20 | /** 21 | * Handler for {@link AsyncResult} 22 | */ 23 | public interface AsyncResultHandler extends Handler>> { 24 | 25 | @Override 26 | void handle(AsyncResult> asyncResult); 27 | } 28 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/AsyncResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel; 15 | 16 | /** 17 | * Represents a result that may not have occurred yet. 18 | */ 19 | public interface AsyncResult { 20 | /** 21 | * An exception describing failure. This will be null if the operation succeeded. 22 | */ 23 | Throwable cause(); 24 | 25 | /** 26 | * Did it fail? 27 | */ 28 | boolean failed(); 29 | 30 | /** 31 | * The result of the operation. This will be null if the operation failed. 32 | */ 33 | T result(); 34 | } -------------------------------------------------------------------------------- /app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "598242009211", 4 | "firebase_url": "https://tencent-live.firebaseio.com", 5 | "project_id": "tencent-live", 6 | "storage_bucket": "tencent-live.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:598242009211:android:3f93420252c0c761", 12 | "android_client_info": { 13 | "package_name": "com.goodow.realtime.android.demo" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "598242009211-ovaujh1m9aliomk2qurq62udsb59uviv.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyDOXVZMrr8aBNnDnCjAo702dhWsyshN5gg" 25 | } 26 | ], 27 | "services": { 28 | "analytics_service": { 29 | "status": 1 30 | }, 31 | "appinvite_service": { 32 | "status": 1, 33 | "other_platform_oauth_client": [] 34 | }, 35 | "ads_service": { 36 | "status": 2 37 | } 38 | } 39 | } 40 | ], 41 | "configuration_version": "1" 42 | } -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/Registration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel; 15 | 16 | /** 17 | * Registration objects returned when an event handler is bound (e.g. via 18 | * {@link com.goodow.realtime.channel.Bus#subscribe}), used to deregister. 19 | */ 20 | public interface Registration { 21 | 22 | Registration EMPTY = new Registration() { 23 | @Override 24 | public void unregister() { 25 | } 26 | }; 27 | 28 | /** 29 | * Deregisters the handler associated with this registration object if the handler is still 30 | * attached to the event bus. If the handler is no longer attached to the event bus, this is a 31 | * no-op. 32 | */ 33 | void unregister(); 34 | } 35 | -------------------------------------------------------------------------------- /realtime-channel/protos/goodow_channel.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | package goodow.channel; 3 | 4 | option objc_class_prefix = 'GDCPB'; 5 | option java_package = "com.goodow.realtime.channel.protobuf"; 6 | option java_outer_classname = "ChannelProtos"; 7 | 8 | import "google/protobuf/any.proto"; 9 | 10 | message Message { 11 | string topic = 1; 12 | google.protobuf.Any payload = 2; 13 | Options options = 3; 14 | string replyTopic = 4; 15 | bool local = 5; 16 | bool send = 6; 17 | 18 | message Options { 19 | bool retained = 1; 20 | bool patch = 2; 21 | int64 timeout = 3; 22 | int32 qos = 4; 23 | google.protobuf.Any extras = 5; 24 | } 25 | } 26 | 27 | // https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html 28 | message ApplePushNotification { 29 | ApnsPayload aps = 1; 30 | Message gdc = 2; 31 | GoogleGCM gcm = 3; 32 | string du = 100; 33 | 34 | message ApnsPayload { 35 | Alert alert = 1; 36 | int32 badge = 2; 37 | string sound = 3; 38 | // int32 content-available = 4; 39 | string category = 5; 40 | // string thread-id = 6; 41 | 42 | message Alert { 43 | string title = 1; 44 | string body = 2; 45 | // string title-loc-key = 1; 46 | } 47 | } 48 | message GoogleGCM { 49 | string message_id = 1; 50 | } 51 | } -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/mqtt/Token.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.channel.mqtt; 2 | 3 | /** 4 | * Created by larry on 2017/10/30. 5 | */ 6 | 7 | /** 8 | * Internal use only class. 9 | */ 10 | class Token { 11 | 12 | static final Token EMPTY = new Token(""); 13 | static final Token MULTI = new Token("#"); 14 | static final Token SINGLE = new Token("+"); 15 | final String name; 16 | 17 | protected Token(String s) { 18 | name = s; 19 | } 20 | 21 | protected String name() { 22 | return name; 23 | } 24 | 25 | protected boolean match(Token t) { 26 | if (t == MULTI || t == SINGLE) { 27 | return false; 28 | } 29 | 30 | if (this == MULTI || this == SINGLE) { 31 | return true; 32 | } 33 | 34 | return equals(t); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | int hash = 7; 40 | hash = 29 * hash + (this.name != null ? this.name.hashCode() : 0); 41 | return hash; 42 | } 43 | 44 | @Override 45 | public boolean equals(Object obj) { 46 | if (obj == null) { 47 | return false; 48 | } 49 | if (getClass() != obj.getClass()) { 50 | return false; 51 | } 52 | final Token other = (Token) obj; 53 | if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) { 54 | return false; 55 | } 56 | return true; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return name; 62 | } 63 | } -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/util/IdGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel.util; 15 | 16 | import java.util.Random; 17 | 18 | public class IdGenerator { 19 | 20 | /** valid characters. */ 21 | static final char[] WEB64_ALPHABET = 22 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray(); 23 | static final char[] NUMBERS = "0123456789".toCharArray(); 24 | 25 | private final Random random; 26 | 27 | public IdGenerator() { 28 | this(new Random()); 29 | } 30 | 31 | public IdGenerator(Random random) { 32 | this.random = random; 33 | } 34 | 35 | /** 36 | * Returns a string with {@code length} random characters. 37 | */ 38 | public String next(int length) { 39 | StringBuilder result = new StringBuilder(length); 40 | for (int i = 0; i < length; i++) { 41 | result.append(WEB64_ALPHABET[random.nextInt(64)]); 42 | } 43 | return result.toString(); 44 | } 45 | 46 | public String nextNumbers(int length) { 47 | StringBuilder result = new StringBuilder(length); 48 | for (int i = 0; i < length; i++) { 49 | result.append(NUMBERS[random.nextInt(10)]); 50 | } 51 | return result.toString(); 52 | } 53 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | //apply plugin: 'com.google.firebase.firebase-perf' 3 | 4 | android { 5 | compileSdkVersion 26 6 | buildToolsVersion "26.0.2" 7 | defaultConfig { 8 | applicationId "com.goodow.realtime.android.demo" 9 | minSdkVersion 15 10 | targetSdkVersion 26 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(dir: 'libs', include: ['*.jar']) 25 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 26 | exclude group: 'com.android.support', module: 'support-annotations' 27 | }) 28 | 29 | compile 'com.android.support:appcompat-v7:26.+' 30 | compile 'com.android.support.constraint:constraint-layout:1.0.2' 31 | testCompile 'junit:junit:4.12' 32 | 33 | compile 'com.google.android.gms:play-services-basement:11.6.0' 34 | compile project(':realtime-channel') 35 | } 36 | 37 | apply plugin: 'com.google.gms.google-services' 38 | 39 | //apply plugin: 'com.google.protobuf' 40 | //protobuf { 41 | // protoc { 42 | // artifact = 'com.google.protobuf:protoc:3.4.0' 43 | // } 44 | // plugins { 45 | // javalite { 46 | // artifact = 'com.google.protobuf:protoc-gen-javalite:3.0.0' 47 | // } 48 | // } 49 | // generateProtoTasks { 50 | // all().each { task -> 51 | // task.builtins { 52 | // // In most cases you don't need the full Java output 53 | // // if you use the lite output. 54 | // remove java 55 | // } 56 | // task.plugins { 57 | // javalite {} 58 | // } 59 | // } 60 | // } 61 | //} 62 | // 63 | 64 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/android/mvp/Router.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.mvp; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.content.Context; 6 | 7 | import com.goodow.realtime.android.mvp.util.BeanUtils; 8 | 9 | import java.util.Map; 10 | import java.util.WeakHashMap; 11 | 12 | import static com.goodow.realtime.android.mvp.RouteBuilder.ACTIVITY_ID; 13 | 14 | /** 15 | * Created by larry on 2017/11/7. 16 | */ 17 | 18 | public class Router { 19 | private volatile static Router instance = null; 20 | static Context context; 21 | static WeakHashMap intentCache = new WeakHashMap<>(); 22 | 23 | /** 24 | * Init, it must be call before used router. 25 | */ 26 | public static void init(Application application) { 27 | if (context != null) { 28 | return; 29 | } 30 | context = application; 31 | } 32 | 33 | /** 34 | * Get instance of router. 35 | * All feature U use, will be starts here. 36 | */ 37 | public static Router getInstance() { 38 | if (context == null) { 39 | throw new RuntimeException("Router::Init::Invoke init(context) first!"); 40 | } 41 | if (instance == null) { 42 | synchronized (Router.class) { 43 | if (instance == null) { 44 | instance = new Router(); 45 | } 46 | } 47 | } 48 | return instance; 49 | } 50 | 51 | private Router() { 52 | } 53 | 54 | /** 55 | * Inject params. 56 | */ 57 | public void inject(Activity activity) { 58 | Object data = this.getData(activity); 59 | if (data instanceof Map) { 60 | BeanUtils.populate(activity, (Map) data); 61 | } 62 | } 63 | 64 | public T getData(Activity activity) { 65 | return (T) Router.intentCache.get(activity.getIntent().getStringExtra(ACTIVITY_ID)); 66 | } 67 | 68 | public RouteBuilder withData(Object data) { 69 | return new RouteBuilder(data); 70 | } 71 | 72 | public void goToClass(Class activity) { 73 | new RouteBuilder(null).goToClass(activity); 74 | } 75 | 76 | public void goToUrl(String url) { 77 | new RouteBuilder(null).goToUrl(url); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/Message.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel; 15 | 16 | /** 17 | * Represents a message on the event bus. 18 | */ 19 | public interface Message { 20 | /** 21 | * The payload of the message 22 | */ 23 | T payload(); 24 | 25 | /** 26 | * Signal that processing of this message failed. If the message was sent specifying a result 27 | * handler the handler will be called with a failure corresponding to the failure code and message 28 | * specified here 29 | * 30 | * @param failureCode A failure code to pass back to the sender 31 | * @param msg A message to pass back to the sender 32 | */ 33 | void fail(int failureCode, String msg); 34 | 35 | /** 36 | * @return Whether this message originated in the local session. 37 | */ 38 | boolean isLocal(); 39 | 40 | /** 41 | * Reply to this message. If the message was sent specifying a reply handler, that handler will be 42 | * called when it has received a reply. If the message wasn't sent specifying a receipt handler 43 | * this method does nothing. 44 | */ 45 | void reply(Object msg); 46 | 47 | /** 48 | * The same as {@code reply(Object msg)} but you can specify handler for the reply - i.e. to 49 | * receive the reply to the reply. 50 | */ 51 | @SuppressWarnings("hiding") 52 | void reply(Object msg, Handler>> replyHandler); 53 | 54 | /** 55 | * The reply topic (if any) 56 | */ 57 | String replyTopic(); 58 | 59 | /** 60 | * The topic the message was sent to 61 | */ 62 | String topic(); 63 | } 64 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/impl/MessageImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel.impl; 15 | 16 | import com.goodow.realtime.channel.Bus; 17 | import com.goodow.realtime.channel.Message; 18 | import com.goodow.realtime.channel.AsyncResult; 19 | import com.goodow.realtime.channel.Handler; 20 | 21 | class MessageImpl implements Message { 22 | protected U payload; 23 | protected Bus bus; 24 | protected String topic; 25 | protected String replyTopic; 26 | protected boolean send; // Is it a send or a publish? 27 | protected boolean local; 28 | 29 | public MessageImpl(boolean local, boolean send, Bus bus, String topic, String replyTopic, 30 | U payload) { 31 | this.local = local; 32 | this.send = send; 33 | this.bus = bus; 34 | this.topic = topic; 35 | this.replyTopic = replyTopic; 36 | this.payload = payload; 37 | } 38 | 39 | @Override 40 | public String topic() { 41 | return topic; 42 | } 43 | 44 | @Override 45 | public U payload() { 46 | return payload; 47 | } 48 | 49 | @Override 50 | public void fail(int failureCode, String msg) { 51 | // sendReply(new ReplyException(ReplyFailure.RECIPIENT_FAILURE, failureCode, message), null); 52 | } 53 | 54 | @Override 55 | public boolean isLocal() { 56 | return local; 57 | } 58 | 59 | @Override 60 | public void reply(Object msg) { 61 | sendReply(msg, null); 62 | } 63 | 64 | @Override 65 | public void reply(Object msg, Handler>> replyHandler) { 66 | sendReply(msg, replyHandler); 67 | } 68 | 69 | @Override 70 | public String replyTopic() { 71 | return replyTopic; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return payload == null ? null : payload.toString(); 77 | } 78 | 79 | private void sendReply(Object msg, Handler>> replyHandler) { 80 | if (bus != null && replyTopic != null) { 81 | // Send back reply 82 | if (local) { 83 | bus.sendLocal(replyTopic, msg, replyHandler); 84 | } else { 85 | bus.send(replyTopic, msg, replyHandler); 86 | } 87 | } 88 | } 89 | 90 | @Override 91 | protected MessageImpl clone() { 92 | return new MessageImpl(this.local, this.send, this.bus, this.topic, this.replyTopic, this.payload); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | realtime-android [![Build Status](https://travis-ci.org/goodow/realtime-android.svg?branch=master)](https://travis-ci.org/goodow/realtime-android) 2 | ================ 3 | 4 | Event bus client over WebSocket for java and andorid 5 | 6 | Visit [Google groups](https://groups.google.com/forum/#!forum/goodow-realtime) for discussions and announcements. 7 | 8 | ## Adding realtime-android to your project 9 | 10 | ### Maven 11 | 12 | ```xml 13 | 14 | 15 | sonatype-nexus-snapshots 16 | Sonatype Nexus Snapshots 17 | https://oss.sonatype.org/content/repositories/snapshots 18 | 19 | false 20 | 21 | 22 | true 23 | 24 | 25 | 26 | 27 | 28 | com.goodow.realtime 29 | realtime-android 30 | 0.5.5-SNAPSHOT 31 | 32 | 33 | ``` 34 | 35 | ## Usage 36 | 37 | ### WebSocket mode 38 | ```java 39 | AndroidPlatform.register(); // or JavaPlatform.register(); 40 | 41 | Bus bus = new ReconnectBus("ws://localhost:1986/channel/websocket", null); 42 | 43 | bus.subscribe("some/topic", new MessageHandler() { 44 | @Override 45 | public void handle(Message message) { 46 | JsonObject payload = message.payload(); 47 | System.out.println("Name: " + payload.get("name")); 48 | } 49 | }); 50 | 51 | bus.publish("some/topic", Json.createObject().set("name", "Larry Tin")); 52 | ``` 53 | 54 | ```java 55 | AndroidPlatform.register(); // or JavaPlatform.register(); 56 | 57 | Store store = new StoreImpl("ws://localhost:1986/channel/websocket", null); 58 | Bus bus = store.getBus(); 59 | 60 | Handler onLoaded = new Handler() { 61 | @Override 62 | public void handle(Document document) { 63 | Model model = document.getModel(); 64 | CollaborativeMap root = model.getRoot(); 65 | CollaborativeString name = root.get("name"); 66 | System.out.println("Name: " + name.getText()); 67 | } 68 | }; 69 | 70 | Handler opt_initializer = new Handler() { 71 | @Override 72 | public void handle(Model model) { 73 | CollaborativeString name = model.createString("Larry Tin"); 74 | CollaborativeMap root = mod.getRoot(); 75 | root.set("name", name); 76 | } 77 | }; 78 | 79 | store.load("docType/docId", onLoaded, opt_initializer, null); 80 | ``` 81 | 82 | See [WebSocketBusTest](https://github.com/goodow/realtime-android/blob/master/src/test/java/com/goodow/realtime/java/WebSocketBusTest.java) 83 | and [ServerStoreTest](https://github.com/goodow/realtime-store/blob/master/src/test/java/com/goodow/realtime/store/impl/ServerStoreTest.java) 84 | for more usage. 85 | 86 | ### Local mode 87 | See https://github.com/goodow/realtime-android/blob/master/src/test/java/com/goodow/realtime/java/SimpleBusTest.java 88 | 89 | **NOTE:** You must register a platform first by invoke JavaPlatform.register() or AndroidPlatform.register() 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/goodow/realtime/android/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.demo; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.goodow.realtime.android.demo.test.TestBActivity; 7 | import com.goodow.realtime.android.demo.test.TestBViewModel; 8 | import com.goodow.realtime.android.mvp.Router; 9 | import com.goodow.realtime.channel.AsyncResult; 10 | import com.goodow.realtime.channel.Bus; 11 | import com.goodow.realtime.channel.Handler; 12 | import com.goodow.realtime.channel.Message; 13 | import com.goodow.realtime.channel.impl.SimpleBus; 14 | import com.goodow.realtime.channel.util.AsyncResultHandler; 15 | import com.goodow.realtime.channel.util.BusProvider; 16 | 17 | import java.util.Map; 18 | 19 | public class MainActivity extends AppCompatActivity { 20 | 21 | private Bus bus; 22 | 23 | @Override 24 | protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(com.goodow.realtime.android.demo.R.layout.activity_main); 27 | 28 | Router.init(getApplication()); 29 | BusProvider.enableRemoteBus(); 30 | bus = BusProvider.get(); 31 | 32 | bus.subscribe("views/#", new Handler() { 33 | @Override 34 | public void handle(Message message) { 35 | // TestBViewModel viewModel = new TestBViewModel(); 36 | // viewModel.id = 21; 37 | // viewModel.title = "test title"; 38 | // Router.getInstance().withData(viewModel).goToClass(TestBActivity.class); 39 | 40 | String url; 41 | if (message.topic().equals("views")) { 42 | boolean noData = message.payload() instanceof String; 43 | if (noData) { 44 | Router.getInstance().goToUrl((String) message.payload()); 45 | return; 46 | } 47 | url = (String) ((Map) message.payload()).get("url"); 48 | } else { 49 | url = "test://" + message.topic(); 50 | } 51 | Router.getInstance().withData(message.payload()).goToUrl(url); 52 | } 53 | }); 54 | 55 | bus.subscribe("#", new Handler() { 56 | @Override 57 | public void handle(Message message) { 58 | System.out.print(message.payload()); 59 | message.reply("Pong!", new AsyncResultHandler() { 60 | @Override 61 | public void handle(AsyncResult> asyncResult) { 62 | if (asyncResult.failed()) { 63 | return; 64 | } 65 | Message message = asyncResult.result(); 66 | System.out.println(message.payload()); 67 | } 68 | }); 69 | } 70 | }); 71 | 72 | bus.send("testTopic/abc", "Ping!", new AsyncResultHandler() { 73 | 74 | @Override 75 | public void handle(AsyncResult> asyncResult) { 76 | if (asyncResult.failed()) { 77 | return; 78 | } 79 | Message message = asyncResult.result(); 80 | System.out.println(message.payload()); 81 | message.reply("Ping 2"); 82 | } 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/firebase/FirebaseChannel.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.channel.firebase; 2 | 3 | import android.util.Log; 4 | 5 | import com.goodow.realtime.channel.AsyncResult; 6 | import com.goodow.realtime.channel.Bus; 7 | import com.goodow.realtime.channel.Handler; 8 | import com.goodow.realtime.channel.Message; 9 | import com.google.firebase.database.ChildEventListener; 10 | import com.google.firebase.database.DataSnapshot; 11 | import com.google.firebase.database.DatabaseError; 12 | import com.google.firebase.database.DatabaseReference; 13 | import com.google.firebase.database.FirebaseDatabase; 14 | import com.google.firebase.iid.FirebaseInstanceId; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | /** 20 | * Created by larry on 2017/11/4. 21 | */ 22 | 23 | public class FirebaseChannel { 24 | 25 | private final String instanceId; 26 | private final DatabaseReference busRef; 27 | private final DatabaseReference toRemoveRef; 28 | private final Bus bus; 29 | private final ChildEventListener childEventListener; 30 | 31 | public FirebaseChannel(final Bus bus) { 32 | this.bus = bus; 33 | instanceId = FirebaseInstanceId.getInstance().getId(); 34 | Log.d("FirebaseChannel", "FirebaseInstanceId: " + instanceId); 35 | 36 | busRef = FirebaseDatabase.getInstance().getReference("bus"); 37 | this.toRemoveRef = busRef.child("queue").child(instanceId); 38 | 39 | this.childEventListener = new ChildEventListener() { 40 | @Override 41 | public void onChildAdded(final DataSnapshot dataSnapshot, String s) { 42 | dataSnapshot.getRef().onDisconnect().removeValue(); 43 | Map msg = (Map) dataSnapshot.getValue(); 44 | String topic = (String) msg.get("topic"); 45 | Object payload = msg.get("payload"); 46 | bus.sendLocal(topic, payload, new Handler>>() { 47 | @Override 48 | public void handle(AsyncResult> asyncResult) { 49 | Message message = asyncResult.result(); 50 | 51 | Map reply = new HashMap<>(); 52 | reply.put("local", Boolean.valueOf(message.isLocal())); 53 | reply.put("replyTopic", message.replyTopic()); 54 | reply.put("payload", message.payload()); 55 | dataSnapshot.getRef().child("reply").setValue(reply); 56 | } 57 | }); 58 | } 59 | 60 | @Override 61 | public void onChildChanged(DataSnapshot dataSnapshot, String s) { 62 | 63 | } 64 | 65 | @Override 66 | public void onChildRemoved(DataSnapshot dataSnapshot) { 67 | 68 | } 69 | 70 | @Override 71 | public void onChildMoved(DataSnapshot dataSnapshot, String s) { 72 | 73 | } 74 | 75 | @Override 76 | public void onCancelled(DatabaseError databaseError) { 77 | 78 | } 79 | }; 80 | toRemoveRef.addChildEventListener(childEventListener); 81 | 82 | } 83 | 84 | @Override 85 | protected void finalize() throws Throwable { 86 | super.finalize(); 87 | 88 | toRemoveRef.removeEventListener(childEventListener); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/android/ChannelInitProvider.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.app.Application; 6 | import android.content.ContentProvider; 7 | import android.content.ContentValues; 8 | import android.content.Context; 9 | import android.content.pm.ProviderInfo; 10 | import android.database.Cursor; 11 | import android.net.Uri; 12 | import android.os.Build; 13 | import android.os.Bundle; 14 | import android.support.annotation.NonNull; 15 | import android.support.annotation.Nullable; 16 | 17 | import com.goodow.realtime.android.mvp.Router; 18 | import com.goodow.realtime.channel.util.BusProvider; 19 | 20 | import java.util.HashMap; 21 | 22 | /** 23 | * Created by larry on 2017/12/11. 24 | */ 25 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) 26 | public class ChannelInitProvider extends ContentProvider { 27 | private static long elapsedTime; // App start 28 | 29 | public static void init(Application application) { 30 | application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() { 31 | @Override 32 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 33 | 34 | } 35 | 36 | @Override 37 | public void onActivityStarted(Activity activity) { 38 | 39 | } 40 | 41 | @Override 42 | public void onActivityResumed(Activity activity) { 43 | if (elapsedTime > 0) { 44 | HashMap evt = new HashMap<>(); 45 | evt.put("time_consuming", (double) (System.currentTimeMillis() - elapsedTime) / 1000.); 46 | elapsedTime = -1; 47 | BusProvider.get().publishLocal("logReport/app_launch", evt); 48 | } 49 | } 50 | 51 | @Override 52 | public void onActivityPaused(Activity activity) { 53 | 54 | } 55 | 56 | @Override 57 | public void onActivityStopped(Activity activity) { 58 | 59 | } 60 | 61 | @Override 62 | public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 63 | 64 | } 65 | 66 | @Override 67 | public void onActivityDestroyed(Activity activity) { 68 | 69 | } 70 | }); 71 | Router.init(application); 72 | } 73 | 74 | public ChannelInitProvider() { 75 | elapsedTime = System.currentTimeMillis(); 76 | } 77 | 78 | @Override 79 | public void attachInfo(Context context, ProviderInfo info) { 80 | super.attachInfo(context, info); 81 | } 82 | 83 | @Override 84 | public boolean onCreate() { 85 | return false; 86 | } 87 | 88 | @Nullable 89 | @Override 90 | public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { 91 | return null; 92 | } 93 | 94 | @Nullable 95 | @Override 96 | public String getType(@NonNull Uri uri) { 97 | return null; 98 | } 99 | 100 | @Nullable 101 | @Override 102 | public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { 103 | return null; 104 | } 105 | 106 | @Override 107 | public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { 108 | return 0; 109 | } 110 | 111 | @Override 112 | public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) { 113 | return 0; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /realtime-channel/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | apply plugin: 'com.jfrog.bintray' 5 | 6 | android { 7 | compileSdkVersion 26 8 | buildToolsVersion "26.0.2" 9 | 10 | defaultConfig { 11 | minSdkVersion 15 12 | targetSdkVersion 26 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 17 | 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | compile fileTree(dir: 'libs', include: ['*.jar']) 29 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 30 | exclude group: 'com.android.support', module: 'support-annotations' 31 | }) 32 | 33 | // compile 'com.google.protobuf:protobuf-lite:3.0.1' 34 | // compile 'com.google.protobuf:protobuf-java-util:3.4.0' 35 | compile 'com.google.firebase:firebase-database:11.6.0' 36 | compile 'com.google.firebase:firebase-iid:11.6.0' 37 | // compile 'com.google.firebase:firebase-perf:11.6.0' 38 | 39 | testCompile 'junit:junit:4.12' 40 | } 41 | 42 | version = "0.8.0" // This is the library version used when deploying the artifact 43 | group = 'com.goodow.realtime' // 所在组 44 | def module_name = 'realtime-android' // 项目的名称 45 | def siteUrl = 'https://github.com/goodow/realtime-android' // 项目主页 46 | def gitUrl = 'https://github.com/goodow/realtime-android.git' // 项目的git地址 47 | 48 | install { 49 | repositories.mavenInstaller { 50 | // This generates POM.xml with proper parameters 51 | pom { 52 | project { 53 | packaging 'aar' 54 | name module_name // 名称 55 | url siteUrl 56 | licenses { 57 | license { 58 | name 'The Apache Software License, Version 2.0' // 开源协议名称 59 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' // 协议地址 60 | } 61 | } 62 | developers { 63 | developer { 64 | id 'larrytin' // 账号 65 | name 'Larry Tin' // 名称 66 | email 'dev@goodow.com' // 邮箱地址 67 | } 68 | } 69 | scm { 70 | connection gitUrl 71 | developerConnection gitUrl 72 | url siteUrl 73 | } 74 | } 75 | } 76 | } 77 | } 78 | task sourcesJar(type: Jar) { 79 | from android.sourceSets.main.java.srcDirs 80 | classifier = 'sources' 81 | } 82 | task javadoc(type: Javadoc) { 83 | source = android.sourceSets.main.java.srcDirs 84 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 85 | } 86 | task javadocJar(type: Jar, dependsOn: javadoc) { 87 | classifier = 'javadoc' 88 | from javadoc.destinationDir 89 | } 90 | artifacts { 91 | archives sourcesJar 92 | } 93 | Properties properties = new Properties() 94 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 95 | bintray { 96 | // 读取配置文件中的用户名和key 97 | user = properties.getProperty("bintray.user") 98 | key = properties.getProperty("bintray.apikey") 99 | configurations = ['archives'] 100 | pkg { 101 | userOrg = "goodow" 102 | repo = "maven" // 你在bintray上创建的库的名称 103 | name = module_name // 在jcenter中的项目名称 104 | websiteUrl = siteUrl 105 | vcsUrl = gitUrl 106 | licenses = ["Apache-2.0"] 107 | publish = true 108 | } 109 | } -------------------------------------------------------------------------------- /realtime-channel/protos/goodow_extras_option.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package goodow.protobuf; 3 | 4 | option objc_class_prefix = "GDDPB"; 5 | option java_package = "com.goodow.realtime.channel.protobuf"; 6 | option java_outer_classname = "ExtrasOptionProtos"; 7 | 8 | import "google/protobuf/field_mask.proto"; 9 | // import "goodow_bool.proto"; 10 | 11 | // 用于指定bus中options的extras 12 | 13 | message ExtrasOption { 14 | ViewOption view_opt = 1; 15 | CacheControl caching = 2; 16 | string reply_topic = 3; 17 | 18 | RequestOption request_opt = 100; 19 | message RequestOption { 20 | uint32 retry_times = 1; // 重试次数, 推荐优先使用 GDCOptions qos = GDCQosAtLeastOnce 来设置需要重试 21 | }; 22 | }; 23 | 24 | message ViewOption { 25 | LaunchMode launch_mode = 1; 26 | StackMode stack_mode = 2; // 仅初始化时有效 27 | 28 | bool status_bar = 6; 29 | bool nav_bar = 7; 30 | uint32 status_bar_style = 8; // UIStatusBarStyle 31 | uint32 nav_bar_style = 9; // UIBarStyle 32 | bool hides_bottom_bar_when_pushed = 10; // 仅初始化时有效 33 | bool tab_bar = 11; 34 | uint32 supported_interface_orientations = 12; // UIInterfaceOrientationMask 35 | bool autorotate = 13; // 是否应该自动旋转 36 | bool nav_bar_translucent = 14; 37 | 38 | bool needs_refresh = 21; // 是否需要刷新数据 39 | bool attempt_rotation_to_device_orientation = 22; 40 | uint32 device_orientation = 23; // 更改设备的朝向 UIDeviceOrientation 41 | bool tool_bar = 24; 42 | 43 | uint32 preferred_interface_orientation_for_presentation = 30; // UIInterfaceOrientation 44 | uint32 modal_presentation_style = 31; // 仅初始化时有效 UIModalPresentationStyle 45 | uint32 modal_transition_style = 32; // 仅初始化时有效 UIModalTransitionStyle 46 | uint32 edges_for_extended_layout = 33; // 仅初始化时有效 UIRectEdge (不支持设为值UIRectEdgeNone) 47 | bool animated = 34; // 是否需要动画, 默认为 YES 48 | }; 49 | 50 | // 是否创建新对象 51 | enum LaunchMode { 52 | LAUNCH_MODE_UNSET = 0; 53 | standard = 1; // 总是创建一个新的实例 54 | singleTop = 2; // 如果在堆栈顶部已有一个同类型的ViewController实例, 则复用该实例; 否则, 创建新实例 55 | 56 | singleTask = 3; 57 | singleInstance = 4; // 单例模式. 先寻找是否已存在该类型的实例, 若存在则回退历史栈直至可见, 不存在则新创建 58 | 59 | none = 5; // 不创建对象, 也不改变是否可见, 只转发消息 60 | } 61 | 62 | // 放入哪个历史回退栈里 63 | enum StackMode { 64 | STACK_MODE_UNSET = 0; 65 | push = 1; // push 到当前的 UINavigationController 66 | present = 2; // 使用 top presentViewController:controller 67 | presentPush = 3; // 使用 top presentViewController:[[UINavigationController alloc] initWithRootViewController:controller] 68 | root = 4; // 替换当前 window 的 rootViewController 69 | }; 70 | 71 | // https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn 72 | message CacheControl { 73 | Status status = 1; 74 | double max_age = 2; // 缓存过期时间, 单位为s 75 | string etag = 3; // 用于校验缓存是否有更新 76 | uint64 last_modified = 4; 77 | google.protobuf.FieldMask key_fields = 5; // 缓存的field paths 参见: https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask 78 | RequestCachePolicy request_policy = 6; 79 | 80 | enum Status { 81 | STATUS_UNSET = 0; 82 | Private = 1; // 只为单个用户缓存 83 | Public = 2; // 即使有关联的用户认证, 甚至响应状态码无法正常缓存, 响应也可以被缓存. 大多数情况下, public不是必须的, 因为明确的缓存信息(例如max_age)已表示响应可以被缓存. 84 | no_cache = 3; // 必须先与服务器确认是否有更新 85 | no_store = 4; // 禁止缓存 86 | unmodified = 5; // 服务器没有更新; 同时复用这个字段表示数据来自缓存 87 | } 88 | 89 | // 含义和 NSURLRequestCachePolicy 保持一致 90 | enum RequestCachePolicy { 91 | // reserved 4, 5; 92 | // reserved "ReloadIgnoringLocalAndRemoteCacheData", "ReloadRevalidatingCacheData"; 93 | 94 | USE_PROTOCOL_CACHE_POLICY = 0; // 按 HTTP 缓存规范实现 95 | RELOAD_IGNORING_LOCAL_CACHE_DATA = 1; // 只发起网络请求, 不使用缓存 96 | RETURN_CACHE_DATA_ELSE_LOAD = 2; // 先读缓存, 不管是否过期, 有即返回缓存; 若无缓存则发起网络请求 97 | RETURN_CACHE_DATA_DONT_LOAD = 3; // 只读缓存, 不管是否过期, 有即返回缓存; 若无缓存则失败 98 | 99 | RELOAD_ELSE_RETURN_CACHE_DATA = 100; // 自定义策略: 先请求网络,若网络请求失败则返回缓存数据, 不管缓存是否过期 100 | }; 101 | }; 102 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/Bus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel; 15 | 16 | /** 17 | * A distributed lightweight event bus which can encompass multiple machines. The event bus 18 | * implements publish/subscribe, point to point messaging and request-response messaging.

19 | * Messages sent over the event bus are represented by instances of the {@link Message} class.

20 | * For publish/subscribe, messages can be published to a topic using one of the {@link #publish} 21 | * methods. A topic is a simple {@code String} instance.

22 | * Handlers are registered against a topic. There can be multiple handlers registered against 23 | * each topic, and a particular handler can be registered against multiple topics. The event 24 | * bus will route a sent message to all handlers which are registered against that topic.

25 | * For point to point messaging, messages can be sent to a topic using one of the {@link #send} 26 | * methods. The messages will be delivered to a single handler, if one is registered on that 27 | * topic. If more than one handler is registered on the same topic, the bus will choose one and 28 | * deliver the message to that. The bus will aim to fairly distribute messages in a round-robin way, 29 | * but does not guarantee strict round-robin under all circumstances.

30 | * The order of messages received by any specific handler from a specific sender should match the 31 | * order of messages sent from that sender.

32 | * When sending a message, a reply handler can be provided. If so, it will be called when the reply 33 | * from the receiver has been received. Reply messages can also be replied to, etc, ad infinitum

34 | * Different event bus instances can be clustered together over a network, to give a single logical 35 | * event bus.

36 | */ 37 | public interface Bus { 38 | 39 | /** 40 | * Publish a message 41 | * 42 | * @param topic The topic to publish it to 43 | * @param msg The message 44 | */ 45 | Bus publish(String topic, Object msg); 46 | 47 | /** 48 | * Publish a local message 49 | * 50 | * @param topic The topic to publish it to 51 | * @param msg The message 52 | */ 53 | Bus publishLocal(String topic, Object msg); 54 | 55 | /** 56 | * Send a message 57 | * 58 | * @param topic The topic to send it to 59 | * @param msg The message 60 | * @param replyHandler Reply handler will be called when any reply from the recipient is received 61 | */ 62 | Bus send(String topic, Object msg, Handler>> replyHandler); 63 | 64 | /** 65 | * Send a local message 66 | * 67 | * @param topic The topic to send it to 68 | * @param msg The message 69 | * @param replyHandler Reply handler will be called when any reply from the recipient is received 70 | */ 71 | Bus sendLocal(String topic, Object msg, Handler>> replyHandler); 72 | 73 | /** 74 | * Registers a handler against the specified topic 75 | * 76 | * @param topicFilter The topicFilter to register it at 77 | * @param handler The handler 78 | * @return the handler registration, can be stored in order to unregister the handler later 79 | */ 80 | @SuppressWarnings("rawtypes") 81 | Registration subscribe(String topicFilter, Handler handler); 82 | 83 | /** 84 | * Registers a local handler against the specified topic. The handler info won't be propagated 85 | * across the cluster 86 | * 87 | * @param topicFilter The topicFilter to register it at 88 | * @param handler The handler 89 | */ 90 | @SuppressWarnings("rawtypes") 91 | Registration subscribeLocal(String topicFilter, Handler handler); 92 | } -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/android/mvp/util/JsonMapper.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.mvp.util; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | import org.json.JSONStringer; 7 | import org.json.JSONTokener; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.Collection; 12 | import java.util.HashMap; 13 | import java.util.Iterator; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | /** 18 | * Created by larry on 2017/11/9. 19 | * 20 | * Helper class to convert from/to JSON strings. 21 | */ 22 | public class JsonMapper { 23 | 24 | public static String serializeJson(Map object) throws IOException { 25 | return serializeJsonValue(object); 26 | } 27 | 28 | @SuppressWarnings("unchecked") 29 | public static String serializeJsonValue(Object object) throws IOException { 30 | if (object == null) { 31 | return "null"; 32 | } else if (object instanceof String) { 33 | return JSONObject.quote((String) object); 34 | } else if (object instanceof Number) { 35 | try { 36 | return JSONObject.numberToString((Number) object); 37 | } catch (JSONException e) { 38 | throw new IOException("Could not serialize number", e); 39 | } 40 | } else if (object instanceof Boolean) { 41 | return ((Boolean) object) ? "true" : "false"; 42 | } else { 43 | try { 44 | JSONStringer stringer = new JSONStringer(); 45 | serializeJsonValue(object, stringer); 46 | return stringer.toString(); 47 | } catch (JSONException e) { 48 | throw new IOException("Failed to serialize JSON", e); 49 | } 50 | } 51 | } 52 | 53 | private static void serializeJsonValue(Object object, JSONStringer stringer) 54 | throws IOException, JSONException { 55 | if (object instanceof Map) { 56 | stringer.object(); 57 | @SuppressWarnings("unchecked") 58 | Map map = (Map) object; 59 | for (Map.Entry entry : map.entrySet()) { 60 | stringer.key(entry.getKey()); 61 | serializeJsonValue(entry.getValue(), stringer); 62 | } 63 | stringer.endObject(); 64 | } else if (object instanceof Collection) { 65 | Collection collection = (Collection) object; 66 | stringer.array(); 67 | for (Object entry : collection) { 68 | serializeJsonValue(entry, stringer); 69 | } 70 | stringer.endArray(); 71 | } else { 72 | stringer.value(object); 73 | } 74 | } 75 | 76 | public static Map parseJson(String json) throws IOException { 77 | try { 78 | return unwrapJsonObject(new JSONObject(json)); 79 | } catch (JSONException e) { 80 | throw new IOException(e); 81 | } 82 | } 83 | 84 | public static Object parseJsonValue(String json) throws IOException { 85 | try { 86 | return unwrapJson(new JSONTokener(json).nextValue()); 87 | } catch (JSONException e) { 88 | throw new IOException(e); 89 | } 90 | } 91 | 92 | @SuppressWarnings("unchecked") 93 | private static Map unwrapJsonObject(JSONObject jsonObject) throws JSONException { 94 | Map map = new HashMap<>(jsonObject.length()); 95 | Iterator keys = jsonObject.keys(); 96 | while (keys.hasNext()) { 97 | String key = keys.next(); 98 | map.put(key, unwrapJson(jsonObject.get(key))); 99 | } 100 | return map; 101 | } 102 | 103 | private static List unwrapJsonArray(JSONArray jsonArray) throws JSONException { 104 | List list = new ArrayList<>(jsonArray.length()); 105 | for (int i = 0; i < jsonArray.length(); i++) { 106 | list.add(unwrapJson(jsonArray.get(i))); 107 | } 108 | return list; 109 | } 110 | 111 | public static Object unwrapJson(Object o) throws JSONException { 112 | if (o instanceof JSONObject) { 113 | return unwrapJsonObject((JSONObject) o); 114 | } else if (o instanceof JSONArray) { 115 | return unwrapJsonArray((JSONArray) o); 116 | } else if (o.equals(JSONObject.NULL)) { 117 | return null; 118 | } else { 119 | return o; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/android/mvp/RouteBuilder.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.mvp; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | import android.support.v4.app.ActivityCompat; 10 | 11 | import com.goodow.realtime.android.mvp.util.CustomClassMapper; 12 | 13 | import java.util.Collections; 14 | import java.util.LinkedHashMap; 15 | import java.util.Map; 16 | import java.util.UUID; 17 | 18 | /** 19 | * Created by larry on 2017/11/7. 20 | */ 21 | 22 | public class RouteBuilder { 23 | static final String ACTIVITY_ID = RouteBuilder.class.getName(); 24 | private Object data; 25 | private int flags = -1; // Flags of route 26 | 27 | /** 28 | * Split query parameters 29 | * 30 | * @param rawUri raw uri 31 | * @return map with params 32 | */ 33 | private static Map splitQueryParameters(Uri rawUri) { 34 | String query = rawUri.getEncodedQuery(); 35 | 36 | if (query == null) { 37 | return Collections.emptyMap(); 38 | } 39 | 40 | Map paramMap = new LinkedHashMap<>(); 41 | int start = 0; 42 | do { 43 | int next = query.indexOf('&', start); 44 | int end = (next == -1) ? query.length() : next; 45 | 46 | int separator = query.indexOf('=', start); 47 | if (separator > end || separator == -1) { 48 | separator = end; 49 | } 50 | 51 | String name = query.substring(start, separator); 52 | 53 | if (!android.text.TextUtils.isEmpty(name)) { 54 | String value = (separator == end ? "" : query.substring(separator + 1, end)); 55 | paramMap.put(Uri.decode(name), Uri.decode(value)); 56 | } 57 | 58 | // Move start to end of name. 59 | start = end + 1; 60 | } while (start < query.length()); 61 | 62 | return Collections.unmodifiableMap(paramMap); 63 | } 64 | 65 | 66 | public RouteBuilder(Object data) { 67 | this.data = data; 68 | } 69 | 70 | public void goToClass(Class activity) { 71 | // Build intent 72 | final Context context = Router.getInstance().context; 73 | final Intent intent = new Intent(context, activity); 74 | 75 | // Navigation in main looper. 76 | new Handler(Looper.getMainLooper()).post(new Runnable() { 77 | @Override 78 | public void run() { 79 | if (data != null) { 80 | String activityId = UUID.randomUUID().toString(); 81 | intent.putExtra(ACTIVITY_ID, activityId); 82 | Router.intentCache.put(activityId, data); 83 | } 84 | intent.setFlags(flags == -1 ? Intent.FLAG_ACTIVITY_NEW_TASK : flags); 85 | ActivityCompat.startActivity(context, intent, null); 86 | } 87 | }); 88 | } 89 | 90 | public void goToUrl(String url) { 91 | Uri uri = Uri.parse(url); 92 | if (null == uri || uri.toString().length() == 0) { 93 | throw new RuntimeException("url Parameter invalid!"); 94 | } 95 | Map parameters = this.splitQueryParameters(uri); 96 | if (parameters.size() > 0) { 97 | if (this.data == null) { 98 | this.data = parameters; 99 | } else if (this.data instanceof Map) { 100 | ((Map) this.data).putAll(parameters); 101 | } 102 | } 103 | if (this.data instanceof Map) { 104 | parameters = (Map) this.data; 105 | } 106 | 107 | if (parameters.containsKey("viewOpt.flags")) { 108 | this.flags = Integer.parseInt(parameters.get("viewOpt.flags")); 109 | } 110 | 111 | String className = uri.getLastPathSegment(); 112 | if (!className.endsWith(Activity.class.getSimpleName())) { 113 | className = className + Activity.class.getSimpleName(); 114 | } 115 | String rootPackageName = Router.getInstance().context.getPackageName(); 116 | String packageName; 117 | if (parameters.containsKey("viewOpt.package")) { 118 | packageName = parameters.get("viewOpt.package"); 119 | if (packageName.startsWith(".")) { 120 | packageName = rootPackageName + packageName; 121 | } 122 | } else if (!className.startsWith(rootPackageName)) { 123 | packageName = rootPackageName; 124 | } else { 125 | packageName = ""; 126 | } 127 | className = packageName + (className.startsWith(".") ? "" : ".") + className; 128 | Class activityClz; 129 | try { 130 | activityClz = Class.forName(className); 131 | } catch (ClassNotFoundException e) { 132 | throw new RuntimeException(e); 133 | } 134 | if (!Activity.class.isAssignableFrom(activityClz)) { 135 | throw new RuntimeException("className Parameter invalid!"); 136 | } 137 | 138 | if (this.data instanceof Map) { 139 | String viewModelName = className.substring(0, className.length() - Activity.class.getSimpleName().length()) + "ViewModel"; 140 | try { 141 | Class viewModelClz = Class.forName(viewModelName); 142 | Object viewModel = CustomClassMapper.convertToCustomClass(this.data, viewModelClz); 143 | this.data = viewModel; 144 | } catch (ClassNotFoundException e) { 145 | } 146 | } 147 | 148 | this.goToClass((Class) activityClz); 149 | } 150 | 151 | /** 152 | * Set special flags controlling how this intent is handled. Most values 153 | * here depend on the type of component being executed by the Intent, 154 | * specifically the FLAG_ACTIVITY_* flags are all for use with 155 | * {@link Context#startActivity Context.startActivity()}. 156 | */ 157 | public RouteBuilder withFlags(int flag) { 158 | this.flags = flag; 159 | return this; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/mqtt/Topic.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.channel.mqtt; 2 | 3 | /** 4 | * Created by larry on 2017/10/30. 5 | */ 6 | 7 | import java.io.Serializable; 8 | import java.text.ParseException; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | 15 | public class Topic implements Serializable { 16 | 17 | private static final Logger LOG = Logger.getLogger(Topic.class.getName()); 18 | 19 | private final String topic; 20 | 21 | private transient List tokens; 22 | 23 | private transient boolean valid; 24 | 25 | /** 26 | * Factory method 27 | */ 28 | public static Topic asTopic(String s) { 29 | return new Topic(s); 30 | } 31 | 32 | public Topic(String topic) { 33 | this.topic = topic; 34 | } 35 | 36 | Topic(List tokens) { 37 | this.tokens = tokens; 38 | boolean isFirst = true; 39 | StringBuilder topic = new StringBuilder(""); 40 | for (Token token : tokens) { 41 | if (isFirst) { 42 | isFirst = false; 43 | } else { 44 | topic.append("/"); 45 | } 46 | topic.append(token.toString()); 47 | } 48 | this.topic = topic.toString(); 49 | this.valid = true; 50 | } 51 | 52 | public List getTokens() { 53 | if (tokens == null) { 54 | try { 55 | tokens = parseTopic(topic); 56 | valid = true; 57 | } catch (ParseException e) { 58 | valid = false; 59 | String[] params = {topic, e.getMessage()}; 60 | LOG.log(Level.SEVERE, "Error parsing the topic: {}, message: {}", params); 61 | } 62 | } 63 | 64 | return tokens; 65 | } 66 | 67 | private List parseTopic(String topic) throws ParseException { 68 | List res = new ArrayList<>(); 69 | String[] splitted = topic.split("/"); 70 | 71 | if (splitted.length == 0) { 72 | res.add(Token.EMPTY); 73 | } 74 | 75 | if (topic.endsWith("/")) { 76 | // Add a fictious space 77 | String[] newSplitted = new String[splitted.length + 1]; 78 | System.arraycopy(splitted, 0, newSplitted, 0, splitted.length); 79 | newSplitted[splitted.length] = ""; 80 | splitted = newSplitted; 81 | } 82 | 83 | for (int i = 0; i < splitted.length; i++) { 84 | String s = splitted[i]; 85 | if (s.isEmpty()) { 86 | // if (i != 0) { 87 | // throw new ParseException("Bad format of topic, expetec topic name between 88 | // separators", i); 89 | // } 90 | res.add(Token.EMPTY); 91 | } else if (s.equals("#")) { 92 | // check that multi is the last symbol 93 | if (i != splitted.length - 1) { 94 | throw new ParseException( 95 | "Bad format of topic, the multi symbol (#) has to be the last one after a separator", 96 | i); 97 | } 98 | res.add(Token.MULTI); 99 | } else if (s.contains("#")) { 100 | throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i); 101 | } else if (s.equals("+")) { 102 | res.add(Token.SINGLE); 103 | } else if (s.contains("+")) { 104 | throw new ParseException("Bad format of topic, invalid subtopic name: " + s, i); 105 | } else { 106 | res.add(new Token(s)); 107 | } 108 | } 109 | 110 | return res; 111 | } 112 | 113 | public Token headToken() { 114 | final List tokens = getTokens(); 115 | if (tokens.isEmpty()) { 116 | //TODO UGLY use Optional 117 | return null; 118 | } 119 | return tokens.get(0); 120 | } 121 | 122 | public boolean isEmpty() { 123 | final List tokens = getTokens(); 124 | return tokens == null || tokens.isEmpty(); 125 | } 126 | 127 | /** 128 | * @return a new Topic corresponding to this less than the head token 129 | */ 130 | public Topic exceptHeadToken() { 131 | List tokens = getTokens(); 132 | if (tokens.isEmpty()) { 133 | return new Topic(Collections.emptyList()); 134 | } 135 | List tokensCopy = new ArrayList<>(tokens); 136 | tokensCopy.remove(0); 137 | return new Topic(tokensCopy); 138 | } 139 | 140 | public boolean isValid() { 141 | if (tokens == null) 142 | getTokens(); 143 | 144 | return valid; 145 | } 146 | 147 | /** 148 | * Verify if the 2 topics matching respecting the rules of MQTT Appendix A 149 | * 150 | * @param subscriptionTopic the topic filter of the subscription 151 | * @return true if the two topics match. 152 | */ 153 | // TODO reimplement with iterators or with queues 154 | public boolean match(Topic subscriptionTopic) { 155 | List msgTokens = getTokens(); 156 | List subscriptionTokens = subscriptionTopic.getTokens(); 157 | int i = 0; 158 | for (; i < subscriptionTokens.size(); i++) { 159 | Token subToken = subscriptionTokens.get(i); 160 | if (subToken != Token.MULTI && subToken != Token.SINGLE) { 161 | if (i >= msgTokens.size()) { 162 | return false; 163 | } 164 | Token msgToken = msgTokens.get(i); 165 | if (!msgToken.equals(subToken)) { 166 | return false; 167 | } 168 | } else { 169 | if (subToken == Token.MULTI) { 170 | return true; 171 | } 172 | if (subToken == Token.SINGLE) { 173 | // skip a step forward 174 | } 175 | } 176 | } 177 | // if last token was a SINGLE then treat it as an empty 178 | // if (subToken == Token.SINGLE && (i - msgTokens.size() == 1)) { 179 | // i--; 180 | // } 181 | return i == msgTokens.size(); 182 | } 183 | 184 | @Override 185 | public String toString() { 186 | return topic; 187 | } 188 | 189 | @Override 190 | public boolean equals(Object obj) { 191 | if (obj == null) { 192 | return false; 193 | } 194 | if (getClass() != obj.getClass()) { 195 | return false; 196 | } 197 | Topic other = (Topic) obj; 198 | 199 | return this.topic.equals(other.topic); 200 | } 201 | 202 | @Override 203 | public int hashCode() { 204 | return topic.hashCode(); 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/channel/impl/SimpleBus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Goodow.com 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.goodow.realtime.channel.impl; 15 | 16 | import android.os.Looper; 17 | 18 | import com.goodow.realtime.channel.AsyncResult; 19 | import com.goodow.realtime.channel.Bus; 20 | import com.goodow.realtime.channel.Handler; 21 | import com.goodow.realtime.channel.Message; 22 | import com.goodow.realtime.channel.Registration; 23 | import com.goodow.realtime.channel.mqtt.Topic; 24 | import com.goodow.realtime.channel.util.IdGenerator; 25 | 26 | import java.lang.reflect.ParameterizedType; 27 | import java.lang.reflect.Type; 28 | import java.util.HashMap; 29 | import java.util.LinkedHashMap; 30 | import java.util.LinkedHashSet; 31 | import java.util.Map; 32 | import java.util.logging.Level; 33 | import java.util.logging.Logger; 34 | 35 | public class SimpleBus implements Bus { 36 | private static final Logger log = Logger.getLogger(SimpleBus.class.getName()); 37 | 38 | static void checkNotNull(String paramName, Object param) { 39 | if (param == null) { 40 | throw new IllegalArgumentException("Parameter " + paramName + " must be specified"); 41 | } 42 | } 43 | 44 | private LinkedHashMap>> handlerMap; 45 | final LinkedHashMap>> replyHandlers; 46 | final IdGenerator idGenerator; 47 | private final android.os.Handler handler; 48 | 49 | public SimpleBus() { 50 | handlerMap = new LinkedHashMap(); 51 | replyHandlers = new LinkedHashMap(); 52 | idGenerator = new IdGenerator(); 53 | handler = new android.os.Handler(Looper.getMainLooper()); 54 | } 55 | 56 | @Override 57 | public Bus publish(String topic, Object msg) { 58 | doSendOrPub(false, false, topic, msg, null); 59 | return this; 60 | } 61 | 62 | @Override 63 | public Bus publishLocal(String topic, Object msg) { 64 | doSendOrPub(true, false, topic, msg, null); 65 | return this; 66 | } 67 | 68 | @Override 69 | public Registration subscribe(final String topicFilter, 70 | final Handler handler) { 71 | return subscribeImpl(false, topicFilter, handler); 72 | } 73 | 74 | @Override 75 | public Registration subscribeLocal(final String topicFilter, 76 | final Handler handler) { 77 | return subscribeImpl(true, topicFilter, handler); 78 | } 79 | 80 | @Override 81 | public Bus send(String topic, Object msg, Handler>> replyHandler) { 82 | doSendOrPub(false, true, topic, msg, replyHandler); 83 | return this; 84 | } 85 | 86 | @Override 87 | public Bus sendLocal(String topic, Object msg, Handler>> replyHandler) { 88 | doSendOrPub(true, true, topic, msg, replyHandler); 89 | return this; 90 | } 91 | 92 | protected boolean doSubscribe(boolean local, String topic, 93 | Handler handler) { 94 | checkNotNull("topic", topic); 95 | checkNotNull("handler", handler); 96 | LinkedHashSet handlers = handlerMap.get(topic); 97 | if (handlers == null) { 98 | handlers = new LinkedHashSet<>(); 99 | handlers.add(handler); 100 | handlerMap.put(topic, handlers); 101 | return true; 102 | } 103 | if (!handlers.contains(handler)) { 104 | handlers.add(handler); 105 | return true; 106 | } 107 | return false; 108 | } 109 | 110 | @SuppressWarnings("unchecked") 111 | protected void doSendOrPub(boolean local, boolean send, String topic, Object msg, 112 | final Handler>> replyHandler) { 113 | checkNotNull("topic", topic); 114 | String replyTopic = null; 115 | if (replyHandler != null) { 116 | replyTopic = makeUUID(); 117 | replyHandlers.put(replyTopic, (Handler) replyHandler); 118 | } 119 | MessageImpl message = new MessageImpl(local, send, this, topic, replyTopic, msg); 120 | doReceiveMessage(message); 121 | } 122 | 123 | protected boolean doUnsubscribe(boolean local, String topic, 124 | Handler handler) { 125 | checkNotNull("topic", topic); 126 | checkNotNull("handler", handler); 127 | LinkedHashSet handlers = handlerMap.get(topic); 128 | if (handlers == null) { 129 | return false; 130 | } 131 | boolean removed = handlers.remove(handler); 132 | if (handlers.isEmpty()) { 133 | handlerMap.remove(topic); 134 | } 135 | return removed; 136 | } 137 | 138 | String makeUUID() { 139 | return idGenerator.next(36); 140 | } 141 | 142 | private void doReceiveMessage(final Message message) { 143 | final String topic = message.topic(); 144 | // Might be a reply message 145 | final Handler> replyHandler = replyHandlers.get(topic); 146 | if (replyHandler != null) { 147 | replyHandlers.remove(topic); 148 | 149 | Message copiedMessage = message; 150 | if (message.payload() != null) { 151 | try { 152 | Type messageType = replyHandler.getClass().getMethod("handle", AsyncResult.class).getGenericParameterTypes()[0]; 153 | if (messageType instanceof ParameterizedType) { 154 | Type payloadType = ((ParameterizedType) messageType).getActualTypeArguments()[0]; 155 | if (payloadType instanceof ParameterizedType) { 156 | payloadType = ((ParameterizedType) payloadType).getActualTypeArguments()[0]; 157 | if (payloadType instanceof Class && !((Class) payloadType).isAssignableFrom(message.payload().getClass())) { 158 | copiedMessage = ((MessageImpl) message).clone(); 159 | ((MessageImpl) copiedMessage).payload = null; 160 | } 161 | } 162 | } 163 | } catch (NoSuchMethodException e) { 164 | } 165 | } 166 | 167 | scheduleHandle(topic, new Handler() { 168 | @Override 169 | public void handle(Message message) { 170 | replyHandler.handle(new AsyncResultImpl(message)); 171 | } 172 | }, copiedMessage); 173 | return; 174 | } 175 | 176 | Topic topic1 = new Topic(topic); 177 | for (String topicFilter : handlerMap.keySet()) { 178 | if (!topic1.match(new Topic(topicFilter))) { 179 | continue; 180 | } 181 | LinkedHashSet handlers = handlerMap.get(topicFilter); 182 | if (handlers != null) { 183 | LinkedHashSet> copiedHandlers = (LinkedHashSet) handlers.clone(); 184 | for (Handler handler : copiedHandlers) { 185 | Message copiedMessage = message; 186 | if (message.payload() != null) { 187 | try { 188 | Type messageType = handler.getClass().getMethod("handle", Message.class).getGenericParameterTypes()[0]; 189 | if (messageType instanceof ParameterizedType) { 190 | Type payloadType = ((ParameterizedType) messageType).getActualTypeArguments()[0]; 191 | if (payloadType instanceof Class && !((Class) payloadType).isAssignableFrom(message.payload().getClass())) { 192 | copiedMessage = ((MessageImpl) message).clone(); 193 | ((MessageImpl) copiedMessage).payload = null; 194 | } 195 | } 196 | } catch (NoSuchMethodException e) { 197 | } 198 | } 199 | scheduleHandle(topicFilter, handler, copiedMessage); 200 | } 201 | } 202 | } 203 | } 204 | 205 | 206 | private void scheduleHandle(final String topic, final Handler handler, final Message message) { 207 | try { 208 | this.handler.post(new Runnable() { 209 | @Override 210 | public void run() { 211 | handler.handle(message); 212 | } 213 | }); 214 | } catch (Throwable e) { 215 | log.log(Level.WARNING, "Failed to handle on topic: " + topic, e); 216 | Map msg = new HashMap<>(); 217 | msg.put("topic", topic); 218 | msg.put("message", message); 219 | msg.put("cause", e); 220 | // publishLocal(ON_ERROR, msg); 221 | } 222 | } 223 | 224 | private Registration subscribeImpl(final boolean local, final String topic, 225 | final Handler handler) { 226 | doSubscribe(local, topic, handler); 227 | return new Registration() { 228 | @Override 229 | public void unregister() { 230 | doUnsubscribe(local, topic, handler); 231 | } 232 | }; 233 | } 234 | } -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/android/mvp/util/BeanUtils.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.mvp.util; 2 | 3 | /** 4 | * Created by larry on 2017/11/9. 5 | */ 6 | 7 | import com.google.firebase.database.DatabaseException; 8 | import com.google.firebase.database.Exclude; 9 | import com.google.firebase.database.IgnoreExtraProperties; 10 | import com.google.firebase.database.PropertyName; 11 | import com.google.firebase.database.ThrowOnExtraProperties; 12 | 13 | import java.lang.reflect.AccessibleObject; 14 | import java.lang.reflect.Constructor; 15 | import java.lang.reflect.Field; 16 | import java.lang.reflect.GenericArrayType; 17 | import java.lang.reflect.InvocationTargetException; 18 | import java.lang.reflect.Method; 19 | import java.lang.reflect.Modifier; 20 | import java.lang.reflect.ParameterizedType; 21 | import java.lang.reflect.Type; 22 | import java.lang.reflect.TypeVariable; 23 | import java.lang.reflect.WildcardType; 24 | import java.util.ArrayList; 25 | import java.util.Collection; 26 | import java.util.Collections; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | import java.util.concurrent.ConcurrentHashMap; 31 | import java.util.concurrent.ConcurrentMap; 32 | import java.util.logging.Level; 33 | import java.util.logging.Logger; 34 | 35 | import static com.goodow.realtime.android.mvp.util.CustomClassMapper.hardAssert; 36 | 37 | /** Helper class to convert to/from custom POJO classes and plain Java types. */ 38 | public class BeanUtils { 39 | 40 | private static final Logger logger = Logger.getLogger(BeanUtils.class.getName()); 41 | 42 | private static final ConcurrentMap, BeanMapper> mappers = new ConcurrentHashMap<>(); 43 | 44 | /** 45 | * Populate the JavaBeans properties of the specified bean, based on the specified name/value pairs. 46 | * 47 | * @param bean JavaBean whose properties are being populated 48 | * @param properties Map keyed by property name, with the corresponding (String or String[]) value(s) to be set 49 | * 50 | */ 51 | public static void populate(Object bean, Map properties) { 52 | deserializeToClass(bean, properties); 53 | } 54 | 55 | @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) 56 | private static T deserializeToType(Object obj, Type type) { 57 | if (obj == null) { 58 | return null; 59 | } else if (type instanceof ParameterizedType) { 60 | return deserializeToParameterizedType(obj, (ParameterizedType) type); 61 | } else if (type instanceof Class) { 62 | return deserializeToClass(obj, (Class) type); 63 | } else if (type instanceof WildcardType) { 64 | throw new DatabaseException("Generic wildcard types are not supported"); 65 | } else if (type instanceof GenericArrayType) { 66 | throw new DatabaseException( 67 | "Generic Arrays are not supported, please use Lists " + "instead"); 68 | } else { 69 | throw new IllegalStateException("Unknown type encountered: " + type); 70 | } 71 | } 72 | 73 | @SuppressWarnings("unchecked") 74 | private static void deserializeToClass(Object bean, Map properties) { 75 | if (properties == null) { 76 | return; 77 | } 78 | convertBean(bean, properties); 79 | } 80 | 81 | @SuppressWarnings("unchecked") 82 | private static T deserializeToClass(Object obj, Class clazz) { 83 | if (obj == null) { 84 | return null; 85 | } else if (clazz.isPrimitive() 86 | || Number.class.isAssignableFrom(clazz) 87 | || Boolean.class.isAssignableFrom(clazz) 88 | || Character.class.isAssignableFrom(clazz)) { 89 | return deserializeToPrimitive(obj, clazz); 90 | } else if (String.class.isAssignableFrom(clazz)) { 91 | return (T) convertString(obj); 92 | } else if (clazz.isArray()) { 93 | throw new DatabaseException( 94 | "Converting to Arrays is not supported, please use Lists" + "instead"); 95 | } else if (clazz.getTypeParameters().length > 0) { 96 | throw new DatabaseException( 97 | "Class " 98 | + clazz.getName() 99 | + " has generic type " 100 | + "parameters, please use GenericTypeIndicator instead"); 101 | } else if (clazz.equals(Object.class)) { 102 | return (T) obj; 103 | } else if (clazz.isEnum()) { 104 | return deserializeToEnum(obj, clazz); 105 | } else { 106 | return convertBean(obj, clazz); 107 | } 108 | } 109 | 110 | @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) 111 | private static T deserializeToParameterizedType(Object obj, ParameterizedType type) { 112 | // getRawType should always return a Class 113 | Class rawType = (Class) type.getRawType(); 114 | if (List.class.isAssignableFrom(rawType)) { 115 | Type genericType = type.getActualTypeArguments()[0]; 116 | if (obj instanceof List) { 117 | List list = (List) obj; 118 | List result = new ArrayList<>(list.size()); 119 | for (Object object : list) { 120 | result.add(deserializeToType(object, genericType)); 121 | } 122 | return (T) result; 123 | } else { 124 | throw new DatabaseException( 125 | "Expected a List while deserializing, but got a " + obj.getClass()); 126 | } 127 | } else if (Map.class.isAssignableFrom(rawType)) { 128 | Type keyType = type.getActualTypeArguments()[0]; 129 | Type valueType = type.getActualTypeArguments()[1]; 130 | if (!keyType.equals(String.class)) { 131 | throw new DatabaseException( 132 | "Only Maps with string keys are supported, " 133 | + "but found Map with key type " 134 | + keyType); 135 | } 136 | Map map = expectMap(obj); 137 | HashMap result = new HashMap<>(); 138 | for (Map.Entry entry : map.entrySet()) { 139 | result.put(entry.getKey(), deserializeToType(entry.getValue(), valueType)); 140 | } 141 | return (T) result; 142 | } else if (Collection.class.isAssignableFrom(rawType)) { 143 | throw new DatabaseException("Collections are not supported, please use Lists instead"); 144 | } else { 145 | Map map = expectMap(obj); 146 | BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType); 147 | HashMap>, Type> typeMapping = new HashMap<>(); 148 | TypeVariable>[] typeVariables = mapper.clazz.getTypeParameters(); 149 | Type[] types = type.getActualTypeArguments(); 150 | if (types.length != typeVariables.length) { 151 | throw new IllegalStateException("Mismatched lengths for type variables and actual types"); 152 | } 153 | for (int i = 0; i < typeVariables.length; i++) { 154 | typeMapping.put(typeVariables[i], types[i]); 155 | } 156 | return mapper.deserialize(map, typeMapping); 157 | } 158 | } 159 | 160 | @SuppressWarnings("unchecked") 161 | private static T deserializeToPrimitive(Object obj, Class clazz) { 162 | if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) { 163 | return (T) convertInteger(obj); 164 | } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) { 165 | return (T) convertBoolean(obj); 166 | } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) { 167 | return (T) convertDouble(obj); 168 | } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) { 169 | return (T) convertLong(obj); 170 | } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { 171 | return (T) (Float) convertDouble(obj).floatValue(); 172 | } else if (Short.class.isAssignableFrom(clazz) || short.class.isAssignableFrom(clazz)) { 173 | throw new DatabaseException("Deserializing to shorts is not supported"); 174 | } else if (Byte.class.isAssignableFrom(clazz) || byte.class.isAssignableFrom(clazz)) { 175 | throw new DatabaseException("Deserializing to bytes is not supported"); 176 | } else if (Character.class.isAssignableFrom(clazz) || char.class.isAssignableFrom(clazz)) { 177 | throw new DatabaseException("Deserializing to char is not supported"); 178 | } else { 179 | throw new IllegalArgumentException("Unknown primitive type: " + clazz); 180 | } 181 | } 182 | 183 | @SuppressWarnings("unchecked") 184 | private static T deserializeToEnum(Object object, Class clazz) { 185 | if (object instanceof String) { 186 | String value = (String) object; 187 | // We cast to Class without generics here since we can't prove the bound 188 | // T extends Enum statically 189 | try { 190 | return (T) Enum.valueOf((Class) clazz, value); 191 | } catch (IllegalArgumentException e) { 192 | throw new DatabaseException( 193 | "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); 194 | } 195 | } else { 196 | throw new DatabaseException( 197 | "Expected a String while deserializing to enum " 198 | + clazz 199 | + " but got a " 200 | + object.getClass()); 201 | } 202 | } 203 | 204 | @SuppressWarnings("unchecked") 205 | private static BeanMapper loadOrCreateBeanMapperForClass(Class clazz) { 206 | BeanMapper mapper = (BeanMapper) mappers.get(clazz); 207 | if (mapper == null) { 208 | mapper = new BeanMapper<>(clazz); 209 | // Inserting without checking is fine because mappers are "pure" and it's okay 210 | // if we create and use multiple by different threads temporarily 211 | mappers.put(clazz, mapper); 212 | } 213 | return mapper; 214 | } 215 | 216 | @SuppressWarnings("unchecked") 217 | private static Map expectMap(Object object) { 218 | if (object instanceof Map) { 219 | // TODO: runtime validation of keys? 220 | return (Map) object; 221 | } else { 222 | throw new DatabaseException( 223 | "Expected a Map while deserializing, but got a " + object.getClass()); 224 | } 225 | } 226 | 227 | private static Integer convertInteger(Object obj) { 228 | if (obj instanceof Integer) { 229 | return (Integer) obj; 230 | } else if (obj instanceof Long || obj instanceof Double) { 231 | double value = ((Number) obj).doubleValue(); 232 | if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { 233 | return ((Number) obj).intValue(); 234 | } else { 235 | throw new DatabaseException( 236 | "Numeric value out of 32-bit integer range: " 237 | + value 238 | + ". Did you mean to use a long or double instead of an int?"); 239 | } 240 | } else if (obj instanceof String) { 241 | return Integer.parseInt((String) obj); 242 | } else { 243 | throw new DatabaseException( 244 | "Failed to convert a value of type " + obj.getClass().getName() + " to int"); 245 | } 246 | } 247 | 248 | private static Long convertLong(Object obj) { 249 | if (obj instanceof Integer) { 250 | return ((Integer) obj).longValue(); 251 | } else if (obj instanceof Long) { 252 | return (Long) obj; 253 | } else if (obj instanceof Double) { 254 | Double value = (Double) obj; 255 | if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { 256 | return value.longValue(); 257 | } else { 258 | throw new DatabaseException( 259 | "Numeric value out of 64-bit long range: " 260 | + value 261 | + ". Did you mean to use a double instead of a long?"); 262 | } 263 | } else if (obj instanceof String) { 264 | return Long.parseLong((String) obj); 265 | } else { 266 | throw new DatabaseException( 267 | "Failed to convert a value of type " + obj.getClass().getName() + " to long"); 268 | } 269 | } 270 | 271 | private static Double convertDouble(Object obj) { 272 | if (obj instanceof Integer) { 273 | return ((Integer) obj).doubleValue(); 274 | } else if (obj instanceof Long) { 275 | Long value = (Long) obj; 276 | Double doubleValue = ((Long) obj).doubleValue(); 277 | if (doubleValue.longValue() == value) { 278 | return doubleValue; 279 | } else { 280 | throw new DatabaseException( 281 | "Loss of precision while converting number to " 282 | + "double: " 283 | + obj 284 | + ". Did you mean to use a 64-bit long instead?"); 285 | } 286 | } else if (obj instanceof Double) { 287 | return (Double) obj; 288 | } else if (obj instanceof String) { 289 | return Double.parseDouble((String) obj); 290 | } else { 291 | throw new DatabaseException( 292 | "Failed to convert a value of type " + obj.getClass().getName() + " to double"); 293 | } 294 | } 295 | 296 | private static Boolean convertBoolean(Object obj) { 297 | if (obj instanceof Boolean) { 298 | return (Boolean) obj; 299 | } else { 300 | throw new DatabaseException( 301 | "Failed to convert value of type " + obj.getClass().getName() + " to boolean"); 302 | } 303 | } 304 | 305 | private static String convertString(Object obj) { 306 | if (obj instanceof String) { 307 | return (String) obj; 308 | } else { 309 | throw new DatabaseException( 310 | "Failed to convert value of type " + obj.getClass().getName() + " to String"); 311 | } 312 | } 313 | 314 | private static void convertBean(Object bean, Map properties) { 315 | BeanMapper mapper = loadOrCreateBeanMapperForClass(bean.getClass()); 316 | if (properties instanceof Map) { 317 | mapper.deserialize(bean, expectMap(properties)); 318 | } else { 319 | throw new DatabaseException( 320 | "Can't convert object of type " 321 | + properties.getClass().getName() 322 | + " to type " 323 | + bean.getClass().getName()); 324 | } 325 | } 326 | 327 | private static T convertBean(Object obj, Class clazz) { 328 | BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); 329 | if (obj instanceof Map) { 330 | return mapper.deserialize(expectMap(obj)); 331 | } else { 332 | throw new DatabaseException( 333 | "Can't convert object of type " 334 | + obj.getClass().getName() 335 | + " to type " 336 | + clazz.getName()); 337 | } 338 | } 339 | 340 | private static class BeanMapper { 341 | 342 | private final Class clazz; 343 | private final Constructor constructor; 344 | private final boolean throwOnUnknownProperties; 345 | private final boolean warnOnUnknownProperties; 346 | // Case insensitive mapping of properties to their case sensitive versions 347 | private final Map properties; 348 | 349 | private final Map getters; 350 | private final Map setters; 351 | private final Map fields; 352 | 353 | public BeanMapper(Class clazz) { 354 | this.clazz = clazz; 355 | this.throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class); 356 | this.warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class); 357 | this.properties = new HashMap<>(); 358 | 359 | this.setters = new HashMap<>(); 360 | this.getters = new HashMap<>(); 361 | this.fields = new HashMap<>(); 362 | 363 | Constructor constructor = null; 364 | try { 365 | constructor = clazz.getDeclaredConstructor(); 366 | constructor.setAccessible(true); 367 | } catch (NoSuchMethodException e) { 368 | // We will only fail at deserialization time if no constructor is present 369 | constructor = null; 370 | } 371 | this.constructor = constructor; 372 | // Add any public getters to properties (including isXyz()) 373 | for (Method method : clazz.getMethods()) { 374 | if (shouldIncludeGetter(method)) { 375 | String propertyName = propertyName(method); 376 | addProperty(propertyName); 377 | method.setAccessible(true); 378 | if (getters.containsKey(propertyName)) { 379 | throw new DatabaseException("Found conflicting getters for name: " + method.getName()); 380 | } 381 | getters.put(propertyName, method); 382 | } 383 | } 384 | 385 | // Add any public fields to properties 386 | for (Field field : clazz.getFields()) { 387 | if (shouldIncludeField(field)) { 388 | String propertyName = propertyName(field); 389 | 390 | addProperty(propertyName); 391 | } 392 | } 393 | 394 | // We can use private setters and fields for known (public) properties/getters. Since 395 | // getMethods/getFields only returns public methods/fields we need to traverse the 396 | // class hierarchy to find the appropriate setter or field. 397 | Class currentClass = clazz; 398 | do { 399 | // Add any setters 400 | for (Method method : currentClass.getDeclaredMethods()) { 401 | if (shouldIncludeSetter(method)) { 402 | String propertyName = propertyName(method); 403 | String existingPropertyName = properties.get(propertyName.toLowerCase()); 404 | if (existingPropertyName != null) { 405 | if (!existingPropertyName.equals(propertyName)) { 406 | throw new DatabaseException( 407 | "Found setter with invalid case-sensitive name: " + method.getName()); 408 | } else { 409 | Method existingSetter = setters.get(propertyName); 410 | if (existingSetter == null) { 411 | method.setAccessible(true); 412 | setters.put(propertyName, method); 413 | } else if (!isSetterOverride(method, existingSetter)) { 414 | // We require that setters with conflicting property names are 415 | // overrides from a base class 416 | throw new DatabaseException( 417 | "Found a conflicting setters " 418 | + "with name: " 419 | + method.getName() 420 | + " (conflicts with " 421 | + existingSetter.getName() 422 | + " defined on " 423 | + existingSetter.getDeclaringClass().getName() 424 | + ")"); 425 | } 426 | } 427 | } 428 | } 429 | } 430 | 431 | for (Field field : currentClass.getDeclaredFields()) { 432 | String propertyName = propertyName(field); 433 | 434 | // Case sensitivity is checked at deserialization time 435 | // Fields are only added if they don't exist on a subclass 436 | if (properties.containsKey(propertyName.toLowerCase()) 437 | && !fields.containsKey(propertyName)) { 438 | field.setAccessible(true); 439 | fields.put(propertyName, field); 440 | } 441 | } 442 | 443 | // Traverse class hierarchy until we reach java.lang.Object which contains a bunch 444 | // of fields/getters we don't want to serialize 445 | currentClass = currentClass.getSuperclass(); 446 | } while (currentClass != null && !currentClass.equals(Object.class)); 447 | 448 | if (properties.isEmpty()) { 449 | throw new DatabaseException("No properties to serialize found on class " + clazz.getName()); 450 | } 451 | } 452 | 453 | private static boolean shouldIncludeGetter(Method method) { 454 | if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { 455 | return false; 456 | } 457 | // Exclude methods from Object.class 458 | if (method.getDeclaringClass().equals(Object.class)) { 459 | return false; 460 | } 461 | // Non-public methods 462 | if (!Modifier.isPublic(method.getModifiers())) { 463 | return false; 464 | } 465 | // Static methods 466 | if (Modifier.isStatic(method.getModifiers())) { 467 | return false; 468 | } 469 | // No return type 470 | if (method.getReturnType().equals(Void.TYPE)) { 471 | return false; 472 | } 473 | // Non-zero parameters 474 | if (method.getParameterTypes().length != 0) { 475 | return false; 476 | } 477 | // Excluded methods 478 | if (method.isAnnotationPresent(Exclude.class)) { 479 | return false; 480 | } 481 | if (method.getDeclaringClass().getPackage().getName().startsWith("android.")) { 482 | return false; 483 | } 484 | return true; 485 | } 486 | 487 | private static boolean shouldIncludeSetter(Method method) { 488 | if (!method.getName().startsWith("set")) { 489 | return false; 490 | } 491 | // Exclude methods from Object.class 492 | if (method.getDeclaringClass().equals(Object.class)) { 493 | return false; 494 | } 495 | // Static methods 496 | if (Modifier.isStatic(method.getModifiers())) { 497 | return false; 498 | } 499 | // Has a return type 500 | if (!method.getReturnType().equals(Void.TYPE)) { 501 | return false; 502 | } 503 | // Methods without exactly one parameters 504 | if (method.getParameterTypes().length != 1) { 505 | return false; 506 | } 507 | // Excluded methods 508 | if (method.isAnnotationPresent(Exclude.class)) { 509 | return false; 510 | } 511 | if (method.getDeclaringClass().getPackage().getName().startsWith("android.")) { 512 | return false; 513 | } 514 | return true; 515 | } 516 | 517 | private static boolean shouldIncludeField(Field field) { 518 | // Exclude methods from Object.class 519 | if (field.getDeclaringClass().equals(Object.class)) { 520 | return false; 521 | } 522 | // Non-public fields 523 | if (!Modifier.isPublic(field.getModifiers())) { 524 | return false; 525 | } 526 | // Static fields 527 | if (Modifier.isStatic(field.getModifiers())) { 528 | return false; 529 | } 530 | // Transient fields 531 | if (Modifier.isTransient(field.getModifiers())) { 532 | return false; 533 | } 534 | // Excluded fields 535 | if (field.isAnnotationPresent(Exclude.class)) { 536 | return false; 537 | } 538 | return true; 539 | } 540 | 541 | private static boolean isSetterOverride(Method base, Method override) { 542 | // We expect an overridden setter here 543 | hardAssert( 544 | base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), 545 | "Expected override from a base class"); 546 | hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); 547 | hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type"); 548 | 549 | Type[] baseParameterTypes = base.getParameterTypes(); 550 | Type[] overrideParameterTypes = override.getParameterTypes(); 551 | hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); 552 | hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); 553 | 554 | return base.getName().equals(override.getName()) 555 | && baseParameterTypes[0].equals(overrideParameterTypes[0]); 556 | } 557 | 558 | private static String propertyName(Field field) { 559 | String annotatedName = annotatedName(field); 560 | return annotatedName != null ? annotatedName : field.getName(); 561 | } 562 | 563 | private static String propertyName(Method method) { 564 | String annotatedName = annotatedName(method); 565 | return annotatedName != null ? annotatedName : serializedName(method.getName()); 566 | } 567 | 568 | private static String annotatedName(AccessibleObject obj) { 569 | if (obj.isAnnotationPresent(PropertyName.class)) { 570 | PropertyName annotation = obj.getAnnotation(PropertyName.class); 571 | return annotation.value(); 572 | } 573 | 574 | return null; 575 | } 576 | 577 | private static String serializedName(String methodName) { 578 | String[] prefixes = new String[] {"get", "set", "is"}; 579 | String methodPrefix = null; 580 | for (String prefix : prefixes) { 581 | if (methodName.startsWith(prefix)) { 582 | methodPrefix = prefix; 583 | } 584 | } 585 | if (methodPrefix == null) { 586 | throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); 587 | } 588 | String strippedName = methodName.substring(methodPrefix.length()); 589 | 590 | // Make sure the first word or upper-case prefix is converted to lower-case 591 | char[] chars = strippedName.toCharArray(); 592 | int pos = 0; 593 | while (pos < chars.length && Character.isUpperCase(chars[pos])) { 594 | chars[pos] = Character.toLowerCase(chars[pos]); 595 | pos++; 596 | } 597 | return new String(chars); 598 | } 599 | 600 | private void addProperty(String property) { 601 | String oldValue = this.properties.put(property.toLowerCase(), property); 602 | if (oldValue != null && !property.equals(oldValue)) { 603 | throw new DatabaseException( 604 | "Found two getters or fields with conflicting case " 605 | + "sensitivity for property: " 606 | + property.toLowerCase()); 607 | } 608 | } 609 | 610 | public void deserialize(Object bean, Map values) { 611 | deserialize(bean, values, Collections.>, Type>emptyMap()); 612 | } 613 | 614 | public void deserialize(Object bean, Map values, Map>, Type> types) { 615 | for (Map.Entry entry : values.entrySet()) { 616 | String propertyName = entry.getKey(); 617 | if (this.setters.containsKey(propertyName)) { 618 | Method setter = this.setters.get(propertyName); 619 | Type[] params = setter.getGenericParameterTypes(); 620 | if (params.length != 1) { 621 | throw new IllegalStateException("Setter does not have exactly one " + "parameter"); 622 | } 623 | Type resolvedType = resolveType(params[0], types); 624 | Object value = BeanUtils.deserializeToType(entry.getValue(), resolvedType); 625 | try { 626 | setter.invoke(bean, value); 627 | } catch (IllegalAccessException e) { 628 | throw new RuntimeException(e); 629 | } catch (InvocationTargetException e) { 630 | throw new RuntimeException(e); 631 | } 632 | } else if (this.fields.containsKey(propertyName)) { 633 | Field field = this.fields.get(propertyName); 634 | Type resolvedType = resolveType(field.getGenericType(), types); 635 | Object value = BeanUtils.deserializeToType(entry.getValue(), resolvedType); 636 | try { 637 | field.set(bean, value); 638 | } catch (IllegalAccessException e) { 639 | throw new RuntimeException(e); 640 | } 641 | } else { 642 | String message = 643 | "No setter/field for " 644 | + propertyName 645 | + " found " 646 | + "on class " 647 | + this.clazz.getName(); 648 | if (this.properties.containsKey(propertyName.toLowerCase())) { 649 | message += " (fields/setters are case sensitive!)"; 650 | } 651 | if (this.throwOnUnknownProperties) { 652 | throw new DatabaseException(message); 653 | } else if (this.warnOnUnknownProperties) { 654 | logger.log(Level.WARNING, message); 655 | } 656 | } 657 | } 658 | } 659 | 660 | public T deserialize(Map values) { 661 | return deserialize(values, Collections.>, Type>emptyMap()); 662 | } 663 | 664 | public T deserialize(Map values, Map>, Type> types) { 665 | if (this.constructor == null) { 666 | throw new DatabaseException( 667 | "Class " + this.clazz.getName() + " is missing a constructor with no arguments"); 668 | } 669 | T instance; 670 | try { 671 | instance = this.constructor.newInstance(); 672 | } catch (InstantiationException e) { 673 | throw new RuntimeException(e); 674 | } catch (IllegalAccessException e) { 675 | throw new RuntimeException(e); 676 | } catch (InvocationTargetException e) { 677 | throw new RuntimeException(e); 678 | } 679 | for (Map.Entry entry : values.entrySet()) { 680 | String propertyName = entry.getKey(); 681 | if (this.setters.containsKey(propertyName)) { 682 | Method setter = this.setters.get(propertyName); 683 | Type[] params = setter.getGenericParameterTypes(); 684 | if (params.length != 1) { 685 | throw new IllegalStateException("Setter does not have exactly one " + "parameter"); 686 | } 687 | Type resolvedType = resolveType(params[0], types); 688 | Object value = BeanUtils.deserializeToType(entry.getValue(), resolvedType); 689 | try { 690 | setter.invoke(instance, value); 691 | } catch (IllegalAccessException e) { 692 | throw new RuntimeException(e); 693 | } catch (InvocationTargetException e) { 694 | throw new RuntimeException(e); 695 | } 696 | } else if (this.fields.containsKey(propertyName)) { 697 | Field field = this.fields.get(propertyName); 698 | Type resolvedType = resolveType(field.getGenericType(), types); 699 | Object value = BeanUtils.deserializeToType(entry.getValue(), resolvedType); 700 | try { 701 | field.set(instance, value); 702 | } catch (IllegalAccessException e) { 703 | throw new RuntimeException(e); 704 | } 705 | } else { 706 | String message = 707 | "No setter/field for " 708 | + propertyName 709 | + " found " 710 | + "on class " 711 | + this.clazz.getName(); 712 | if (this.properties.containsKey(propertyName.toLowerCase())) { 713 | message += " (fields/setters are case sensitive!)"; 714 | } 715 | if (this.throwOnUnknownProperties) { 716 | throw new DatabaseException(message); 717 | } else if (this.warnOnUnknownProperties) { 718 | logger.log(Level.WARNING, message); 719 | } 720 | } 721 | } 722 | return instance; 723 | } 724 | 725 | private Type resolveType(Type type, Map>, Type> types) { 726 | if (type instanceof TypeVariable) { 727 | Type resolvedType = types.get(type); 728 | if (resolvedType == null) { 729 | throw new IllegalStateException("Could not resolve type " + type); 730 | } else { 731 | return resolvedType; 732 | } 733 | } else { 734 | return type; 735 | } 736 | } 737 | } 738 | } -------------------------------------------------------------------------------- /realtime-channel/src/main/java/com/goodow/realtime/android/mvp/util/CustomClassMapper.java: -------------------------------------------------------------------------------- 1 | package com.goodow.realtime.android.mvp.util; 2 | 3 | /** 4 | * Created by larry on 2017/11/9. 5 | */ 6 | 7 | //import static com.google.firebase.database.utilities.Utilities.hardAssert; 8 | 9 | import com.google.firebase.database.DatabaseException; 10 | import com.google.firebase.database.Exclude; 11 | import com.google.firebase.database.GenericTypeIndicator; 12 | import com.google.firebase.database.IgnoreExtraProperties; 13 | import com.google.firebase.database.PropertyName; 14 | import com.google.firebase.database.ThrowOnExtraProperties; 15 | 16 | import java.lang.reflect.AccessibleObject; 17 | import java.lang.reflect.Constructor; 18 | import java.lang.reflect.Field; 19 | import java.lang.reflect.GenericArrayType; 20 | import java.lang.reflect.InvocationTargetException; 21 | import java.lang.reflect.Method; 22 | import java.lang.reflect.Modifier; 23 | import java.lang.reflect.ParameterizedType; 24 | import java.lang.reflect.Type; 25 | import java.lang.reflect.TypeVariable; 26 | import java.lang.reflect.WildcardType; 27 | import java.util.ArrayList; 28 | import java.util.Collection; 29 | import java.util.Collections; 30 | import java.util.HashMap; 31 | import java.util.List; 32 | import java.util.Map; 33 | import java.util.concurrent.ConcurrentHashMap; 34 | import java.util.concurrent.ConcurrentMap; 35 | import java.util.logging.Level; 36 | import java.util.logging.Logger; 37 | 38 | /** Helper class to convert to/from custom POJO classes and plain Java types. */ 39 | public class CustomClassMapper { 40 | 41 | private static final Logger logger = Logger.getLogger(CustomClassMapper.class.getName()); 42 | 43 | private static final ConcurrentMap, BeanMapper> mappers = new ConcurrentHashMap<>(); 44 | 45 | public static void hardAssert(boolean condition) { 46 | hardAssert(condition, ""); 47 | } 48 | 49 | public static void hardAssert(boolean condition, String message) { 50 | if (!condition) { 51 | throw new AssertionError("hardAssert failed: " + message); 52 | } 53 | } 54 | 55 | /** 56 | * Converts a Java representation of JSON data to standard library Java data types: Map, Array, 57 | * String, Double, Integer and Boolean. POJOs are converted to Java Maps. 58 | * 59 | * @param object The representation of the JSON data 60 | * @return JSON representation containing only standard library Java types 61 | */ 62 | public static Object convertToPlainJavaTypes(Object object) { 63 | return serialize(object); 64 | } 65 | 66 | @SuppressWarnings("unchecked") 67 | public static Map convertToPlainJavaTypes(Map update) { 68 | Object converted = serialize(update); 69 | hardAssert(converted instanceof Map); 70 | return (Map) converted; 71 | } 72 | 73 | /** 74 | * Converts a standard library Java representation of JSON data to an object of the provided 75 | * class. 76 | * 77 | * @param object The representation of the JSON data 78 | * @param clazz The class of the object to convert to 79 | * @return The POJO object. 80 | */ 81 | public static T convertToCustomClass(Object object, Class clazz) { 82 | return deserializeToClass(object, clazz); 83 | } 84 | 85 | /** 86 | * Converts a standard library Java representation of JSON data to an object of the class provided 87 | * through the GenericTypeIndicator 88 | * 89 | * @param object The representation of the JSON data 90 | * @param typeIndicator The indicator providing class of the object to convert to 91 | * @return The POJO object. 92 | */ 93 | public static T convertToCustomClass(Object object, GenericTypeIndicator typeIndicator) { 94 | Class clazz = typeIndicator.getClass(); 95 | Type genericTypeIndicatorType = clazz.getGenericSuperclass(); 96 | if (genericTypeIndicatorType instanceof ParameterizedType) { 97 | ParameterizedType parameterizedType = (ParameterizedType) genericTypeIndicatorType; 98 | if (!parameterizedType.getRawType().equals(GenericTypeIndicator.class)) { 99 | throw new DatabaseException( 100 | "Not a direct subclass of GenericTypeIndicator: " + genericTypeIndicatorType); 101 | } 102 | // We are guaranteed to have exactly one type parameter 103 | Type type = parameterizedType.getActualTypeArguments()[0]; 104 | return deserializeToType(object, type); 105 | } else { 106 | throw new DatabaseException( 107 | "Not a direct subclass of GenericTypeIndicator: " + genericTypeIndicatorType); 108 | } 109 | } 110 | 111 | @SuppressWarnings("unchecked") 112 | private static Object serialize(T obj) { 113 | if (obj == null) { 114 | return null; 115 | } else if (obj instanceof Number) { 116 | if (obj instanceof Float) { 117 | return ((Float) obj).doubleValue(); 118 | } else if (obj instanceof Short) { 119 | throw new DatabaseException("Shorts are not supported, please use int or long"); 120 | } else if (obj instanceof Byte) { 121 | throw new DatabaseException("Bytes are not supported, please use int or long"); 122 | } else { 123 | // Long, Integer, Double 124 | return obj; 125 | } 126 | } else if (obj instanceof String) { 127 | return obj; 128 | } else if (obj instanceof Boolean) { 129 | return obj; 130 | } else if (obj instanceof Character) { 131 | throw new DatabaseException("Characters are not supported, please strings"); 132 | } else if (obj instanceof Map) { 133 | Map result = new HashMap<>(); 134 | for (Map.Entry entry : ((Map) obj).entrySet()) { 135 | Object key = entry.getKey(); 136 | if (key instanceof String) { 137 | String keyString = (String) key; 138 | result.put(keyString, serialize(entry.getValue())); 139 | } else { 140 | throw new DatabaseException("Maps with non-string keys are not supported"); 141 | } 142 | } 143 | return result; 144 | } else if (obj instanceof Collection) { 145 | if (obj instanceof List) { 146 | List list = (List) obj; 147 | List result = new ArrayList<>(list.size()); 148 | for (Object object : list) { 149 | result.add(serialize(object)); 150 | } 151 | return result; 152 | } else { 153 | throw new DatabaseException( 154 | "Serializing Collections is not supported, " + "please use Lists instead"); 155 | } 156 | } else if (obj.getClass().isArray()) { 157 | throw new DatabaseException( 158 | "Serializing Arrays is not supported, please use Lists " + "instead"); 159 | } else if (obj instanceof Enum) { 160 | return ((Enum) obj).name(); 161 | } else { 162 | Class clazz = (Class) obj.getClass(); 163 | BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); 164 | return mapper.serialize(obj); 165 | } 166 | } 167 | 168 | @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) 169 | private static T deserializeToType(Object obj, Type type) { 170 | if (obj == null) { 171 | return null; 172 | } else if (type instanceof ParameterizedType) { 173 | return deserializeToParameterizedType(obj, (ParameterizedType) type); 174 | } else if (type instanceof Class) { 175 | return deserializeToClass(obj, (Class) type); 176 | } else if (type instanceof WildcardType) { 177 | throw new DatabaseException("Generic wildcard types are not supported"); 178 | } else if (type instanceof GenericArrayType) { 179 | throw new DatabaseException( 180 | "Generic Arrays are not supported, please use Lists " + "instead"); 181 | } else { 182 | throw new IllegalStateException("Unknown type encountered: " + type); 183 | } 184 | } 185 | 186 | @SuppressWarnings("unchecked") 187 | private static T deserializeToClass(Object obj, Class clazz) { 188 | if (obj == null) { 189 | return null; 190 | } else if (clazz.isPrimitive() 191 | || Number.class.isAssignableFrom(clazz) 192 | || Boolean.class.isAssignableFrom(clazz) 193 | || Character.class.isAssignableFrom(clazz)) { 194 | return deserializeToPrimitive(obj, clazz); 195 | } else if (String.class.isAssignableFrom(clazz)) { 196 | return (T) convertString(obj); 197 | } else if (clazz.isArray()) { 198 | throw new DatabaseException( 199 | "Converting to Arrays is not supported, please use Lists" + "instead"); 200 | } else if (clazz.getTypeParameters().length > 0) { 201 | throw new DatabaseException( 202 | "Class " 203 | + clazz.getName() 204 | + " has generic type " 205 | + "parameters, please use GenericTypeIndicator instead"); 206 | } else if (clazz.equals(Object.class)) { 207 | return (T) obj; 208 | } else if (clazz.isEnum()) { 209 | return deserializeToEnum(obj, clazz); 210 | } else { 211 | return convertBean(obj, clazz); 212 | } 213 | } 214 | 215 | @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) 216 | private static T deserializeToParameterizedType(Object obj, ParameterizedType type) { 217 | // getRawType should always return a Class 218 | Class rawType = (Class) type.getRawType(); 219 | if (List.class.isAssignableFrom(rawType)) { 220 | Type genericType = type.getActualTypeArguments()[0]; 221 | if (obj instanceof List) { 222 | List list = (List) obj; 223 | List result = new ArrayList<>(list.size()); 224 | for (Object object : list) { 225 | result.add(deserializeToType(object, genericType)); 226 | } 227 | return (T) result; 228 | } else { 229 | throw new DatabaseException( 230 | "Expected a List while deserializing, but got a " + obj.getClass()); 231 | } 232 | } else if (Map.class.isAssignableFrom(rawType)) { 233 | Type keyType = type.getActualTypeArguments()[0]; 234 | Type valueType = type.getActualTypeArguments()[1]; 235 | if (!keyType.equals(String.class)) { 236 | throw new DatabaseException( 237 | "Only Maps with string keys are supported, " 238 | + "but found Map with key type " 239 | + keyType); 240 | } 241 | Map map = expectMap(obj); 242 | HashMap result = new HashMap<>(); 243 | for (Map.Entry entry : map.entrySet()) { 244 | result.put(entry.getKey(), deserializeToType(entry.getValue(), valueType)); 245 | } 246 | return (T) result; 247 | } else if (Collection.class.isAssignableFrom(rawType)) { 248 | throw new DatabaseException("Collections are not supported, please use Lists instead"); 249 | } else { 250 | Map map = expectMap(obj); 251 | BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType); 252 | HashMap>, Type> typeMapping = new HashMap<>(); 253 | TypeVariable>[] typeVariables = mapper.clazz.getTypeParameters(); 254 | Type[] types = type.getActualTypeArguments(); 255 | if (types.length != typeVariables.length) { 256 | throw new IllegalStateException("Mismatched lengths for type variables and actual types"); 257 | } 258 | for (int i = 0; i < typeVariables.length; i++) { 259 | typeMapping.put(typeVariables[i], types[i]); 260 | } 261 | return mapper.deserialize(map, typeMapping); 262 | } 263 | } 264 | 265 | @SuppressWarnings("unchecked") 266 | private static T deserializeToPrimitive(Object obj, Class clazz) { 267 | if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) { 268 | return (T) convertInteger(obj); 269 | } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) { 270 | return (T) convertBoolean(obj); 271 | } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) { 272 | return (T) convertDouble(obj); 273 | } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) { 274 | return (T) convertLong(obj); 275 | } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { 276 | return (T) (Float) convertDouble(obj).floatValue(); 277 | } else if (Short.class.isAssignableFrom(clazz) || short.class.isAssignableFrom(clazz)) { 278 | throw new DatabaseException("Deserializing to shorts is not supported"); 279 | } else if (Byte.class.isAssignableFrom(clazz) || byte.class.isAssignableFrom(clazz)) { 280 | throw new DatabaseException("Deserializing to bytes is not supported"); 281 | } else if (Character.class.isAssignableFrom(clazz) || char.class.isAssignableFrom(clazz)) { 282 | throw new DatabaseException("Deserializing to char is not supported"); 283 | } else { 284 | throw new IllegalArgumentException("Unknown primitive type: " + clazz); 285 | } 286 | } 287 | 288 | @SuppressWarnings("unchecked") 289 | private static T deserializeToEnum(Object object, Class clazz) { 290 | if (object instanceof String) { 291 | String value = (String) object; 292 | // We cast to Class without generics here since we can't prove the bound 293 | // T extends Enum statically 294 | try { 295 | return (T) Enum.valueOf((Class) clazz, value); 296 | } catch (IllegalArgumentException e) { 297 | throw new DatabaseException( 298 | "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); 299 | } 300 | } else { 301 | throw new DatabaseException( 302 | "Expected a String while deserializing to enum " 303 | + clazz 304 | + " but got a " 305 | + object.getClass()); 306 | } 307 | } 308 | 309 | @SuppressWarnings("unchecked") 310 | private static BeanMapper loadOrCreateBeanMapperForClass(Class clazz) { 311 | BeanMapper mapper = (BeanMapper) mappers.get(clazz); 312 | if (mapper == null) { 313 | mapper = new BeanMapper<>(clazz); 314 | // Inserting without checking is fine because mappers are "pure" and it's okay 315 | // if we create and use multiple by different threads temporarily 316 | mappers.put(clazz, mapper); 317 | } 318 | return mapper; 319 | } 320 | 321 | @SuppressWarnings("unchecked") 322 | private static Map expectMap(Object object) { 323 | if (object instanceof Map) { 324 | // TODO: runtime validation of keys? 325 | return (Map) object; 326 | } else { 327 | throw new DatabaseException( 328 | "Expected a Map while deserializing, but got a " + object.getClass()); 329 | } 330 | } 331 | 332 | private static Integer convertInteger(Object obj) { 333 | if (obj instanceof Integer) { 334 | return (Integer) obj; 335 | } else if (obj instanceof Long || obj instanceof Double) { 336 | double value = ((Number) obj).doubleValue(); 337 | if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { 338 | return ((Number) obj).intValue(); 339 | } else { 340 | throw new DatabaseException( 341 | "Numeric value out of 32-bit integer range: " 342 | + value 343 | + ". Did you mean to use a long or double instead of an int?"); 344 | } 345 | } else if (obj instanceof String) { 346 | return Integer.parseInt((String) obj); 347 | } else { 348 | throw new DatabaseException( 349 | "Failed to convert a value of type " + obj.getClass().getName() + " to int"); 350 | } 351 | } 352 | 353 | private static Long convertLong(Object obj) { 354 | if (obj instanceof Integer) { 355 | return ((Integer) obj).longValue(); 356 | } else if (obj instanceof Long) { 357 | return (Long) obj; 358 | } else if (obj instanceof Double) { 359 | Double value = (Double) obj; 360 | if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { 361 | return value.longValue(); 362 | } else { 363 | throw new DatabaseException( 364 | "Numeric value out of 64-bit long range: " 365 | + value 366 | + ". Did you mean to use a double instead of a long?"); 367 | } 368 | } else if (obj instanceof String) { 369 | return Long.parseLong((String) obj); 370 | } else { 371 | throw new DatabaseException( 372 | "Failed to convert a value of type " + obj.getClass().getName() + " to long"); 373 | } 374 | } 375 | 376 | private static Double convertDouble(Object obj) { 377 | if (obj instanceof Integer) { 378 | return ((Integer) obj).doubleValue(); 379 | } else if (obj instanceof Long) { 380 | Long value = (Long) obj; 381 | Double doubleValue = ((Long) obj).doubleValue(); 382 | if (doubleValue.longValue() == value) { 383 | return doubleValue; 384 | } else { 385 | throw new DatabaseException( 386 | "Loss of precision while converting number to " 387 | + "double: " 388 | + obj 389 | + ". Did you mean to use a 64-bit long instead?"); 390 | } 391 | } else if (obj instanceof Double) { 392 | return (Double) obj; 393 | } else if (obj instanceof String) { 394 | return Double.parseDouble((String) obj); 395 | } else { 396 | throw new DatabaseException( 397 | "Failed to convert a value of type " + obj.getClass().getName() + " to double"); 398 | } 399 | } 400 | 401 | private static Boolean convertBoolean(Object obj) { 402 | if (obj instanceof Boolean) { 403 | return (Boolean) obj; 404 | } else { 405 | throw new DatabaseException( 406 | "Failed to convert value of type " + obj.getClass().getName() + " to boolean"); 407 | } 408 | } 409 | 410 | private static String convertString(Object obj) { 411 | if (obj instanceof String) { 412 | return (String) obj; 413 | } else { 414 | throw new DatabaseException( 415 | "Failed to convert value of type " + obj.getClass().getName() + " to String"); 416 | } 417 | } 418 | 419 | private static T convertBean(Object obj, Class clazz) { 420 | BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); 421 | if (obj instanceof Map) { 422 | return mapper.deserialize(expectMap(obj)); 423 | } else { 424 | throw new DatabaseException( 425 | "Can't convert object of type " 426 | + obj.getClass().getName() 427 | + " to type " 428 | + clazz.getName()); 429 | } 430 | } 431 | 432 | private static class BeanMapper { 433 | 434 | private final Class clazz; 435 | private final Constructor constructor; 436 | private final boolean throwOnUnknownProperties; 437 | private final boolean warnOnUnknownProperties; 438 | // Case insensitive mapping of properties to their case sensitive versions 439 | private final Map properties; 440 | 441 | private final Map getters; 442 | private final Map setters; 443 | private final Map fields; 444 | 445 | public BeanMapper(Class clazz) { 446 | this.clazz = clazz; 447 | this.throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class); 448 | this.warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class); 449 | this.properties = new HashMap<>(); 450 | 451 | this.setters = new HashMap<>(); 452 | this.getters = new HashMap<>(); 453 | this.fields = new HashMap<>(); 454 | 455 | Constructor constructor = null; 456 | try { 457 | constructor = clazz.getDeclaredConstructor(); 458 | constructor.setAccessible(true); 459 | } catch (NoSuchMethodException e) { 460 | // We will only fail at deserialization time if no constructor is present 461 | constructor = null; 462 | } 463 | this.constructor = constructor; 464 | // Add any public getters to properties (including isXyz()) 465 | for (Method method : clazz.getMethods()) { 466 | if (shouldIncludeGetter(method)) { 467 | String propertyName = propertyName(method); 468 | addProperty(propertyName); 469 | method.setAccessible(true); 470 | if (getters.containsKey(propertyName)) { 471 | throw new DatabaseException("Found conflicting getters for name: " + method.getName()); 472 | } 473 | getters.put(propertyName, method); 474 | } 475 | } 476 | 477 | // Add any public fields to properties 478 | for (Field field : clazz.getFields()) { 479 | if (shouldIncludeField(field)) { 480 | String propertyName = propertyName(field); 481 | 482 | addProperty(propertyName); 483 | } 484 | } 485 | 486 | // We can use private setters and fields for known (public) properties/getters. Since 487 | // getMethods/getFields only returns public methods/fields we need to traverse the 488 | // class hierarchy to find the appropriate setter or field. 489 | Class currentClass = clazz; 490 | do { 491 | // Add any setters 492 | for (Method method : currentClass.getDeclaredMethods()) { 493 | if (shouldIncludeSetter(method)) { 494 | String propertyName = propertyName(method); 495 | String existingPropertyName = properties.get(propertyName.toLowerCase()); 496 | if (existingPropertyName != null) { 497 | if (!existingPropertyName.equals(propertyName)) { 498 | throw new DatabaseException( 499 | "Found setter with invalid case-sensitive name: " + method.getName()); 500 | } else { 501 | Method existingSetter = setters.get(propertyName); 502 | if (existingSetter == null) { 503 | method.setAccessible(true); 504 | setters.put(propertyName, method); 505 | } else if (!isSetterOverride(method, existingSetter)) { 506 | // We require that setters with conflicting property names are 507 | // overrides from a base class 508 | throw new DatabaseException( 509 | "Found a conflicting setters " 510 | + "with name: " 511 | + method.getName() 512 | + " (conflicts with " 513 | + existingSetter.getName() 514 | + " defined on " 515 | + existingSetter.getDeclaringClass().getName() 516 | + ")"); 517 | } 518 | } 519 | } 520 | } 521 | } 522 | 523 | for (Field field : currentClass.getDeclaredFields()) { 524 | String propertyName = propertyName(field); 525 | 526 | // Case sensitivity is checked at deserialization time 527 | // Fields are only added if they don't exist on a subclass 528 | if (properties.containsKey(propertyName.toLowerCase()) 529 | && !fields.containsKey(propertyName)) { 530 | field.setAccessible(true); 531 | fields.put(propertyName, field); 532 | } 533 | } 534 | 535 | // Traverse class hierarchy until we reach java.lang.Object which contains a bunch 536 | // of fields/getters we don't want to serialize 537 | currentClass = currentClass.getSuperclass(); 538 | } while (currentClass != null && !currentClass.equals(Object.class)); 539 | 540 | if (properties.isEmpty()) { 541 | throw new DatabaseException("No properties to serialize found on class " + clazz.getName()); 542 | } 543 | } 544 | 545 | private static boolean shouldIncludeGetter(Method method) { 546 | if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { 547 | return false; 548 | } 549 | // Exclude methods from Object.class 550 | if (method.getDeclaringClass().equals(Object.class)) { 551 | return false; 552 | } 553 | // Non-public methods 554 | if (!Modifier.isPublic(method.getModifiers())) { 555 | return false; 556 | } 557 | // Static methods 558 | if (Modifier.isStatic(method.getModifiers())) { 559 | return false; 560 | } 561 | // No return type 562 | if (method.getReturnType().equals(Void.TYPE)) { 563 | return false; 564 | } 565 | // Non-zero parameters 566 | if (method.getParameterTypes().length != 0) { 567 | return false; 568 | } 569 | // Excluded methods 570 | if (method.isAnnotationPresent(Exclude.class)) { 571 | return false; 572 | } 573 | return true; 574 | } 575 | 576 | private static boolean shouldIncludeSetter(Method method) { 577 | if (!method.getName().startsWith("set")) { 578 | return false; 579 | } 580 | // Exclude methods from Object.class 581 | if (method.getDeclaringClass().equals(Object.class)) { 582 | return false; 583 | } 584 | // Static methods 585 | if (Modifier.isStatic(method.getModifiers())) { 586 | return false; 587 | } 588 | // Has a return type 589 | if (!method.getReturnType().equals(Void.TYPE)) { 590 | return false; 591 | } 592 | // Methods without exactly one parameters 593 | if (method.getParameterTypes().length != 1) { 594 | return false; 595 | } 596 | // Excluded methods 597 | if (method.isAnnotationPresent(Exclude.class)) { 598 | return false; 599 | } 600 | return true; 601 | } 602 | 603 | private static boolean shouldIncludeField(Field field) { 604 | // Exclude methods from Object.class 605 | if (field.getDeclaringClass().equals(Object.class)) { 606 | return false; 607 | } 608 | // Non-public fields 609 | if (!Modifier.isPublic(field.getModifiers())) { 610 | return false; 611 | } 612 | // Static fields 613 | if (Modifier.isStatic(field.getModifiers())) { 614 | return false; 615 | } 616 | // Transient fields 617 | if (Modifier.isTransient(field.getModifiers())) { 618 | return false; 619 | } 620 | // Excluded fields 621 | if (field.isAnnotationPresent(Exclude.class)) { 622 | return false; 623 | } 624 | return true; 625 | } 626 | 627 | private static boolean isSetterOverride(Method base, Method override) { 628 | // We expect an overridden setter here 629 | hardAssert( 630 | base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), 631 | "Expected override from a base class"); 632 | hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); 633 | hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type"); 634 | 635 | Type[] baseParameterTypes = base.getParameterTypes(); 636 | Type[] overrideParameterTypes = override.getParameterTypes(); 637 | hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); 638 | hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); 639 | 640 | return base.getName().equals(override.getName()) 641 | && baseParameterTypes[0].equals(overrideParameterTypes[0]); 642 | } 643 | 644 | private static String propertyName(Field field) { 645 | String annotatedName = annotatedName(field); 646 | return annotatedName != null ? annotatedName : field.getName(); 647 | } 648 | 649 | private static String propertyName(Method method) { 650 | String annotatedName = annotatedName(method); 651 | return annotatedName != null ? annotatedName : serializedName(method.getName()); 652 | } 653 | 654 | private static String annotatedName(AccessibleObject obj) { 655 | if (obj.isAnnotationPresent(PropertyName.class)) { 656 | PropertyName annotation = obj.getAnnotation(PropertyName.class); 657 | return annotation.value(); 658 | } 659 | 660 | return null; 661 | } 662 | 663 | private static String serializedName(String methodName) { 664 | String[] prefixes = new String[] {"get", "set", "is"}; 665 | String methodPrefix = null; 666 | for (String prefix : prefixes) { 667 | if (methodName.startsWith(prefix)) { 668 | methodPrefix = prefix; 669 | } 670 | } 671 | if (methodPrefix == null) { 672 | throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); 673 | } 674 | String strippedName = methodName.substring(methodPrefix.length()); 675 | 676 | // Make sure the first word or upper-case prefix is converted to lower-case 677 | char[] chars = strippedName.toCharArray(); 678 | int pos = 0; 679 | while (pos < chars.length && Character.isUpperCase(chars[pos])) { 680 | chars[pos] = Character.toLowerCase(chars[pos]); 681 | pos++; 682 | } 683 | return new String(chars); 684 | } 685 | 686 | private void addProperty(String property) { 687 | String oldValue = this.properties.put(property.toLowerCase(), property); 688 | if (oldValue != null && !property.equals(oldValue)) { 689 | throw new DatabaseException( 690 | "Found two getters or fields with conflicting case " 691 | + "sensitivity for property: " 692 | + property.toLowerCase()); 693 | } 694 | } 695 | 696 | public T deserialize(Map values) { 697 | return deserialize(values, Collections.>, Type>emptyMap()); 698 | } 699 | 700 | public T deserialize(Map values, Map>, Type> types) { 701 | if (this.constructor == null) { 702 | throw new DatabaseException( 703 | "Class " + this.clazz.getName() + " is missing a constructor with no arguments"); 704 | } 705 | T instance; 706 | try { 707 | instance = this.constructor.newInstance(); 708 | } catch (InstantiationException e) { 709 | throw new RuntimeException(e); 710 | } catch (IllegalAccessException e) { 711 | throw new RuntimeException(e); 712 | } catch (InvocationTargetException e) { 713 | throw new RuntimeException(e); 714 | } 715 | for (Map.Entry entry : values.entrySet()) { 716 | String propertyName = entry.getKey(); 717 | if (this.setters.containsKey(propertyName)) { 718 | Method setter = this.setters.get(propertyName); 719 | Type[] params = setter.getGenericParameterTypes(); 720 | if (params.length != 1) { 721 | throw new IllegalStateException("Setter does not have exactly one " + "parameter"); 722 | } 723 | Type resolvedType = resolveType(params[0], types); 724 | Object value = CustomClassMapper.deserializeToType(entry.getValue(), resolvedType); 725 | try { 726 | setter.invoke(instance, value); 727 | } catch (IllegalAccessException e) { 728 | throw new RuntimeException(e); 729 | } catch (InvocationTargetException e) { 730 | throw new RuntimeException(e); 731 | } 732 | } else if (this.fields.containsKey(propertyName)) { 733 | Field field = this.fields.get(propertyName); 734 | Type resolvedType = resolveType(field.getGenericType(), types); 735 | Object value = CustomClassMapper.deserializeToType(entry.getValue(), resolvedType); 736 | try { 737 | field.set(instance, value); 738 | } catch (IllegalAccessException e) { 739 | throw new RuntimeException(e); 740 | } 741 | } else { 742 | String message = 743 | "No setter/field for " 744 | + propertyName 745 | + " found " 746 | + "on class " 747 | + this.clazz.getName(); 748 | if (this.properties.containsKey(propertyName.toLowerCase())) { 749 | message += " (fields/setters are case sensitive!)"; 750 | } 751 | if (this.throwOnUnknownProperties) { 752 | throw new DatabaseException(message); 753 | } else if (this.warnOnUnknownProperties) { 754 | logger.log(Level.WARNING, message); 755 | } 756 | } 757 | } 758 | return instance; 759 | } 760 | 761 | private Type resolveType(Type type, Map>, Type> types) { 762 | if (type instanceof TypeVariable) { 763 | Type resolvedType = types.get(type); 764 | if (resolvedType == null) { 765 | throw new IllegalStateException("Could not resolve type " + type); 766 | } else { 767 | return resolvedType; 768 | } 769 | } else { 770 | return type; 771 | } 772 | } 773 | 774 | public Map serialize(T object) { 775 | if (!clazz.isAssignableFrom(object.getClass())) { 776 | throw new IllegalArgumentException( 777 | "Can't serialize object of class " 778 | + object.getClass() 779 | + " with BeanMapper for class " 780 | + clazz); 781 | } 782 | Map result = new HashMap<>(); 783 | for (String property : this.properties.values()) { 784 | Object propertyValue; 785 | if (this.getters.containsKey(property)) { 786 | Method getter = this.getters.get(property); 787 | try { 788 | propertyValue = getter.invoke(object); 789 | } catch (IllegalAccessException e) { 790 | throw new RuntimeException(e); 791 | } catch (InvocationTargetException e) { 792 | throw new RuntimeException(e); 793 | } 794 | } else { 795 | // Must be a field 796 | Field field = this.fields.get(property); 797 | if (field == null) { 798 | throw new IllegalStateException("Bean property without field or getter:" + property); 799 | } 800 | try { 801 | propertyValue = field.get(object); 802 | } catch (IllegalAccessException e) { 803 | throw new RuntimeException(e); 804 | } 805 | } 806 | Object serializedValue = CustomClassMapper.serialize(propertyValue); 807 | result.put(property, serializedValue); 808 | } 809 | return result; 810 | } 811 | } 812 | } 813 | --------------------------------------------------------------------------------