├── library ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── cocosw │ │ │ └── favor │ │ │ ├── Commit.java │ │ │ ├── AllFavor.java │ │ │ ├── Default.java │ │ │ ├── Favor.java │ │ │ ├── FavorAdapter.java │ │ │ ├── Taste.java │ │ │ ├── MethodInfo.java │ │ │ └── Types.java │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── cocosw │ │ │ └── favor │ │ │ ├── Image.java │ │ │ ├── Account.java │ │ │ ├── Wrong.java │ │ │ ├── Profile.java │ │ │ └── ApplicationTest.java │ └── test │ │ └── java │ │ └── com │ │ └── cocosw │ │ └── favor │ │ ├── DummyTaste.java │ │ ├── IntTasteTest.java │ │ ├── LongTasteTest.java │ │ ├── FloatTasteTest.java │ │ ├── BoolTasteTest.java │ │ ├── StringTasteTest.java │ │ ├── TasteTest.java │ │ ├── TypesTest.java │ │ └── TypesEqualsTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── CHANGELOG.md ├── .travis.yml ├── gradle.properties ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soarcn/Favor/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | gen 3 | bin 4 | out 5 | *.iml 6 | *.iws 7 | *.ipr 8 | build.xml 9 | target 10 | .idea 11 | .classpath 12 | .project 13 | /res 14 | build 15 | /.gradle 16 | project.properties 17 | release.properties 18 | local.properties 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 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-2.14.1-all.zip 7 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/cocosw/favor/Image.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created by kai on 4/11/2015. 7 | */ 8 | public class Image implements Serializable { 9 | public String url; 10 | public String format; 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | ============ 3 | 4 | 0.2.0 5 | ------- 6 | 7 | - Fix bug: fail to set null to String type preference 8 | - Support String Set preference 9 | - support Serializable preference 10 | 11 | 0.1.0 12 | ------- 13 | 14 | - Better error handling now. 15 | - Favor is ready to go 16 | 17 | 0.0.1 18 | ------- 19 | 20 | - Initial release -------------------------------------------------------------------------------- /library/src/main/java/com/cocosw/favor/Commit.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.METHOD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | *

12 | * Created by kai on 29/09/15. 13 | */ 14 | @Documented 15 | @Target(METHOD) 16 | @Retention(RUNTIME) 17 | public @interface Commit { 18 | } 19 | -------------------------------------------------------------------------------- /library/src/main/java/com/cocosw/favor/AllFavor.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | *

12 | * Created by kai on 30/09/15. 13 | */ 14 | 15 | @Documented 16 | @Target(TYPE) 17 | @Retention(RUNTIME) 18 | public @interface AllFavor { 19 | } 20 | -------------------------------------------------------------------------------- /library/src/main/java/com/cocosw/favor/Default.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.METHOD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | *

12 | * Created by kai on 25/09/15. 13 | */ 14 | @Documented 15 | @Target(METHOD) 16 | @Retention(RUNTIME) 17 | public @interface Default { 18 | String[] value(); 19 | } 20 | -------------------------------------------------------------------------------- /library/src/main/java/com/cocosw/favor/Favor.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.METHOD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | *

12 | * Created by kai on 25/09/15. 13 | */ 14 | 15 | @Documented 16 | @Target(METHOD) 17 | @Retention(RUNTIME) 18 | public @interface Favor { 19 | String value() default ""; 20 | } 21 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/cocosw/favor/Account.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | /** 4 | *

5 | * Created by kai on 30/09/15. 6 | */ 7 | @AllFavor 8 | public interface Account { 9 | 10 | @Default("No Name") 11 | String getUserName(); 12 | 13 | String getPassword(); 14 | 15 | void setPassword(String password); 16 | 17 | @Default("-1") 18 | int getGroupId(); 19 | 20 | @Commit 21 | void setGroupId(int id); 22 | 23 | @Favor("extra") 24 | @Commit 25 | void setExtraPassword(String extra); 26 | } 27 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/cocosw/favor/Wrong.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import com.f2prateek.rx.preferences.Preference; 4 | 5 | /** 6 | * Created by kai on 2/10/15. 7 | */ 8 | @AllFavor 9 | public interface Wrong { 10 | 11 | String testReturnValueForSetter(int value); 12 | 13 | @Default("Hello") 14 | int testWrongDefaultValue(); 15 | 16 | void testUnsupportedType(Profile profile); 17 | 18 | Profile testUnsupportedType(); 19 | 20 | void testTooMuchParameters(int a, int b); 21 | 22 | @Commit 23 | Preference commitForRxPreference(); 24 | 25 | class FavorClz { 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /library/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/kai/Library/Android/sdk/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 | -dontwarn com.cocosw.favor.** { *; } 19 | -keep class com.cocosw.favor.** { *; } -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/DummyTaste.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | /** 6 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 7 | */ 8 | public class DummyTaste extends Taste { 9 | 10 | private SharedPreferences.Editor editor; 11 | 12 | DummyTaste(SharedPreferences sp, String key, String[] defaultValue) { 13 | super(sp, key, defaultValue); 14 | } 15 | 16 | @Override 17 | Object get() { 18 | return "DummyValue"; 19 | } 20 | 21 | @Override 22 | SharedPreferences.Editor editor(Object object) { 23 | return editor; 24 | } 25 | 26 | public SharedPreferences.Editor getEditor() { 27 | return editor; 28 | } 29 | 30 | public void setEditor(SharedPreferences.Editor editor) { 31 | this.editor = editor; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply from: 'https://raw.githubusercontent.com/soarcn/gradle/master/android-library.gradle' 2 | apply from: 'https://raw.githubusercontent.com/soarcn/gradle/master/maven_push.gradle' 3 | 4 | 5 | android { 6 | defaultConfig { 7 | consumerProguardFiles 'proguard-rules.pro' 8 | } 9 | buildTypes { 10 | debug { 11 | // output coverage with ./gradlew clean build createDebugCoverageReport 12 | testCoverageEnabled true 13 | } 14 | } 15 | } 16 | 17 | dependencies { 18 | testCompile 'junit:junit:4.12' 19 | testCompile 'org.mockito:mockito-core:1.10.19' 20 | testCompile 'org.hamcrest:hamcrest-library:1.1' 21 | testCompile 'org.powermock:powermock-api-mockito:1.6.2' 22 | testCompile 'org.powermock:powermock-module-junit4:1.6.2' 23 | testCompile 'com.openpojo:openpojo:0.8.0' 24 | testCompile 'asm:asm:3.3.1' 25 | compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.0' 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | env: 4 | global: 5 | # switch glibc to a memory conserving mode 6 | - MALLOC_ARENA_MAX=2 7 | # wait up to 10 minutes for adb to connect to emulator 8 | - ADB_INSTALL_TIMEOUT=10 9 | 10 | jdk: 11 | - oraclejdk8 12 | 13 | before_install: 14 | - chmod +x gradlew 15 | 16 | android: 17 | components: 18 | - tools 19 | - platform-tools 20 | - build-tools-24.0.2 21 | - android-24 22 | - extra-android-support 23 | - extra-android-m2repository 24 | - extra-google-m2repository 25 | 26 | 27 | script: 28 | - ./gradlew assemble lint 29 | - ./gradlew test 30 | 31 | 32 | #after_script: 33 | # # Emulator Management: Create, Start and Wait 34 | # - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a 35 | # - emulator -avd test -no-skin -no-audio -no-window & 36 | # - android-wait-for-emulator 37 | # - adb shell input keyevent 82 & 38 | # # now run the tests 39 | # - ./gradlew connectedCheck 40 | 41 | cache: 42 | directories: 43 | - $HOME/.m2 -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/IntTasteTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mockito; 8 | 9 | import static org.junit.Assert.*; 10 | 11 | /** 12 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 13 | */ 14 | public class IntTasteTest { 15 | 16 | private SharedPreferences sharedPreferences; 17 | 18 | private final String[] defaultValues = {"10", "100"}; 19 | 20 | private SharedPreferences.Editor editor; 21 | 22 | private Taste.IntTaste intTaste; 23 | 24 | @Before 25 | public void setUp() throws Exception { 26 | sharedPreferences = Mockito.mock(SharedPreferences.class); 27 | Mockito.when(sharedPreferences.getInt("key", 10)).thenReturn(100); 28 | editor = Mockito.mock(SharedPreferences.Editor.class); 29 | intTaste = new Taste.IntTaste(sharedPreferences, "key", defaultValues); 30 | } 31 | 32 | @Test 33 | public void testGet() throws Exception { 34 | //when 35 | Object getResult = intTaste.get(); 36 | 37 | //then 38 | assertEquals("get() must return 100 as configured in sharedPreferences", 100, getResult); 39 | } 40 | 41 | @Test 42 | public void testEditor() throws Exception { 43 | //given 44 | Mockito.when(sharedPreferences.edit()).thenReturn(editor); 45 | 46 | //when 47 | intTaste.editor(100); 48 | 49 | //then 50 | Mockito.verify(editor).putInt("key", 100); 51 | } 52 | } -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/LongTasteTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mockito; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | /** 12 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 13 | */ 14 | public class LongTasteTest { 15 | 16 | private SharedPreferences sharedPreferences; 17 | 18 | private final String[] defaultValues = {"10", "100"}; 19 | 20 | private SharedPreferences.Editor editor; 21 | 22 | private Taste.LongTaste LongTaste; 23 | 24 | @Before 25 | public void setUp() throws Exception { 26 | sharedPreferences = Mockito.mock(SharedPreferences.class); 27 | Mockito.when(sharedPreferences.getLong("key", 10l)).thenReturn(100l); 28 | editor = Mockito.mock(SharedPreferences.Editor.class); 29 | LongTaste = new Taste.LongTaste(sharedPreferences, "key", defaultValues); 30 | } 31 | 32 | @Test 33 | public void testGet() throws Exception { 34 | //when 35 | Object getResult = LongTaste.get(); 36 | 37 | //then 38 | assertEquals("get() must return 100l as configured in sharedPreferences", 100l, getResult); 39 | } 40 | 41 | @Test 42 | public void testEditor() throws Exception { 43 | //given 44 | Mockito.when(sharedPreferences.edit()).thenReturn(editor); 45 | 46 | //when 47 | LongTaste.editor(100l); 48 | 49 | //then 50 | Mockito.verify(editor).putLong("key", 100l); 51 | } 52 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/cocosw/favor/Profile.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | 4 | import com.f2prateek.rx.preferences.Preference; 5 | 6 | import java.util.Set; 7 | 8 | /** 9 | *

10 | * Created by kai on 25/09/15. 11 | */ 12 | 13 | public interface Profile { 14 | 15 | @Favor("city") 16 | @Default("Sydney") 17 | String city(); 18 | 19 | @Favor 20 | String getAddress(); 21 | 22 | @Favor 23 | @Commit 24 | void setAddress(String address); 25 | 26 | @Favor 27 | @Default("32.5") 28 | float getAge(); 29 | 30 | @Favor 31 | void setAge(float age); 32 | 33 | @Favor 34 | void isAlive(boolean alive); 35 | 36 | @Favor 37 | @Default("true") 38 | boolean alive(); 39 | 40 | @Favor 41 | @Default("170") 42 | int getHeight(); 43 | 44 | @Favor 45 | void setHeight(int height); 46 | 47 | @Favor 48 | @Default("10000") 49 | long getDistance(); 50 | 51 | @Favor 52 | void setDistance(long distance); 53 | 54 | @Favor 55 | Set hobbies(); 56 | 57 | @Favor 58 | void setHobbies(Set hobbies); 59 | 60 | @Favor("name") 61 | @Default("No Name") 62 | Preference name(); 63 | 64 | @Favor(value = "gender") 65 | Preference gender(); 66 | 67 | @Favor 68 | Preference age(); 69 | 70 | @Favor 71 | Preference height(); 72 | 73 | @Favor 74 | Preference distance(); 75 | 76 | @Favor 77 | Preference avatar(); 78 | 79 | @Favor 80 | Image image(); 81 | 82 | @Favor 83 | void setImage(Image image); 84 | 85 | } 86 | -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/FloatTasteTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mockito; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | /** 12 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 13 | */ 14 | public class FloatTasteTest { 15 | 16 | private SharedPreferences sharedPreferences; 17 | 18 | private final String[] defaultValues = {"10", "100"}; 19 | 20 | private SharedPreferences.Editor editor; 21 | 22 | private Taste.FloatTaste FloatTaste; 23 | 24 | @Before 25 | public void setUp() throws Exception { 26 | sharedPreferences = Mockito.mock(SharedPreferences.class); 27 | Mockito.when(sharedPreferences.getFloat("key", 10f)).thenReturn(100f); 28 | editor = Mockito.mock(SharedPreferences.Editor.class); 29 | FloatTaste = new Taste.FloatTaste(sharedPreferences, "key", defaultValues); 30 | } 31 | 32 | @Test 33 | public void testGet() throws Exception { 34 | //when 35 | Object getResult = FloatTaste.get(); 36 | 37 | //then 38 | assertEquals("get() must return 100f as configured in sharedPreferences", 100f, getResult); 39 | } 40 | 41 | @Test 42 | public void testEditor() throws Exception { 43 | //given 44 | Mockito.when(sharedPreferences.edit()).thenReturn(editor); 45 | 46 | //when 47 | FloatTaste.editor(100f); 48 | 49 | //then 50 | Mockito.verify(editor).putFloat("key", 100f); 51 | } 52 | } -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/BoolTasteTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mockito; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertTrue; 11 | 12 | /** 13 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 14 | */ 15 | public class BoolTasteTest { 16 | 17 | private SharedPreferences sharedPreferences; 18 | 19 | private final String[] defaultValues = {"true", "false"}; 20 | 21 | private SharedPreferences.Editor editor; 22 | 23 | private Taste.BoolTaste BoolTaste; 24 | 25 | @Before 26 | public void setup() { 27 | sharedPreferences = Mockito.mock(SharedPreferences.class); 28 | Mockito.when(sharedPreferences.getBoolean("key", true)).thenReturn(true); 29 | editor = Mockito.mock(SharedPreferences.Editor.class); 30 | BoolTaste = new Taste.BoolTaste(sharedPreferences, "key", defaultValues); 31 | } 32 | 33 | @Test 34 | public void testGet() throws Exception { 35 | //when 36 | Object getResult = BoolTaste.get(); 37 | 38 | //then 39 | assertEquals("get() must return true as configured in sharedPreferences", true, getResult); 40 | } 41 | 42 | @Test 43 | public void testEditor() throws Exception { 44 | //given 45 | Mockito.when(sharedPreferences.edit()).thenReturn(editor); 46 | 47 | //when 48 | BoolTaste.editor(true); 49 | 50 | //then 51 | Mockito.verify(editor).putBoolean("key", true); 52 | } 53 | } -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/StringTasteTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mockito; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | /** 12 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 13 | */ 14 | public class StringTasteTest { 15 | 16 | private SharedPreferences sharedPreferences; 17 | 18 | private final String[] defaultValues = {"default-value-first", "default-value-second"}; 19 | 20 | private SharedPreferences.Editor editor; 21 | 22 | private Taste.StringTaste stringTaste; 23 | 24 | @Before 25 | public void setup() { 26 | sharedPreferences = Mockito.mock(SharedPreferences.class); 27 | Mockito.when(sharedPreferences.getString("key", "default-value-first")).thenReturn("return-val-for-key-and-valueString"); 28 | editor = Mockito.mock(SharedPreferences.Editor.class); 29 | stringTaste = new Taste.StringTaste(sharedPreferences, "key", defaultValues); 30 | } 31 | 32 | @Test 33 | public void testGet() throws Exception { 34 | //when 35 | Object getResult = stringTaste.get(); 36 | 37 | //then 38 | assertEquals("return-val-for-key-and-valueString", getResult); 39 | } 40 | 41 | @Test 42 | public void testEditor() throws Exception { 43 | //given 44 | Mockito.when(sharedPreferences.edit()).thenReturn(editor); 45 | 46 | //when 47 | stringTaste.editor("some-value"); 48 | 49 | //then 50 | Mockito.verify(editor).putString("key", "some-value"); 51 | } 52 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | 21 | VERSION_NAME=0.1.0 22 | VERSION_CODE=1 23 | MIN_SDK_VERSION=9 24 | TARGET_SDK_VERSION=22 25 | COMPILE_SDK_VERSION=24 26 | BUILD_TOOLS_VERSION=24.0.2 27 | 28 | POM_GROUP_ID=com.cocosw 29 | POM_ARTIFACT_ID=favor 30 | POM_VERSION=0.3.0 31 | POM_NAME=favor 32 | POM_PACKAGING=pom 33 | POM_DESCRIPTION= One of the most easiest way to use sharepreference in android 34 | POM_URL=https://github.com/soarcn/Favor 35 | POM_INCEPTION_YEAR=2015 36 | 37 | POM_SCM_URL=git@github.com:soarcn/Favor.git 38 | POM_SCM_CONNECTION=scm:git:git@github.com:soarcn/Favor.git 39 | POM_SCM_DEV_CONNECTION=scm:git:git@github.com:soarcn/Favor.git 40 | 41 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 42 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 43 | POM_LICENCE_DIST=repoA business-friendly OSS license 44 | POM_LICENCE_COMMENTS= 45 | 46 | POM_DEVELOPER_ID=soarcn 47 | POM_DEVELOPER_NAME=LiaoKai 48 | POM_DEVELOPER_EMAIL=soarcn@gmail.com 49 | POM_DEVELOPER_URL=http://www.cocosw.com 50 | 51 | POM_ISSUE_MANAGEMENT_SYSTEM=Github 52 | POM_ISSUE_MANAGEMENT_URL=https://github.com/soarcn/Favor/issues -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/TasteTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.SharedPreferences; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.Mockito; 8 | 9 | import static org.junit.Assert.*; 10 | import static org.mockito.Mockito.mock; 11 | import static org.mockito.Mockito.spy; 12 | import static org.mockito.Mockito.verify; 13 | 14 | /** 15 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 16 | */ 17 | public class TasteTest { 18 | 19 | private SharedPreferences sharedPreferences; 20 | 21 | private final String[] defaultValues = {"default-value-first", "default-value-second"}; 22 | 23 | private SharedPreferences.Editor editor; 24 | 25 | private DummyTaste dummyTaste; 26 | 27 | @Before 28 | public void setup() { 29 | sharedPreferences = mock(SharedPreferences.class); 30 | Mockito.when(sharedPreferences.getString("key", "default-value-first")).thenReturn("return-val-for-key-and-valueString"); 31 | editor = mock(SharedPreferences.Editor.class); 32 | dummyTaste = new DummyTaste(sharedPreferences, "key", defaultValues); 33 | } 34 | 35 | @Test 36 | public void testSave() throws Exception { 37 | //when 38 | dummyTaste.save(editor, true); 39 | 40 | //then 41 | verify(editor).commit(); 42 | } 43 | 44 | @Test 45 | public void testSet() throws Exception { 46 | //given 47 | dummyTaste.setEditor(editor); 48 | dummyTaste = spy(dummyTaste); 49 | 50 | //when 51 | dummyTaste.set("sample-value"); 52 | 53 | //then 54 | verify(dummyTaste).editor("sample-value"); 55 | verify(editor).apply(); 56 | } 57 | 58 | @Test 59 | public void testCommit() throws Exception { 60 | //given 61 | dummyTaste.setEditor(editor); 62 | dummyTaste = spy(dummyTaste); 63 | 64 | //when 65 | dummyTaste.commit("sample-value"); 66 | 67 | //then 68 | verify(dummyTaste).editor("sample-value"); 69 | verify(dummyTaste).save(editor, true); 70 | } 71 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/TypesTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.powermock.core.classloader.annotations.PrepareForTest; 6 | import org.powermock.modules.junit4.PowerMockRunner; 7 | 8 | import java.lang.reflect.Array; 9 | import java.lang.reflect.GenericArrayType; 10 | import java.lang.reflect.ParameterizedType; 11 | import java.lang.reflect.Type; 12 | import java.lang.reflect.TypeVariable; 13 | import java.lang.reflect.WildcardType; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertTrue; 17 | import static org.junit.Assert.fail; 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.when; 20 | 21 | /** 22 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 23 | */ 24 | @RunWith(PowerMockRunner.class) 25 | @PrepareForTest({Class.class}) 26 | public class TypesTest { 27 | 28 | @Test 29 | public void testGetRawType_ParameterizedType() throws Exception { 30 | //given 31 | ParameterizedType object = mock(ParameterizedType.class); 32 | when(object.getRawType()).thenReturn(ParameterizedType.class); 33 | 34 | //when 35 | Class result = Types.getRawType(object); 36 | 37 | //then 38 | assertEquals("Raw type must be ParameterizedType", ParameterizedType.class, result); 39 | } 40 | 41 | @Test 42 | public void testGetRawType_GenericArrayType() throws Exception { 43 | //given 44 | GenericArrayType object = mock(GenericArrayType.class); 45 | when(object.getGenericComponentType()).thenReturn(GenericArrayType.class); 46 | 47 | //when 48 | Class result = Types.getRawType(object); 49 | 50 | //then 51 | assertEquals("Raw type must be Array's class for GenericArrayType", Array.newInstance(GenericArrayType.class, 0).getClass(), result); 52 | } 53 | 54 | @Test 55 | public void testGetRawType_TypeVariable() throws Exception { 56 | //given 57 | TypeVariable object = mock(TypeVariable.class); 58 | 59 | //when 60 | Class result = Types.getRawType(object); 61 | 62 | //then 63 | assertEquals("Raw type for the TypeVariable must be Object", Object.class, result); 64 | } 65 | 66 | @Test 67 | public void testGetRawType_WildcardType() throws Exception { 68 | //given 69 | WildcardType object = mock(WildcardType.class); 70 | Type[] upperBounds = {ParameterizedType.class, ParameterizedType.class}; 71 | when(object.getUpperBounds()).thenReturn(upperBounds); 72 | 73 | //when 74 | Class result = Types.getRawType(object); 75 | 76 | //then 77 | assertEquals("RawType for WildcardType must be the first element of the upperBounds", 78 | ParameterizedType.class, result); 79 | } 80 | 81 | @Test 82 | public void testGetRawType_Null() throws Exception { 83 | try { 84 | Types.getRawType(null); 85 | fail("An IllegalArgumentException must have occurred as we passed a null object to this method"); 86 | } catch (IllegalArgumentException ex) { 87 | assertTrue(ex.getMessage().startsWith("Expected a Class, ParameterizedType, or GenericArrayType")); 88 | } 89 | } 90 | 91 | @Test 92 | public void testEquals_null_null() throws Exception { 93 | //when 94 | boolean result = Types.equals(null, null); 95 | 96 | //then 97 | assertTrue("equals must return true for null and null values", result); 98 | } 99 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Favor 2 | ======= 3 | [![Build Status](https://travis-ci.org/soarcn/Favor.svg)](https://travis-ci.org/soarcn/Favor) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Favor-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/2695) 4 | 5 | A easy way of using Android SharedPreferences. 6 | 7 | How to use this library 8 | ======= 9 | 10 | - Using Gradle 11 | 12 | ```groovy 13 | compile 'com.cocosw:favor:0.2.0@aar' 14 | ``` 15 | - Using Maven 16 | 17 | ```xml 18 | 19 | com.cocosw 20 | favor 21 | 0.2.0 22 | apklib 23 | 24 | ``` 25 | 26 | API 27 | ======= 28 | 29 | 1 Define a interface. 30 | 31 | ```java 32 | @AllFavor 33 | public interface Account { 34 | @Default("No Name") 35 | String getUserName(); 36 | String setPassword(String password); 37 | } 38 | ``` 39 | 40 | 2 The FavorAdapter class generates an implementation of the interface. 41 | 42 | ```java 43 | account = new FavorAdapter.Builder(getContext()).build().create(Account.class); 44 | account.setPassword("Passw0rd"); 45 | String username = account.getUserName(); 46 | ``` 47 | 48 | API Declaration 49 | ====== 50 | 51 | @AllFavor 52 | ----- 53 | 54 | ```java 55 | @AllFavor 56 | public interface Account { 57 | @Default("No Name") 58 | String getUserName(); 59 | String getPassword(); 60 | } 61 | ``` 62 | 63 | @Favor 64 | ----- 65 | 66 | ```java 67 | @Favor("city") 68 | @Default("Sydney") 69 | String city(); 70 | ``` 71 | 72 | equals 73 | 74 | ```java 75 | PreferenceManager.getDefaultSharedPreferences(context).getString("city","Sydney"); 76 | ``` 77 | 78 | And you can simplify it, Favor will extract the key from the method name 79 | 80 | ```java 81 | @Favor 82 | @Default("Sydney") 83 | String city(); 84 | ``` 85 | 86 | @Default 87 | ------ 88 | 89 | ```java 90 | @Default("18") 91 | int age(); 92 | 93 | @Default("true") 94 | boolean alive(); 95 | ``` 96 | 97 | @Commit 98 | ------ 99 | 100 | By default, Favor will call editor.apply() (>api9), but you can force it to use editor.commit() by @Commit 101 | 102 | ```java 103 | @Favor 104 | @Commit 105 | void setAddress(String address); 106 | ``` 107 | 108 | RxPreference 109 | ------ 110 | 111 | You are a RxJava fan, easy! (rx-preferences dependency is required) 112 | 113 | ```java 114 | @Favor 115 | @Default("No Name") 116 | Preference name(); 117 | ``` 118 | 119 | Advanced usage 120 | ------- 121 | 122 | Favor support put/get all primitive types, including int/long/float/String/bool, String set is also supported for API>=11. 123 | From 0.2.0, Favor (Experimentally) supports Serializable object saving/loading. 124 | 125 | ```java 126 | public class Image implements Serializable { 127 | .... 128 | } 129 | 130 | @Favor 131 | Image image(); 132 | 133 | @Favor 134 | void setImage(Image image); 135 | ``` 136 | 137 | There is one limitation that you can't set default value for Serializable preference item. 138 | 139 | 140 | Proguard 141 | ======= 142 | 143 | ```xml 144 | # Favor 145 | -dontwarn com.cocosw.favor.** { *; } 146 | -keep class com.cocosw.favor.** { *; } 147 | ``` 148 | 149 | Contribute 150 | ======= 151 | 152 | - Feel free to send your pull request to me. 153 | 154 | License 155 | ======= 156 | 157 | Copyright 2011, 2015 Kai Liao 158 | 159 | Licensed under the Apache License, Version 2.0 (the "License"); 160 | you may not use this file except in compliance with the License. 161 | You may obtain a copy of the License at 162 | 163 | http://www.apache.org/licenses/LICENSE-2.0 164 | 165 | Unless required by applicable law or agreed to in writing, software 166 | distributed under the License is distributed on an "AS IS" BASIS, 167 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 168 | See the License for the specific language governing permissions and 169 | limitations under the License. 170 | -------------------------------------------------------------------------------- /library/src/main/java/com/cocosw/favor/FavorAdapter.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | import android.util.Log; 7 | 8 | import java.lang.annotation.Annotation; 9 | import java.lang.reflect.InvocationHandler; 10 | import java.lang.reflect.Method; 11 | import java.lang.reflect.Proxy; 12 | import java.util.LinkedHashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | *

17 | * Created by kai on 25/09/15. 18 | */ 19 | public class FavorAdapter { 20 | 21 | private static final String TAG = "Favor"; 22 | private final SharedPreferences sp; 23 | 24 | private final Map, Map> serviceMethodInfoCache = 25 | new LinkedHashMap<>(); 26 | private String prefix; 27 | private boolean log; 28 | private boolean allFavor; 29 | 30 | 31 | private FavorAdapter(SharedPreferences sp) { 32 | this.sp = sp; 33 | } 34 | 35 | private static MethodInfo getMethodInfo(Map cache, Method method, SharedPreferences sp, String prefix, boolean allFavor) { 36 | synchronized (cache) { 37 | MethodInfo methodInfo = cache.get(method); 38 | if (methodInfo == null) { 39 | methodInfo = new MethodInfo(method, sp, prefix, allFavor); 40 | cache.put(method, methodInfo); 41 | } 42 | return methodInfo; 43 | } 44 | } 45 | 46 | private static void validateServiceClass(Class service) { 47 | if (!service.isInterface()) { 48 | throw new IllegalArgumentException("Only interface definitions are supported."); 49 | } 50 | if (service.getInterfaces().length > 0) { 51 | throw new IllegalArgumentException("Interface definitions must not extend other interfaces."); 52 | } 53 | } 54 | 55 | /** 56 | * Create an implementation of the API defined by the specified {@code service} interface. 57 | */ 58 | @SuppressWarnings("unchecked") 59 | public T create(Class service) { 60 | validateServiceClass(service); 61 | for (Annotation annotation : service.getDeclaredAnnotations()) { 62 | if (annotation.annotationType() == AllFavor.class) 63 | allFavor = true; 64 | } 65 | return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class[]{service}, 66 | new FavorHandler(getMethodInfoCache(service))); 67 | } 68 | 69 | void enableLog(boolean enable) { 70 | this.log = enable; 71 | } 72 | 73 | private Map getMethodInfoCache(Class service) { 74 | synchronized (serviceMethodInfoCache) { 75 | Map methodInfoCache = serviceMethodInfoCache.get(service); 76 | if (methodInfoCache == null) { 77 | methodInfoCache = new LinkedHashMap<>(); 78 | serviceMethodInfoCache.put(service, methodInfoCache); 79 | } 80 | return methodInfoCache; 81 | } 82 | } 83 | 84 | public static class Builder { 85 | 86 | private String prefix; 87 | private SharedPreferences sp; 88 | 89 | public Builder(Context context) { 90 | sp = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); 91 | } 92 | 93 | public Builder(SharedPreferences sp) { 94 | this.sp = sp; 95 | } 96 | 97 | public Builder prefix(String prefix) { 98 | this.prefix = prefix; 99 | return this; 100 | } 101 | 102 | public FavorAdapter build() { 103 | FavorAdapter adapter = new FavorAdapter(sp); 104 | adapter.prefix = prefix; 105 | return adapter; 106 | } 107 | } 108 | 109 | private class FavorHandler implements InvocationHandler { 110 | private final Map methodDetailsCache; 111 | 112 | FavorHandler(Map methodDetailsCache) { 113 | this.methodDetailsCache = methodDetailsCache; 114 | } 115 | 116 | @SuppressWarnings("unchecked") // 117 | @Override 118 | public Object invoke(Object proxy, Method method, final Object[] args) 119 | throws Throwable { 120 | if (method.getDeclaringClass() == Object.class) { 121 | return method.invoke(this, args); 122 | } 123 | 124 | final MethodInfo methodInfo = getMethodInfo(methodDetailsCache, method, sp, prefix, allFavor); 125 | methodInfo.init(); 126 | 127 | if (log) { 128 | Log.d(TAG, methodInfo.toString()); 129 | } 130 | 131 | switch (methodInfo.responseType) { 132 | case VOID: 133 | return methodInfo.set(args); 134 | case OBJECT: 135 | return methodInfo.get(); 136 | case OBSERVABLE: 137 | return methodInfo.rxPref; 138 | } 139 | return null; 140 | } 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /library/src/main/java/com/cocosw/favor/Taste.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.annotation.TargetApi; 5 | import android.content.SharedPreferences; 6 | import android.os.Build; 7 | import android.text.TextUtils; 8 | import android.util.Base64; 9 | 10 | import java.io.ByteArrayInputStream; 11 | import java.io.ByteArrayOutputStream; 12 | import java.io.IOException; 13 | import java.io.ObjectInputStream; 14 | import java.io.ObjectOutputStream; 15 | import java.util.Arrays; 16 | import java.util.HashSet; 17 | import java.util.Set; 18 | 19 | /** 20 | *

21 | * Created by kai on 29/09/15. 22 | */ 23 | 24 | abstract class Taste { 25 | 26 | protected final SharedPreferences sp; 27 | protected final String key; 28 | protected final String[] defaultValue; 29 | 30 | Taste(SharedPreferences sp, String key, String[] defaultValue) { 31 | this.sp = sp; 32 | this.key = key; 33 | this.defaultValue = defaultValue; 34 | } 35 | 36 | @SuppressLint("CommitPrefEdits") 37 | void save(SharedPreferences.Editor editor, boolean commit) { 38 | if (!commit || Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) 39 | editor.apply(); 40 | else 41 | editor.commit(); 42 | } 43 | 44 | void set(Object object) { 45 | save(editor(object), false); 46 | } 47 | 48 | abstract Object get(); 49 | 50 | void commit(Object object) { 51 | save(editor(object), true); 52 | } 53 | 54 | abstract SharedPreferences.Editor editor(Object object); 55 | 56 | static class StringTaste extends Taste { 57 | 58 | StringTaste(SharedPreferences sp, String key, String[] defaultValue) { 59 | super(sp, key, defaultValue); 60 | } 61 | 62 | @Override 63 | public Object get() { 64 | return sp.getString(key, defaultValue[0]); 65 | } 66 | 67 | @SuppressLint("CommitPrefEdits") 68 | @Override 69 | SharedPreferences.Editor editor(Object object) { 70 | return sp.edit().putString(key, (String) object); 71 | } 72 | } 73 | 74 | static class IntTaste extends Taste { 75 | 76 | IntTaste(SharedPreferences sp, String key, String[] defaultValue) { 77 | super(sp, key, defaultValue); 78 | } 79 | 80 | @Override 81 | Object get() { 82 | return sp.getInt(key, defaultValue[0] == null ? 0 : Integer.valueOf(defaultValue[0])); 83 | } 84 | 85 | @SuppressLint("CommitPrefEdits") 86 | @Override 87 | SharedPreferences.Editor editor(Object object) { 88 | return sp.edit().putInt(key, (Integer) object); 89 | } 90 | } 91 | 92 | static class BoolTaste extends Taste { 93 | 94 | BoolTaste(SharedPreferences sp, String key, String[] defaultValue) { 95 | super(sp, key, defaultValue); 96 | } 97 | 98 | @Override 99 | Object get() { 100 | return sp.getBoolean(key, defaultValue[0] == null ? false : Boolean.valueOf(defaultValue[0])); 101 | } 102 | 103 | @SuppressLint("CommitPrefEdits") 104 | @Override 105 | SharedPreferences.Editor editor(Object object) { 106 | return sp.edit().putBoolean(key, (Boolean) object); 107 | } 108 | } 109 | 110 | static class FloatTaste extends Taste { 111 | FloatTaste(SharedPreferences sp, String key, String[] defaultValue) { 112 | super(sp, key, defaultValue); 113 | } 114 | 115 | @Override 116 | Object get() { 117 | return sp.getFloat(key, defaultValue[0] == null ? 0 : Float.valueOf(defaultValue[0])); 118 | } 119 | 120 | @SuppressLint("CommitPrefEdits") 121 | @Override 122 | SharedPreferences.Editor editor(Object object) { 123 | return sp.edit().putFloat(key, (Float) object); 124 | } 125 | } 126 | 127 | static class LongTaste extends Taste { 128 | LongTaste(SharedPreferences sp, String key, String[] defaultValue) { 129 | super(sp, key, defaultValue); 130 | } 131 | 132 | @Override 133 | Object get() { 134 | return sp.getLong(key, defaultValue[0] == null ? 0 : Long.valueOf(defaultValue[0])); 135 | } 136 | 137 | @SuppressLint("CommitPrefEdits") 138 | @Override 139 | SharedPreferences.Editor editor(Object object) { 140 | return sp.edit().putLong(key, (Long) object); 141 | } 142 | } 143 | 144 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 145 | static class StringSetTaste extends Taste { 146 | private final HashSet dv; 147 | 148 | StringSetTaste(SharedPreferences sp, String key, String[] defaultValue) { 149 | super(sp, key, defaultValue); 150 | if (defaultValue != null && defaultValue.length > 0 && defaultValue[0] != null) 151 | dv = new HashSet<>(Arrays.asList(defaultValue)); 152 | else dv = null; 153 | } 154 | 155 | @Override 156 | Object get() { 157 | return sp.getStringSet(key, dv); 158 | } 159 | 160 | @SuppressLint("CommitPrefEdits") 161 | @Override 162 | SharedPreferences.Editor editor(Object object) { 163 | return sp.edit().putStringSet(key, (Set) object); 164 | } 165 | } 166 | 167 | static class SerializableTaste extends Taste { 168 | 169 | 170 | SerializableTaste(SharedPreferences sp, String key, String[] defaultValue) { 171 | super(sp, key, defaultValue); 172 | } 173 | 174 | public static byte[] serialize(Object obj) { 175 | try { 176 | ByteArrayOutputStream b = new ByteArrayOutputStream(); 177 | ObjectOutputStream o = new ObjectOutputStream(b); 178 | o.writeObject(obj); 179 | return b.toByteArray(); 180 | } catch (IOException e) { 181 | e.printStackTrace(); 182 | } 183 | return null; 184 | } 185 | 186 | public static Object deserialize(byte[] bytes) { 187 | try { 188 | ByteArrayInputStream b = new ByteArrayInputStream(bytes); 189 | ObjectInputStream o = new ObjectInputStream(b); 190 | return o.readObject(); 191 | } catch (Exception e) { 192 | e.printStackTrace(); 193 | } 194 | return null; 195 | } 196 | 197 | @Override 198 | Object get() { 199 | String gets = sp.getString(key, null); 200 | if (TextUtils.isEmpty(gets)) 201 | return null; 202 | byte[] bytes = Base64.decode(gets, Base64.DEFAULT); 203 | return deserialize(bytes); 204 | } 205 | 206 | @SuppressLint("CommitPrefEdits") 207 | @Override 208 | SharedPreferences.Editor editor(Object object) { 209 | return put(sp.edit(), object); 210 | } 211 | 212 | @SuppressLint("CommitPrefEdits") 213 | SharedPreferences.Editor put(SharedPreferences.Editor editor, Object object) { 214 | byte[] bytes = serialize(object); 215 | if (bytes != null) 216 | return editor.putString(key, Base64.encodeToString(bytes, Base64.DEFAULT)); 217 | return null; 218 | } 219 | 220 | } 221 | 222 | static class EmptyTaste extends Taste { 223 | 224 | EmptyTaste(SharedPreferences sp, String key, String[] defaultValue) { 225 | super(sp, key, defaultValue); 226 | } 227 | 228 | @Override 229 | Object get() { 230 | return null; 231 | } 232 | 233 | @Override 234 | SharedPreferences.Editor editor(Object object) { 235 | return null; 236 | } 237 | 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /library/src/test/java/com/cocosw/favor/TypesEqualsTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.powermock.api.mockito.PowerMockito; 6 | import org.powermock.core.classloader.annotations.PrepareForTest; 7 | import org.powermock.modules.junit4.PowerMockRunner; 8 | 9 | import java.lang.reflect.GenericArrayType; 10 | import java.lang.reflect.GenericDeclaration; 11 | import java.lang.reflect.ParameterizedType; 12 | import java.lang.reflect.Type; 13 | import java.lang.reflect.TypeVariable; 14 | import java.lang.reflect.WildcardType; 15 | 16 | import static org.junit.Assert.assertFalse; 17 | import static org.junit.Assert.assertTrue; 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.when; 20 | 21 | /** 22 | * @author Mohd Farid mohd.farid@devfactory.com @link mfarid 23 | */ 24 | @RunWith(PowerMockRunner.class) 25 | @PrepareForTest({Class.class, Type.class}) 26 | public class TypesEqualsTest { 27 | 28 | @Test 29 | public void testEquals_null_null() throws Exception { 30 | //when 31 | boolean result = Types.equals(null, null); 32 | 33 | //then 34 | assertTrue("null must be equal to null", result); 35 | } 36 | 37 | @Test 38 | public void testEquals_Class_Class() throws Exception { 39 | //when 40 | boolean result = Types.equals(Integer.class, Long.class); 41 | 42 | //then 43 | assertFalse("Integer class must not be equal to Long", result); 44 | 45 | //when 46 | result = Types.equals(Integer.class, Integer.class); 47 | 48 | //then 49 | assertTrue("Integer class must be equal to itself", result); 50 | 51 | //when 52 | result = Types.equals(Object.class, Object.class); 53 | 54 | //then 55 | assertTrue("Object class must be equal to itself", result); 56 | } 57 | 58 | @Test 59 | public void testEquals_ParameterizedType_and_GenericArrayType() throws Exception { 60 | //when 61 | boolean result = Types.equals(ParameterizedType.class, GenericArrayType.class); 62 | 63 | //then 64 | assertFalse("ParameterizedType and GenericArrayType must not be equal", result); 65 | } 66 | 67 | @Test 68 | public void testEquals_ParameterizedType() throws Exception { 69 | //given 70 | ParameterizedType object1 = mock(ParameterizedType.class); 71 | when(object1.getOwnerType()).thenReturn(ParameterizedType.class); 72 | when(object1.getRawType()).thenReturn(ParameterizedType.class); 73 | Type[] types1 = {ParameterizedType.class, GenericArrayType.class, WildcardType.class}; 74 | when(object1.getActualTypeArguments()).thenReturn(types1); 75 | 76 | ParameterizedType object2 = mock(ParameterizedType.class); 77 | when(object2.getOwnerType()).thenReturn(ParameterizedType.class); 78 | when(object2.getRawType()).thenReturn(ParameterizedType.class); 79 | Type[] types2 = {ParameterizedType.class, GenericArrayType.class, WildcardType.class}; 80 | when(object2.getActualTypeArguments()).thenReturn(types2); 81 | 82 | //when 83 | boolean result = Types.equals(object1, object2); 84 | 85 | //then 86 | assertTrue("Must be true because both both objects have same values for ownerType, rawType and actualTypeArguments", 87 | result); 88 | 89 | //given 90 | when(object1.getOwnerType()).thenReturn(GenericArrayType.class); 91 | 92 | //when 93 | result = Types.equals(object1, object2); 94 | 95 | //then 96 | assertFalse("Must be false because value for ownerType is different for object1 from object2", result); 97 | } 98 | 99 | @Test 100 | public void testEquals_GenericArrayType() throws Exception { 101 | //given 102 | GenericArrayType object1 = mock(GenericArrayType.class); 103 | when(object1.getGenericComponentType()).thenReturn(GenericArrayType.class); 104 | 105 | GenericArrayType object2 = mock(GenericArrayType.class); 106 | when(object2.getGenericComponentType()).thenReturn(GenericArrayType.class); 107 | 108 | //when 109 | boolean result = Types.equals(object1, object2); 110 | 111 | //then 112 | assertTrue("Must be true because objects have same value for getGenericTypeComponent()", result); 113 | 114 | //given 115 | when(object1.getGenericComponentType()).thenReturn(ParameterizedType.class); 116 | 117 | //when 118 | result = Types.equals(object1, object2); 119 | 120 | //then 121 | assertFalse("Must be false because genericTypeComponent() for object1 is different from object2", result); 122 | } 123 | 124 | @Test 125 | public void testEquals_WildcardType() throws Exception { 126 | //given 127 | WildcardType object1 = mock(WildcardType.class); 128 | Type[] upperBounds1 = {Integer.class, Float.class}; 129 | Type[] lowerBounds1 = {String.class, Float.class}; 130 | when(object1.getUpperBounds()).thenReturn(upperBounds1); 131 | when(object1.getLowerBounds()).thenReturn(lowerBounds1); 132 | 133 | WildcardType object2 = mock(WildcardType.class); 134 | Type[] upperBounds2 = {Integer.class, Float.class}; 135 | Type[] lowerBounds2 = {String.class, Float.class}; 136 | when(object2.getUpperBounds()).thenReturn(upperBounds2); 137 | when(object2.getLowerBounds()).thenReturn(lowerBounds2); 138 | 139 | //when 140 | boolean result = Types.equals(object1, object2); 141 | 142 | //then 143 | assertTrue("Must be true because both upperBounds and lowerBounds are set with equal arrays respectively", result); 144 | 145 | //given 146 | Type[] upperBoundNew = {Integer.class, Integer.class}; 147 | when(object1.getUpperBounds()).thenReturn(upperBoundNew); 148 | 149 | //when 150 | result = Types.equals(object1, object2); 151 | 152 | //then 153 | assertFalse("Must be false because upperBounds for object1 is different from object2", result); 154 | } 155 | 156 | 157 | @Test 158 | public void testEquals_TypeVariable() throws Exception { 159 | //given 160 | GenericDeclaration genericDeclaration = mock(GenericDeclaration.class); 161 | 162 | TypeVariable object1 = mock(TypeVariable.class); 163 | when(object1.getGenericDeclaration()).thenReturn(genericDeclaration); 164 | when(object1.getName()).thenReturn("SimpleName"); 165 | 166 | TypeVariable object2 = mock(TypeVariable.class); 167 | when(object2.getGenericDeclaration()).thenReturn(genericDeclaration); 168 | when(object2.getName()).thenReturn("SimpleName"); 169 | 170 | //when 171 | boolean result = Types.equals(object1, object2); 172 | 173 | //then 174 | assertTrue("Must be true because genericDeclaration and name have same values", result); 175 | 176 | //given 177 | 178 | when(object1.getName()).thenReturn("AnotherName"); 179 | 180 | //when 181 | result = Types.equals(object1, object2); 182 | 183 | //then 184 | assertFalse("Must be false because name for object1 is different from object2", result); 185 | } 186 | 187 | @Test 188 | public void testEquals_UnSupportedType() throws Exception { 189 | //given 190 | class MyNewType implements Type { 191 | } 192 | 193 | MyNewType object1 = PowerMockito.mock(MyNewType.class); 194 | MyNewType object2 = PowerMockito.mock(MyNewType.class); 195 | 196 | when(object1.equals(object2)).thenReturn(true); 197 | when(object2.equals(object1)).thenReturn(true); 198 | 199 | assertTrue("Pre-asserting that object1 is equal to object2 through equals check", object1.equals(object2)); 200 | 201 | //when 202 | boolean result = Types.equals(object1, object2); 203 | 204 | //then 205 | assertFalse("Even though object1 and object2 are equal as checked in the pre-assertion, " + 206 | "Result must be false because MyNewType is not a supported type", result); 207 | } 208 | } -------------------------------------------------------------------------------- /library/src/androidTest/java/com/cocosw/favor/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.SharedPreferences; 6 | import android.preference.PreferenceManager; 7 | import android.test.ApplicationTestCase; 8 | 9 | import com.f2prateek.rx.preferences.Preference; 10 | 11 | import junit.framework.Assert; 12 | 13 | import java.util.HashSet; 14 | import java.util.Set; 15 | 16 | import static android.os.SystemClock.sleep; 17 | 18 | /** 19 | * Testing Fundamentals 20 | */ 21 | public class ApplicationTest extends ApplicationTestCase { 22 | 23 | private Profile profile; 24 | 25 | public ApplicationTest() { 26 | super(Application.class); 27 | } 28 | 29 | @Override 30 | protected void setUp() throws Exception { 31 | super.setUp(); 32 | createApplication(); 33 | profile = new FavorAdapter.Builder(getContext()).build().create(Profile.class); 34 | } 35 | 36 | public void testGetValue() { 37 | setSP("city", "Sydney"); 38 | Assert.assertEquals("Sydney", profile.city()); 39 | } 40 | 41 | public void testPutValue() { 42 | profile.setHeight(120); 43 | Assert.assertEquals(120, sp().getInt("height", 0)); 44 | assertEquals(120, profile.getHeight()); 45 | } 46 | 47 | 48 | public void testDefaultValue() { 49 | remove("city"); 50 | Assert.assertEquals("Sydney", profile.city()); 51 | remove("alive"); 52 | Assert.assertEquals(true, profile.alive()); 53 | remove("height"); 54 | Assert.assertEquals(170, profile.getHeight()); 55 | remove("distance"); 56 | Assert.assertEquals(10000, profile.getDistance()); 57 | remove("age"); 58 | Assert.assertEquals(32.5f, profile.getAge()); 59 | } 60 | 61 | public void testLongValue() { 62 | profile.setDistance(1000); 63 | Assert.assertEquals(1000, sp().getLong("distance", 0)); 64 | Assert.assertEquals(1000, profile.getDistance()); 65 | } 66 | 67 | public void testBoolValue() { 68 | remove("alive"); 69 | Assert.assertEquals(true, profile.alive()); 70 | profile.isAlive(false); 71 | Assert.assertEquals(false, profile.alive()); 72 | Assert.assertEquals(false, sp().getBoolean("alive", true)); 73 | } 74 | 75 | public void testFloatValue() { 76 | profile.setAge(10.5f); 77 | Assert.assertEquals(10.5f, sp().getFloat("age", 0)); 78 | assertEquals(10.5f, profile.getAge()); 79 | } 80 | 81 | public void testAllFavor() { 82 | FavorAdapter adapter = new FavorAdapter.Builder(getContext()).build(); 83 | adapter.enableLog(true); 84 | Account account = adapter.create(Account.class); 85 | remove("password"); 86 | remove("username"); 87 | assertEquals("No Name", account.getUserName()); 88 | Assert.assertNull(account.getPassword()); 89 | account.setPassword("Password"); 90 | assertEquals("Password", account.getPassword()); 91 | remove("extra"); 92 | assertEquals(null, sp().getString("extra", null)); 93 | account.setExtraPassword("extra"); 94 | assertEquals("extra", sp().getString("extra", null)); 95 | assertNull(sp().getString("extrapassword", null)); 96 | } 97 | 98 | public void testPrefix() { 99 | Account account = new FavorAdapter.Builder(getContext()).build().create(Account.class); 100 | remove("password"); 101 | assertNull(account.getPassword()); 102 | account.setPassword("Password"); 103 | assertEquals("Password", account.getPassword()); 104 | 105 | remove("_password"); 106 | FavorAdapter favor = new FavorAdapter.Builder(getContext()).prefix("_").build(); 107 | Account account1 = favor.create(Account.class); 108 | assertNull(account1.getPassword()); 109 | account1.setPassword("Password"); 110 | assertEquals("Password", account1.getPassword()); 111 | assertEquals("Password", sp().getString("_password", null)); 112 | } 113 | 114 | public void testRxSharePreference() { 115 | Preference name = profile.name(); 116 | assertNotNull(name); 117 | assertEquals("No Name", name.get()); 118 | Preference gender = profile.gender(); 119 | 120 | remove("gender"); 121 | 122 | assertNotNull(gender); 123 | gender.set(true); 124 | sleep(2000); 125 | assertTrue(gender.get()); 126 | assertTrue(sp().getBoolean("gender", false)); 127 | 128 | gender.delete(); 129 | sleep(2000); 130 | assertNull(gender.get()); 131 | } 132 | 133 | public void testRxSPTypes() { 134 | profile.age().get(); 135 | profile.height().get(); 136 | profile.distance().get(); 137 | } 138 | 139 | 140 | public void testWrongCases() { 141 | final FavorAdapter adapter = new FavorAdapter.Builder(getContext()).build(); 142 | adapter.enableLog(true); 143 | final Wrong wrong = adapter.create(Wrong.class); 144 | 145 | //A warning 146 | assertNotNull(wrong.commitForRxPreference()); 147 | 148 | try { 149 | wrong.testWrongDefaultValue(); 150 | Assert.fail("Fail to check the wrong default value type"); 151 | } catch (Exception e) { 152 | Assert.assertEquals(NumberFormatException.class, e.getClass()); 153 | } 154 | 155 | assertNull(wrong.testReturnValueForSetter(11)); 156 | 157 | assertException(new Runnable() { 158 | @Override 159 | public void run() { 160 | wrong.testUnsupportedType(null); 161 | } 162 | }, "Unsupported type", "Fail to check unsupported value type"); 163 | 164 | assertException(new Runnable() { 165 | @Override 166 | public void run() { 167 | wrong.testUnsupportedType(); 168 | } 169 | }, "Unsupported type", "Fail to check unsupported value type"); 170 | 171 | 172 | assertException(new Runnable() { 173 | @Override 174 | public void run() { 175 | wrong.testUnsupportedType(null); 176 | } 177 | }, "Unsupported type", "Fail to check unsupported value type"); 178 | 179 | assertException(new Runnable() { 180 | @Override 181 | public void run() { 182 | wrong.testTooMuchParameters(1, 2); 183 | } 184 | }, "more than one parameter", "Fail to check redundant parameters"); 185 | 186 | assertException(new Runnable() { 187 | @Override 188 | public void run() { 189 | adapter.create(Wrong.FavorClz.class); 190 | } 191 | }, "interface definitions", "Only interface definitions are supported"); 192 | } 193 | 194 | public void testGivenSharedPreferences() { 195 | SharedPreferences sp = getContext().getSharedPreferences("user", Context.MODE_PRIVATE); 196 | FavorAdapter userAdapter = new FavorAdapter.Builder(sp).build(); 197 | userAdapter.enableLog(true); 198 | profile.setAddress(null); 199 | Profile userprofile = userAdapter.create(Profile.class); 200 | userprofile.setAddress("address"); 201 | assertNull(profile.getAddress()); 202 | assertEquals("address", userprofile.getAddress()); 203 | } 204 | 205 | public void testSetNull() { 206 | profile.setAddress(null); 207 | assertNull(profile.getAddress()); 208 | } 209 | 210 | 211 | private void setSP(String key, String value) { 212 | sp().edit().putString(key, value).commit(); 213 | } 214 | 215 | private void remove(String key) { 216 | sp().edit().remove(key).commit(); 217 | } 218 | 219 | private SharedPreferences sp() { 220 | return PreferenceManager.getDefaultSharedPreferences(getContext()); 221 | } 222 | 223 | 224 | private void assertException(Runnable runnable, String message, String failMsg) { 225 | try { 226 | runnable.run(); 227 | Assert.fail(failMsg); 228 | } catch (Exception e) { 229 | Assert.assertTrue(e.getMessage().contains(message)); 230 | } 231 | } 232 | 233 | public void testStringSet() { 234 | remove("hobbies"); 235 | assertTrue(profile.hobbies() == null); 236 | HashSet hobbies = new HashSet<>(); 237 | hobbies.add("swimming"); 238 | hobbies.add("programming"); 239 | hobbies.add("having sex"); 240 | profile.setHobbies(hobbies); 241 | Set out = profile.hobbies(); 242 | assertEquals(3, out.size()); 243 | assertTrue(out.contains("swimming")); 244 | assertTrue(out.contains("programming")); 245 | assertTrue(out.contains("having sex")); 246 | } 247 | 248 | public void testSerializableObject() { 249 | remove("image"); 250 | assertNull(profile.image()); 251 | Image image = new Image(); 252 | image.url = "http://www.cocosw.com"; 253 | image.format = "jpg"; 254 | profile.setImage(image); 255 | 256 | Image img = profile.image(); 257 | assertNotNull(img); 258 | assertEquals("jpg", img.format); 259 | assertEquals("http://www.cocosw.com", img.url); 260 | } 261 | 262 | public void testRxSerializableObject() { 263 | remove("avatar"); 264 | assertNull(profile.avatar().get()); 265 | Image avatar = new Image(); 266 | avatar.url = "http://www.cocosw.com"; 267 | avatar.format = "png"; 268 | profile.avatar().set(avatar); 269 | 270 | Image img = profile.avatar().get(); 271 | assertNotNull(img); 272 | assertEquals("png", img.format); 273 | assertEquals("http://www.cocosw.com", img.url); 274 | } 275 | 276 | 277 | public void testNegativeNumber() { 278 | FavorAdapter adapter = new FavorAdapter.Builder(getContext()).build(); 279 | adapter.enableLog(true); 280 | Account account = adapter.create(Account.class); 281 | assertEquals(-1, account.getGroupId()); 282 | } 283 | 284 | 285 | } -------------------------------------------------------------------------------- /library/src/main/java/com/cocosw/favor/MethodInfo.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | import android.content.SharedPreferences; 4 | import android.support.annotation.NonNull; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | 8 | import com.f2prateek.rx.preferences.Preference; 9 | import com.f2prateek.rx.preferences.RxSharedPreferences; 10 | 11 | import java.io.Serializable; 12 | import java.lang.annotation.Annotation; 13 | import java.lang.reflect.Method; 14 | import java.lang.reflect.ParameterizedType; 15 | import java.lang.reflect.Type; 16 | import java.lang.reflect.WildcardType; 17 | import java.util.Arrays; 18 | import java.util.Set; 19 | 20 | /** 21 | *

22 | * Created by kai on 25/09/15. 23 | */ 24 | 25 | class MethodInfo { 26 | 27 | static final boolean HAS_RX_JAVA = hasRxJavaOnClasspath(); 28 | private static final String TAG = "Favor"; 29 | final Method method; 30 | // Method-level details 31 | final ResponseType responseType; 32 | final boolean isObservable; 33 | private final SharedPreferences sp; 34 | private final String prefix; 35 | private final boolean allFavor; 36 | boolean loaded = false; 37 | Type responseObjectType; 38 | String key; 39 | String[] defaultValues = new String[1]; 40 | Object rxPref; 41 | private Taste taste; 42 | private boolean commit; 43 | private Type FavorType; 44 | 45 | MethodInfo(Method method, SharedPreferences sp, String prefix, boolean allFavor) { 46 | this.method = method; 47 | this.sp = sp; 48 | this.prefix = prefix; 49 | this.allFavor = allFavor; 50 | responseType = parseResponseType(); 51 | isObservable = (responseType == ResponseType.OBSERVABLE); 52 | } 53 | 54 | private static Type getParameterUpperBound(ParameterizedType type) { 55 | Type[] types = type.getActualTypeArguments(); 56 | for (int i = 0; i < types.length; i++) { 57 | Type paramType = types[i]; 58 | if (paramType instanceof WildcardType) { 59 | types[i] = ((WildcardType) paramType).getUpperBounds()[0]; 60 | } 61 | } 62 | return types[0]; 63 | } 64 | 65 | private static boolean hasRxJavaOnClasspath() { 66 | try { 67 | Class.forName("com.f2prateek.rx.preferences.Preference"); 68 | return true; 69 | } catch (ClassNotFoundException ignored) { 70 | } 71 | return false; 72 | } 73 | 74 | private static void checkDefaultValueType(Type reponse, String[] defaultValues) { 75 | if (reponse == String.class || defaultValues[0] == null) { 76 | } else if (reponse == int.class || reponse == Integer.class) { 77 | Integer.parseInt(defaultValues[0]); 78 | } else if (reponse == long.class || reponse == Long.class) { 79 | Long.parseLong(defaultValues[0]); 80 | } else if (reponse == boolean.class || reponse == Boolean.class) { 81 | Boolean.parseBoolean(defaultValues[0]); 82 | } else if (reponse == float.class || reponse == Float.class) { 83 | Float.parseFloat(defaultValues[0]); 84 | } 85 | } 86 | 87 | private RuntimeException methodError(String message, Object... args) { 88 | if (args.length > 0) { 89 | message = String.format(message, args); 90 | } 91 | return new IllegalArgumentException( 92 | method.getDeclaringClass().getSimpleName() + "." + method.getName() + ": " + message); 93 | } 94 | 95 | private RuntimeException parameterError(int index, String message, Object... args) { 96 | return methodError(message + " (parameter #" + (index + 1) + ")", args); 97 | } 98 | 99 | synchronized void init() { 100 | if (loaded) return; 101 | parseMethodAnnotations(); 102 | loaded = true; 103 | } 104 | 105 | private void parseMethodAnnotations() { 106 | for (Annotation methodAnnotation : method.getAnnotations()) { 107 | Class annotationType = methodAnnotation.annotationType(); 108 | 109 | if (annotationType == Favor.class) { 110 | key = ((Favor) methodAnnotation).value(); 111 | if (key.trim().length() == 0) { 112 | key = getKeyFromMethod(method); 113 | } 114 | if (!TextUtils.isEmpty(prefix)) { 115 | key = prefix + key; 116 | } 117 | } else if (annotationType == Default.class) { 118 | defaultValues = ((Default) methodAnnotation).value(); 119 | } else if (annotationType == Commit.class) { 120 | commit = true; 121 | } 122 | } 123 | 124 | if (allFavor && key == null) { 125 | key = getKeyFromMethod(method); 126 | if (!TextUtils.isEmpty(prefix)) { 127 | key = prefix + key; 128 | } 129 | } 130 | 131 | if (responseType == ResponseType.OBSERVABLE) { 132 | checkDefaultValueType(responseObjectType, defaultValues); 133 | if (commit) { 134 | Log.w(TAG, "@Commit will be ignored for RxReference"); 135 | } 136 | RxSharedPreferences rx = RxSharedPreferences.create(sp); 137 | if (responseObjectType == String.class) { 138 | rxPref = rx.getString(key, defaultValues[0]); 139 | } else if (responseObjectType == Integer.class) { 140 | rxPref = rx.getInteger(key, defaultValues[0] == null ? null : Integer.valueOf(defaultValues[0])); 141 | } else if (responseObjectType == Float.class) { 142 | rxPref = rx.getFloat(key, defaultValues[0] == null ? null : Float.valueOf(defaultValues[0])); 143 | } else if (responseObjectType == Long.class) { 144 | rxPref = rx.getLong(key, defaultValues[0] == null ? null : Long.valueOf(defaultValues[0])); 145 | } else if (responseObjectType == Boolean.class) { 146 | rxPref = rx.getBoolean(key, defaultValues[0] == null ? null : Boolean.valueOf(defaultValues[0])); 147 | } else if (Serializable.class.isAssignableFrom(Types.getRawType(responseObjectType))) { 148 | rxPref = rx.getObject(key, new SerializableAdapter<>()); 149 | } else { 150 | // Class returnTypeClass = Types.getRawType(returnType); 151 | // if (returnTypeClass == Set.class) { 152 | // rxPref = rx.getStringSet(key,new HashSet(defaultValues)) 153 | // } 154 | } 155 | } else { 156 | if (FavorType == String.class) { 157 | taste = new Taste.StringTaste(sp, key, defaultValues); 158 | } else if (FavorType == boolean.class) { 159 | taste = new Taste.BoolTaste(sp, key, defaultValues); 160 | } else if (FavorType == int.class) { 161 | taste = new Taste.IntTaste(sp, key, defaultValues); 162 | } else if (FavorType == float.class) { 163 | taste = new Taste.FloatTaste(sp, key, defaultValues); 164 | } else if (FavorType == long.class) { 165 | taste = new Taste.LongTaste(sp, key, defaultValues); 166 | } else if (Types.getRawType(FavorType) == Set.class) { 167 | taste = new Taste.StringSetTaste(sp, key, defaultValues); 168 | } else if (Serializable.class.isAssignableFrom(Types.getRawType(FavorType))) { 169 | taste = new Taste.SerializableTaste(sp, key, defaultValues); 170 | } else { 171 | taste = new Taste.EmptyTaste(sp, key, defaultValues); 172 | throw methodError("Unsupported type " + FavorType.toString()); 173 | } 174 | } 175 | } 176 | 177 | private String getKeyFromMethod(Method method) { 178 | String value = method.getName().toLowerCase(); 179 | if (value.startsWith("is") && FavorType == boolean.class) return value.substring(2); 180 | if (value.startsWith("get")) return value.substring(3); 181 | if (value.startsWith("set")) return value.substring(3); 182 | return value; 183 | } 184 | 185 | private ResponseType parseResponseType() { 186 | Type returnType = method.getGenericReturnType(); 187 | Type[] parameterTypes = method.getGenericParameterTypes(); 188 | 189 | if (parameterTypes.length > 1) { 190 | throw methodError("%s method has more than one parameter", method.getName()); 191 | } 192 | Type typeToCheck = null; 193 | 194 | if (parameterTypes.length > 0) { 195 | typeToCheck = parameterTypes[0]; 196 | } 197 | 198 | boolean hasReturnType = returnType != void.class; 199 | 200 | if (typeToCheck != null && hasReturnType) { 201 | Log.w(TAG, String.format("Setter method %s should not have return value", method.getName())); 202 | hasReturnType = false; 203 | returnType = void.class; 204 | } 205 | 206 | if (hasReturnType) { 207 | Class rawReturnType = Types.getRawType(returnType); 208 | // if (parameterTypes.length > 0) { 209 | // throw methodError("getter method %s should not have parameter", method.getName()); 210 | // } 211 | 212 | if (HAS_RX_JAVA) { 213 | if (rawReturnType == Preference.class) { 214 | 215 | returnType = Types.getSupertype(returnType, rawReturnType, Preference.class); 216 | responseObjectType = getParameterUpperBound((ParameterizedType) returnType); 217 | return ResponseType.OBSERVABLE; 218 | } 219 | } 220 | responseObjectType = returnType; 221 | } 222 | FavorType = (hasReturnType ? returnType : typeToCheck); 223 | return hasReturnType ? ResponseType.OBJECT : ResponseType.VOID; 224 | } 225 | 226 | Object get() { 227 | if (responseType == ResponseType.OBSERVABLE) 228 | return rxPref; 229 | else 230 | return taste.get(); 231 | } 232 | 233 | Object set(Object[] args) { 234 | if (commit) 235 | taste.commit(args[0]); 236 | else 237 | taste.set(args[0]); 238 | return null; 239 | } 240 | 241 | @Override 242 | public String toString() { 243 | return "MethodInfo{" + 244 | "method=" + method + 245 | ", responseType=" + responseType + 246 | ", isObservable=" + isObservable + 247 | ", sp=" + sp + 248 | ", prefix='" + prefix + '\'' + 249 | ", allFavor=" + allFavor + 250 | ", loaded=" + loaded + 251 | ", responseObjectType=" + responseObjectType + 252 | ", key='" + key + '\'' + 253 | ", defaultValues=" + Arrays.toString(defaultValues) + 254 | ", rxPref=" + rxPref + 255 | ", taste=" + taste + 256 | ", commit=" + commit + 257 | ", FavorType=" + FavorType + 258 | '}'; 259 | } 260 | 261 | enum ResponseType { 262 | VOID, 263 | OBSERVABLE, 264 | OBJECT 265 | } 266 | 267 | private class SerializableAdapter implements Preference.Adapter { 268 | 269 | 270 | @Override 271 | public T get(@NonNull String key, @NonNull SharedPreferences preferences) { 272 | return (T) new Taste.SerializableTaste(preferences, key, null).get(); 273 | } 274 | 275 | @Override 276 | public void set(@NonNull String key, @NonNull T value, @NonNull SharedPreferences.Editor editor) { 277 | new Taste.SerializableTaste(null, key, null).put(editor, value); 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /library/src/main/java/com/cocosw/favor/Types.java: -------------------------------------------------------------------------------- 1 | package com.cocosw.favor; 2 | 3 | /* 4 | * Copyright (C) 2008 Google Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import java.lang.reflect.Array; 20 | import java.lang.reflect.GenericArrayType; 21 | import java.lang.reflect.GenericDeclaration; 22 | import java.lang.reflect.ParameterizedType; 23 | import java.lang.reflect.Type; 24 | import java.lang.reflect.TypeVariable; 25 | import java.lang.reflect.WildcardType; 26 | import java.util.Arrays; 27 | import java.util.NoSuchElementException; 28 | 29 | /** 30 | * Static methods for working with types. 31 | * 32 | * @author Bob Lee 33 | * @author Jesse Wilson 34 | */ 35 | final class Types { 36 | private static final Type[] EMPTY_TYPE_ARRAY = new Type[0]; 37 | 38 | private Types() { 39 | // No instances. 40 | } 41 | 42 | public static Class getRawType(Type type) { 43 | if (type instanceof Class) { 44 | // Type is a normal class. 45 | return (Class) type; 46 | 47 | } else if (type instanceof ParameterizedType) { 48 | ParameterizedType parameterizedType = (ParameterizedType) type; 49 | 50 | // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but 51 | // suspects some pathological case related to nested classes exists. 52 | Type rawType = parameterizedType.getRawType(); 53 | if (!(rawType instanceof Class)) throw new IllegalArgumentException(); 54 | return (Class) rawType; 55 | 56 | } else if (type instanceof GenericArrayType) { 57 | Type componentType = ((GenericArrayType) type).getGenericComponentType(); 58 | return Array.newInstance(getRawType(componentType), 0).getClass(); 59 | 60 | } else if (type instanceof TypeVariable) { 61 | // We could use the variable's bounds, but that won't work if there are multiple. Having a raw 62 | // type that's more general than necessary is okay. 63 | return Object.class; 64 | 65 | } else if (type instanceof WildcardType) { 66 | return getRawType(((WildcardType) type).getUpperBounds()[0]); 67 | 68 | } else { 69 | String className = type == null ? "null" : type.getClass().getName(); 70 | throw new IllegalArgumentException("Expected a Class, ParameterizedType, or " 71 | + "GenericArrayType, but <" + type + "> is of type " + className); 72 | } 73 | } 74 | 75 | /** 76 | * Returns true if {@code a} and {@code b} are equal. 77 | */ 78 | public static boolean equals(Type a, Type b) { 79 | if (a == b) { 80 | return true; // Also handles (a == null && b == null). 81 | 82 | } else if (a instanceof Class) { 83 | return a.equals(b); // Class already specifies equals(). 84 | 85 | } else if (a instanceof ParameterizedType) { 86 | if (!(b instanceof ParameterizedType)) return false; 87 | ParameterizedType pa = (ParameterizedType) a; 88 | ParameterizedType pb = (ParameterizedType) b; 89 | return equal(pa.getOwnerType(), pb.getOwnerType()) 90 | && pa.getRawType().equals(pb.getRawType()) 91 | && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments()); 92 | 93 | } else if (a instanceof GenericArrayType) { 94 | if (!(b instanceof GenericArrayType)) return false; 95 | GenericArrayType ga = (GenericArrayType) a; 96 | GenericArrayType gb = (GenericArrayType) b; 97 | return equals(ga.getGenericComponentType(), gb.getGenericComponentType()); 98 | 99 | } else if (a instanceof WildcardType) { 100 | if (!(b instanceof WildcardType)) return false; 101 | WildcardType wa = (WildcardType) a; 102 | WildcardType wb = (WildcardType) b; 103 | return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds()) 104 | && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds()); 105 | 106 | } else if (a instanceof TypeVariable) { 107 | if (!(b instanceof TypeVariable)) return false; 108 | TypeVariable va = (TypeVariable) a; 109 | TypeVariable vb = (TypeVariable) b; 110 | return va.getGenericDeclaration() == vb.getGenericDeclaration() 111 | && va.getName().equals(vb.getName()); 112 | 113 | } else { 114 | return false; // This isn't a type we support! 115 | } 116 | } 117 | 118 | /** 119 | * Returns the generic supertype for {@code supertype}. For example, given a class {@code 120 | * IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set} and the 121 | * result when the supertype is {@code Collection.class} is {@code Collection}. 122 | */ 123 | static Type getGenericSupertype(Type context, Class rawType, Class toResolve) { 124 | if (toResolve == rawType) return context; 125 | 126 | // We skip searching through interfaces if unknown is an interface. 127 | if (toResolve.isInterface()) { 128 | Class[] interfaces = rawType.getInterfaces(); 129 | for (int i = 0, length = interfaces.length; i < length; i++) { 130 | if (interfaces[i] == toResolve) { 131 | return rawType.getGenericInterfaces()[i]; 132 | } else if (toResolve.isAssignableFrom(interfaces[i])) { 133 | return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve); 134 | } 135 | } 136 | } 137 | 138 | // Check our supertypes. 139 | if (!rawType.isInterface()) { 140 | while (rawType != Object.class) { 141 | Class rawSupertype = rawType.getSuperclass(); 142 | if (rawSupertype == toResolve) { 143 | return rawType.getGenericSuperclass(); 144 | } else if (toResolve.isAssignableFrom(rawSupertype)) { 145 | return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve); 146 | } 147 | rawType = rawSupertype; 148 | } 149 | } 150 | 151 | // We can't resolve this further. 152 | return toResolve; 153 | } 154 | 155 | private static int indexOf(Object[] array, Object toFind) { 156 | for (int i = 0; i < array.length; i++) { 157 | if (toFind.equals(array[i])) return i; 158 | } 159 | throw new NoSuchElementException(); 160 | } 161 | 162 | private static boolean equal(Object a, Object b) { 163 | return a == b || (a != null && a.equals(b)); 164 | } 165 | 166 | private static int hashCodeOrZero(Object o) { 167 | return o != null ? o.hashCode() : 0; 168 | } 169 | 170 | public static String typeToString(Type type) { 171 | return type instanceof Class ? ((Class) type).getName() : type.toString(); 172 | } 173 | 174 | /** 175 | * Returns the generic form of {@code supertype}. For example, if this is {@code 176 | * ArrayList}, this returns {@code Iterable} given the input {@code 177 | * Iterable.class}. 178 | * 179 | * @param supertype a superclass of, or interface implemented by, this. 180 | */ 181 | public static Type getSupertype(Type context, Class contextRawType, Class supertype) { 182 | if (!supertype.isAssignableFrom(contextRawType)) throw new IllegalArgumentException(); 183 | return resolve(context, contextRawType, 184 | getGenericSupertype(context, contextRawType, supertype)); 185 | } 186 | 187 | public static Type resolve(Type context, Class contextRawType, Type toResolve) { 188 | // This implementation is made a little more complicated in an attempt to avoid object-creation. 189 | while (true) { 190 | if (toResolve instanceof TypeVariable) { 191 | TypeVariable typeVariable = (TypeVariable) toResolve; 192 | toResolve = resolveTypeVariable(context, contextRawType, typeVariable); 193 | if (toResolve == typeVariable) { 194 | return toResolve; 195 | } 196 | 197 | } else if (toResolve instanceof Class && ((Class) toResolve).isArray()) { 198 | Class original = (Class) toResolve; 199 | Type componentType = original.getComponentType(); 200 | Type newComponentType = resolve(context, contextRawType, componentType); 201 | return componentType == newComponentType ? original : new GenericArrayTypeImpl( 202 | newComponentType); 203 | 204 | } else if (toResolve instanceof GenericArrayType) { 205 | GenericArrayType original = (GenericArrayType) toResolve; 206 | Type componentType = original.getGenericComponentType(); 207 | Type newComponentType = resolve(context, contextRawType, componentType); 208 | return componentType == newComponentType ? original : new GenericArrayTypeImpl( 209 | newComponentType); 210 | 211 | } else if (toResolve instanceof ParameterizedType) { 212 | ParameterizedType original = (ParameterizedType) toResolve; 213 | Type ownerType = original.getOwnerType(); 214 | Type newOwnerType = resolve(context, contextRawType, ownerType); 215 | boolean changed = newOwnerType != ownerType; 216 | 217 | Type[] args = original.getActualTypeArguments(); 218 | for (int t = 0, length = args.length; t < length; t++) { 219 | Type resolvedTypeArgument = resolve(context, contextRawType, args[t]); 220 | if (resolvedTypeArgument != args[t]) { 221 | if (!changed) { 222 | args = args.clone(); 223 | changed = true; 224 | } 225 | args[t] = resolvedTypeArgument; 226 | } 227 | } 228 | 229 | return changed 230 | ? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args) 231 | : original; 232 | 233 | } else if (toResolve instanceof WildcardType) { 234 | WildcardType original = (WildcardType) toResolve; 235 | Type[] originalLowerBound = original.getLowerBounds(); 236 | Type[] originalUpperBound = original.getUpperBounds(); 237 | 238 | if (originalLowerBound.length == 1) { 239 | Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]); 240 | if (lowerBound != originalLowerBound[0]) { 241 | return new WildcardTypeImpl(new Type[]{Object.class}, new Type[]{lowerBound}); 242 | } 243 | } else if (originalUpperBound.length == 1) { 244 | Type upperBound = resolve(context, contextRawType, originalUpperBound[0]); 245 | if (upperBound != originalUpperBound[0]) { 246 | return new WildcardTypeImpl(new Type[]{upperBound}, EMPTY_TYPE_ARRAY); 247 | } 248 | } 249 | return original; 250 | 251 | } else { 252 | return toResolve; 253 | } 254 | } 255 | } 256 | 257 | private static Type resolveTypeVariable( 258 | Type context, Class contextRawType, TypeVariable unknown) { 259 | Class declaredByRaw = declaringClassOf(unknown); 260 | 261 | // We can't reduce this further. 262 | if (declaredByRaw == null) return unknown; 263 | 264 | Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw); 265 | if (declaredBy instanceof ParameterizedType) { 266 | int index = indexOf(declaredByRaw.getTypeParameters(), unknown); 267 | return ((ParameterizedType) declaredBy).getActualTypeArguments()[index]; 268 | } 269 | 270 | return unknown; 271 | } 272 | 273 | /** 274 | * Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by 275 | * a class. 276 | */ 277 | private static Class declaringClassOf(TypeVariable typeVariable) { 278 | GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration(); 279 | return genericDeclaration instanceof Class ? (Class) genericDeclaration : null; 280 | } 281 | 282 | private static void checkNotPrimitive(Type type) { 283 | if (type instanceof Class && ((Class) type).isPrimitive()) { 284 | throw new IllegalArgumentException(); 285 | } 286 | } 287 | 288 | private static final class ParameterizedTypeImpl implements ParameterizedType { 289 | private final Type ownerType; 290 | private final Type rawType; 291 | private final Type[] typeArguments; 292 | 293 | public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) { 294 | // Require an owner type if the raw type needs it. 295 | if (rawType instanceof Class 296 | && (ownerType == null) != (((Class) rawType).getEnclosingClass() == null)) { 297 | throw new IllegalArgumentException(); 298 | } 299 | 300 | this.ownerType = ownerType; 301 | this.rawType = rawType; 302 | this.typeArguments = typeArguments.clone(); 303 | 304 | for (Type typeArgument : this.typeArguments) { 305 | if (typeArgument == null) throw new NullPointerException(); 306 | checkNotPrimitive(typeArgument); 307 | } 308 | } 309 | 310 | @Override 311 | public Type[] getActualTypeArguments() { 312 | return typeArguments.clone(); 313 | } 314 | 315 | @Override 316 | public Type getRawType() { 317 | return rawType; 318 | } 319 | 320 | @Override 321 | public Type getOwnerType() { 322 | return ownerType; 323 | } 324 | 325 | @Override 326 | public boolean equals(Object other) { 327 | return other instanceof ParameterizedType && Types.equals(this, (ParameterizedType) other); 328 | } 329 | 330 | @Override 331 | public int hashCode() { 332 | return Arrays.hashCode(typeArguments) ^ rawType.hashCode() ^ hashCodeOrZero(ownerType); 333 | } 334 | 335 | @Override 336 | public String toString() { 337 | StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1)); 338 | result.append(typeToString(rawType)); 339 | if (typeArguments.length == 0) return result.toString(); 340 | result.append("<").append(typeToString(typeArguments[0])); 341 | for (int i = 1; i < typeArguments.length; i++) { 342 | result.append(", ").append(typeToString(typeArguments[i])); 343 | } 344 | return result.append(">").toString(); 345 | } 346 | } 347 | 348 | private static final class GenericArrayTypeImpl implements GenericArrayType { 349 | private final Type componentType; 350 | 351 | public GenericArrayTypeImpl(Type componentType) { 352 | this.componentType = componentType; 353 | } 354 | 355 | @Override 356 | public Type getGenericComponentType() { 357 | return componentType; 358 | } 359 | 360 | @Override 361 | public boolean equals(Object o) { 362 | return o instanceof GenericArrayType 363 | && Types.equals(this, (GenericArrayType) o); 364 | } 365 | 366 | @Override 367 | public int hashCode() { 368 | return componentType.hashCode(); 369 | } 370 | 371 | @Override 372 | public String toString() { 373 | return typeToString(componentType) + "[]"; 374 | } 375 | } 376 | 377 | /** 378 | * The WildcardType interface supports multiple upper bounds and multiple 379 | * lower bounds. We only support what the Java 6 language needs - at most one 380 | * bound. If a lower bound is set, the upper bound must be Object.class. 381 | */ 382 | private static final class WildcardTypeImpl implements WildcardType { 383 | private final Type upperBound; 384 | private final Type lowerBound; 385 | 386 | public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) { 387 | if (lowerBounds.length > 1) throw new IllegalArgumentException(); 388 | if (upperBounds.length != 1) throw new IllegalArgumentException(); 389 | 390 | if (lowerBounds.length == 1) { 391 | if (lowerBounds[0] == null) throw new NullPointerException(); 392 | checkNotPrimitive(lowerBounds[0]); 393 | if (upperBounds[0] != Object.class) throw new IllegalArgumentException(); 394 | this.lowerBound = lowerBounds[0]; 395 | this.upperBound = Object.class; 396 | } else { 397 | if (upperBounds[0] == null) throw new NullPointerException(); 398 | checkNotPrimitive(upperBounds[0]); 399 | this.lowerBound = null; 400 | this.upperBound = upperBounds[0]; 401 | } 402 | } 403 | 404 | @Override 405 | public Type[] getUpperBounds() { 406 | return new Type[]{upperBound}; 407 | } 408 | 409 | @Override 410 | public Type[] getLowerBounds() { 411 | return lowerBound != null ? new Type[]{lowerBound} : EMPTY_TYPE_ARRAY; 412 | } 413 | 414 | @Override 415 | public boolean equals(Object other) { 416 | return other instanceof WildcardType && Types.equals(this, (WildcardType) other); 417 | } 418 | 419 | @Override 420 | public int hashCode() { 421 | // This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()). 422 | return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) ^ (31 + upperBound.hashCode()); 423 | } 424 | 425 | @Override 426 | public String toString() { 427 | if (lowerBound != null) return "? super " + typeToString(lowerBound); 428 | if (upperBound == Object.class) return "?"; 429 | return "? extends " + typeToString(upperBound); 430 | } 431 | } 432 | } 433 | --------------------------------------------------------------------------------