diff --git a/storebox-harness/src/androidTest/java/net/orange_box/storebox/harness/advanced/FormattedKeysTestCase.java b/storebox-harness/src/androidTest/java/net/orange_box/storebox/harness/advanced/FormattedKeysTestCase.java new file mode 100644 index 0000000..1aefbbb --- /dev/null +++ b/storebox-harness/src/androidTest/java/net/orange_box/storebox/harness/advanced/FormattedKeysTestCase.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015 Martin Bella + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.orange_box.storebox.harness.advanced; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import net.orange_box.storebox.StoreBox; +import net.orange_box.storebox.harness.interfaces.advanced.FormattedKeysInterface; + +public class FormattedKeysTestCase extends InstrumentationTestCase { + + private FormattedKeysInterface uut; + private SharedPreferences prefs; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + uut = StoreBox.create( + getInstrumentation().getTargetContext(), FormattedKeysInterface.class); + + prefs = PreferenceManager.getDefaultSharedPreferences( + getInstrumentation().getTargetContext()); + } + + @SuppressLint("CommitPrefEdits") + @Override + protected void tearDown() throws Exception { + uut = null; + + // we are saving to the actual preferences so let's clear them + prefs.edit().clear().commit(); + prefs = null; + + super.tearDown(); + } + + @SmallTest + public void testInt() { + assertEquals(0, uut.getInt(0)); + assertEquals(0, uut.getInt(1)); + + uut.setInt(0, 1); + uut.setInt(1, 2); + + assertEquals(1, uut.getInt(0)); + assertEquals(2, uut.getInt(1)); + } + + @SmallTest + public void testString() { + assertNull(uut.getString("index_0")); + assertNull(uut.getString("index_1")); + + uut.setString("index_0", "value_1"); + uut.setString("index_1", "value_2"); + + assertEquals("value_1", uut.getString("index_0")); + assertEquals("value_2", uut.getString("index_1")); + } + + @SmallTest + public void testStringByComplexKey() { + assertNull(uut.getStringByComplexKey("prefix_0", 0)); + assertNull(uut.getStringByComplexKey("prefix_0", 1)); + assertNull(uut.getStringByComplexKey("prefix_1", 0)); + assertNull(uut.getStringByComplexKey("prefix_1", 1)); + + assertEquals("string", uut.getStringByComplexKey("prefix_0", 0, "string")); + assertEquals("string", uut.getStringByComplexKey("prefix_0", 1, "string")); + assertEquals("string", uut.getStringByComplexKey("prefix_1", 0, "string")); + assertEquals("string", uut.getStringByComplexKey("prefix_1", 1, "string")); + + uut.setStringByComplexKey("prefix_0", 0, "value_1"); + uut.setStringByComplexKey("prefix_0", 1, "value_2"); + uut.setStringByComplexKey("prefix_1", 0, "value_3"); + uut.setStringByComplexKey("prefix_1", 1, "value_4"); + + assertEquals("value_1", uut.getStringByComplexKey("prefix_0", 0)); + assertEquals("value_2", uut.getStringByComplexKey("prefix_0", 1)); + assertEquals("value_3", uut.getStringByComplexKey("prefix_1", 0)); + assertEquals("value_4", uut.getStringByComplexKey("prefix_1", 1)); + } + +} diff --git a/storebox-harness/src/main/java/net/orange_box/storebox/harness/interfaces/advanced/FormattedKeysInterface.java b/storebox-harness/src/main/java/net/orange_box/storebox/harness/interfaces/advanced/FormattedKeysInterface.java new file mode 100644 index 0000000..b5f6f74 --- /dev/null +++ b/storebox-harness/src/main/java/net/orange_box/storebox/harness/interfaces/advanced/FormattedKeysInterface.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Martin Bella + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.orange_box.storebox.harness.interfaces.advanced; + +import net.orange_box.storebox.annotations.method.KeyByFormattedString; + +public interface FormattedKeysInterface { + + @KeyByFormattedString("int_%d_key") + int getInt(int index); + + @KeyByFormattedString("int_%d_key") + void setInt(int index, int value); + + @KeyByFormattedString("string_%s_key") + String getString(String index); + + @KeyByFormattedString("string_%s_key") + void setString(String index, String value); + + @KeyByFormattedString("%s_string_%d") + String getStringByComplexKey(String prefix, int index); + + @KeyByFormattedString("%s_string_%d") + String getStringByComplexKey(String prefix, int index, String defaultValue); + + @KeyByFormattedString("%s_string_%d") + void setStringByComplexKey(String prefix, int index, String value); + +} diff --git a/storebox-lib/build.gradle b/storebox-lib/build.gradle index 540f3af..655a656 100644 --- a/storebox-lib/build.gradle +++ b/storebox-lib/build.gradle @@ -6,14 +6,14 @@ apply plugin: 'com.jfrog.bintray' android { compileSdkVersion project.compileSdkVersion buildToolsVersion project.buildToolsVersion - + defaultConfig { minSdkVersion project.minSdkVersion targetSdkVersion project.targetSdkVersion - + versionName project.versionName } - + buildTypes { debug { testCoverageEnabled true @@ -24,6 +24,7 @@ android { dependencies { compile 'com.android.support:support-annotations:23.3.0' compile 'net.jodah:typetools:0.4.6' + compile 'org.apache.commons:commons-lang3:3.5' } // FindBugs @@ -71,7 +72,7 @@ install { pom { project { packaging 'jar' - + name 'StroreBox Android Library' description 'Android library for streamlining SharedPreferences.' url 'http://martino2k6.github.io/StoreBox' @@ -127,16 +128,16 @@ bintray { configurations = ['archives'] publish = true - + pkg { repo = 'maven' name = 'storebox' desc = 'Android library for streamlining SharedPreferences.' - + websiteUrl = 'http://martino2k6.github.io/StoreBox' issueTrackerUrl = 'https://github.com/martino2k6/StoreBox/issues' vcsUrl = 'https://github.com/martino2k6/StoreBox.git' - + licenses = ['Apache-2.0'] labels = ['android', 'preferences', 'library', 'jar'] publicDownloadNumbers = true @@ -144,7 +145,7 @@ bintray { version { name = project.versionName vcsTag = project.versionName - + gpg { sign = true passphrase = properties.getProperty('bintray.gpg.password') diff --git a/storebox-lib/src/main/java/net/orange_box/storebox/StoreBoxInvocationHandler.java b/storebox-lib/src/main/java/net/orange_box/storebox/StoreBoxInvocationHandler.java index 38469cb..b4691f7 100644 --- a/storebox-lib/src/main/java/net/orange_box/storebox/StoreBoxInvocationHandler.java +++ b/storebox-lib/src/main/java/net/orange_box/storebox/StoreBoxInvocationHandler.java @@ -29,6 +29,7 @@ import net.orange_box.storebox.annotations.method.DefaultValue; import net.orange_box.storebox.annotations.method.KeyByResource; import net.orange_box.storebox.annotations.method.KeyByString; +import net.orange_box.storebox.annotations.method.KeyByFormattedString; import net.orange_box.storebox.annotations.method.RemoveMethod; import net.orange_box.storebox.annotations.method.TypeAdapter; import net.orange_box.storebox.annotations.method.RegisterChangeListenerMethod; @@ -43,6 +44,8 @@ import net.orange_box.storebox.utils.PreferenceUtils; import net.orange_box.storebox.utils.TypeUtils; +import org.apache.commons.lang3.StringUtils; + import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -65,7 +68,7 @@ class StoreBoxInvocationHandler implements InvocationHandler { private final SharedPreferences prefs; private final SharedPreferences.Editor editor; private final Resources res; - + private final SaveMode saveMode; private final MethodHandler mChangesHandler; @@ -84,20 +87,20 @@ public StoreBoxInvocationHandler( prefs = ((Activity) context).getPreferences( preferencesMode.value()); break; - + case FILE: prefs = context.getSharedPreferences( openNameValue, preferencesMode.value()); break; - + case DEFAULT_SHARED: default: prefs = PreferenceManager.getDefaultSharedPreferences(context); } - + editor = prefs.edit(); res = context.getResources(); - + this.saveMode = saveMode; mChangesHandler = new ChangeListenerMethodHandler(prefs); @@ -113,12 +116,25 @@ public Object invoke( * the SharedPreferences or Editor implementations. */ final String key; + final int numberOfFormattedKeyArgs; final boolean isRemove; final boolean isClear; final boolean isChange; if (method.isAnnotationPresent(KeyByString.class)) { key = method.getAnnotation(KeyByString.class).value(); - + + numberOfFormattedKeyArgs = 0; + isRemove = method.isAnnotationPresent(RemoveMethod.class); + isClear = false; + isChange = MethodUtils.areAnyAnnotationsPresent( + method, + RegisterChangeListenerMethod.class, + UnregisterChangeListenerMethod.class); + } else if (method.isAnnotationPresent(KeyByFormattedString.class)) { + String keyFormat = method.getAnnotation(KeyByFormattedString.class).value(); + numberOfFormattedKeyArgs = StringUtils.countMatches(keyFormat, "%"); + key = String.format(keyFormat, args); + isRemove = method.isAnnotationPresent(RemoveMethod.class); isClear = false; isChange = MethodUtils.areAnyAnnotationsPresent( @@ -128,7 +144,8 @@ public Object invoke( } else if (method.isAnnotationPresent(KeyByResource.class)) { key = res.getString( method.getAnnotation(KeyByResource.class).value()); - + + numberOfFormattedKeyArgs = 0; isRemove = method.isAnnotationPresent(RemoveMethod.class); isClear = false; isChange = MethodUtils.areAnyAnnotationsPresent( @@ -137,13 +154,15 @@ public Object invoke( UnregisterChangeListenerMethod.class); } else if (method.isAnnotationPresent(RemoveMethod.class)) { key = MethodUtils.getKeyForRemove(res, args); - + + numberOfFormattedKeyArgs = 0; isRemove = true; isClear = false; isChange = false; } else if (method.isAnnotationPresent(ClearMethod.class)) { key = null; - + + numberOfFormattedKeyArgs = 0; isRemove = false; isClear = true; isChange = false; @@ -167,13 +186,13 @@ public Object invoke( } catch (NoSuchMethodException e) { // NOP } - + // can we forward the method to the Editor? try { final Method editorMethod = editor.getClass().getDeclaredMethod( method.getName(), method.getParameterTypes()); - + return editorMethod.invoke(editor, args); } catch (NoSuchMethodException e) { // NOP @@ -188,7 +207,7 @@ public Object invoke( KeyByString.class.getSimpleName(), KeyByResource.class.getSimpleName())); } - + /* * Find out based on the method return type whether it's a get or set * operation. We could provide a further annotation for get/set methods, @@ -234,6 +253,7 @@ public Object invoke( final Object defValue = getDefaultValueArg( method, + numberOfFormattedKeyArgs, args); final Object value = PreferenceUtils.getValue( @@ -292,14 +312,15 @@ private int internalHashCode() { private Object getDefaultValueArg( Method method, + int numberOfFormattedKeyArgs, Object... args) { Object result = null; final Class type = TypeUtils.wrapToBoxedType(method.getReturnType()); // parameter default > method-level default - if (args != null && args.length > 0) { - result = args[0]; + if (args != null && args.length > numberOfFormattedKeyArgs) { + result = args[args.length-1]; } if (result == null && method.isAnnotationPresent(DefaultValue.class)) { final TypedValue value = new TypedValue(); @@ -307,7 +328,7 @@ private Object getDefaultValueArg( method.getAnnotation(DefaultValue.class).value(), value, true); - + if (type == Boolean.class) { result = value.data != 0; } else if (type == Float.class) { diff --git a/storebox-lib/src/main/java/net/orange_box/storebox/annotations/method/KeyByFormattedString.java b/storebox-lib/src/main/java/net/orange_box/storebox/annotations/method/KeyByFormattedString.java new file mode 100644 index 0000000..c64058e --- /dev/null +++ b/storebox-lib/src/main/java/net/orange_box/storebox/annotations/method/KeyByFormattedString.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Martin Bella + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.orange_box.storebox.annotations.method; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation which should be used on set/get methods to declare the key formated by String.format() + * Format args must be specified prior to get's default value or set's key + *
+ * Examples usage {@code
+ *
+ *     \u0040KeyByFormattedString("%s_string_key_%d")
+ *     String getString(String prefix, int index, String defaultValue);
+ *
+ *     \u0040KeyByFormattedString("%s_string_key_%d")
+ *     void setString(String prefix, int index, String value);
+ * }
+ * @see KeyByString
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface KeyByFormattedString {
+    
+    String value();
+}
diff --git a/storebox-lib/src/main/java/net/orange_box/storebox/utils/MethodUtils.java b/storebox-lib/src/main/java/net/orange_box/storebox/utils/MethodUtils.java
index 70eb647..3d09a80 100644
--- a/storebox-lib/src/main/java/net/orange_box/storebox/utils/MethodUtils.java
+++ b/storebox-lib/src/main/java/net/orange_box/storebox/utils/MethodUtils.java
@@ -37,7 +37,7 @@ public static > boolean areAnyAnnotationsP
     
     public static String getKeyForRemove(Resources res, Object... args) {
         if (args != null && args.length > 0) {
-            final Object value = args[0];
+            final Object value = args[args.length-1];
             final Class type = TypeUtils.wrapToBoxedType(value.getClass());
             
             if (type == String.class) {
@@ -56,7 +56,7 @@ public static String getKeyForRemove(Resources res, Object... args) {
 
     public static Object getValueArg(Object... args) {
         if (args != null && args.length > 0) {
-            return args[0];
+            return args[args.length-1];
         } else {
             throw new UnsupportedOperationException(
                     "Value argument not found");
@@ -66,7 +66,7 @@ public static Object getValueArg(Object... args) {
     public static Class getValueParameterType(Method method) {
         final Class[] types = method.getParameterTypes();
         if (types != null && types.length > 0) {
-            return TypeUtils.wrapToBoxedType(types[0]);
+            return TypeUtils.wrapToBoxedType(types[types.length-1]);
         } else {
             throw new UnsupportedOperationException(
                     "Value parameter type not found");