diff --git a/src/main/java/eu/jonahbauer/android/preference/annotations/Preference.java b/src/main/java/eu/jonahbauer/android/preference/annotations/Preference.java new file mode 100644 index 0000000..77f04f2 --- /dev/null +++ b/src/main/java/eu/jonahbauer/android/preference/annotations/Preference.java @@ -0,0 +1,60 @@ +package eu.jonahbauer.android.preference.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines a preference. The preference key is the value of the string resource + *
{@code R.string.${preference_group_prefix}${name}${preference_group_suffix}}
+ * with {@code ${preference_group_prefix}} and {@code ${preference_group_suffix}} being the + * {@linkplain PreferenceGroup#prefix() prefix} and {@linkplain PreferenceGroup#suffix() suffix} defined + * in the enclosing {@link PreferenceGroup}-Annotation. + *
+ * For each preference with a {@link #type()} other than {@code void.class} a getter + *
{@code public ${type} ${name}()}
+ * and a setter + *
{@code public void ${name}(${type} value)}
+ * will be generated in the preference group. Additionally, a key accessor + *
{@code public String ${name}()}
+ * is generated in the {@code Keys} class of the preference group for each preference (including {@code void}). + * @see Preferences + * @see PreferenceGroup + */ +@Target({}) +@Retention(RetentionPolicy.CLASS) +public @interface Preference { + String NO_DEFAULT_VALUE = "__NO_DEFAULT_VALUE__"; + + /** + * The name of the preference. Must be a valid Java identifier on its own and when combined with + * the preference gropus {@link PreferenceGroup#prefix()} and {@link PreferenceGroup#suffix()}. + */ + String name(); + + /** + * The type of the preference. Must be one of {@code byte.class}, {@code char.class}, {@code short.class}, + * {@code int.class}, {@code long.class}, {@code float.class}, {@code double.class}, {@code boolean.class} + * {@code String.class} or {@code void.class}. + */ + Class type(); + + /** + * The default value for the preference. If no default value is provided the default value will be + * + * This field does not have an effect for {@code void} preferences. If the {@link #type()} is {@code String}, then + * the default value is automatically escaped and quoted, otherwise it will be copied into the generated class + * source code as is. + * @implNote it is possibly to inject code into the generated classes by misusing this field. Just don't. + */ + String defaultValue() default NO_DEFAULT_VALUE; + + /** + * A description that will be used as documentation for the preference accessors in the generated class. + */ + String description() default ""; +} diff --git a/src/main/java/eu/jonahbauer/android/preference/annotations/PreferenceGroup.java b/src/main/java/eu/jonahbauer/android/preference/annotations/PreferenceGroup.java new file mode 100644 index 0000000..5e29327 --- /dev/null +++ b/src/main/java/eu/jonahbauer/android/preference/annotations/PreferenceGroup.java @@ -0,0 +1,40 @@ +package eu.jonahbauer.android.preference.annotations; + +import java.lang.annotation.*; + +/** + * Defines a group of preferences. Every preference group has a name and may contain + * multiple preferences. + *
+ * For each preference group a {@code static} inner class + *
{@code public static final class ${name} {...}}
+ * and an accessor + *
{@code public static ${name} ${name}()}
+ * will be generated in the preferences class. + * @see Preferences + * @see Preference + */ +@Target({}) +@Retention(RetentionPolicy.CLASS) +public @interface PreferenceGroup { + /** + * A prefix that is prepended to the {@linkplain Preference#name() preference name} in order to build the preference key. + */ + String prefix() default ""; + + /** + * A suffix that is appended to the {@linkplain Preference#name() preference name} in order to build the preference key. + */ + String suffix() default ""; + + /** + * The name of the preference group. This is only used as a class name and does not contribute to the preference key. + * Must be a valid Java identifier. + */ + String name(); + + /** + * A list of {@link Preference}s. + */ + Preference[] value(); +} diff --git a/src/main/java/eu/jonahbauer/android/preference/annotations/Preferences.java b/src/main/java/eu/jonahbauer/android/preference/annotations/Preferences.java new file mode 100644 index 0000000..089c9dd --- /dev/null +++ b/src/main/java/eu/jonahbauer/android/preference/annotations/Preferences.java @@ -0,0 +1,117 @@ +package eu.jonahbauer.android.preference.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Defines a set of preferences. A preference class for easier and type-safe access to {@code SharedPreferences} + * is generated from this annotation. + *
+ *

Example

+ * Source Code + *
{@code @Preferences(name = "org.example.AppPreferences$Generated", r = R.class, value = {
+ *     @PreferenceGroup(name = "general", prefix = "preferences_general_", suffix = "_key", value = {
+ *         @Preference(type = byte.class, name = "byte_pref"),
+ *         @Preference(type = short.class, name = "short_pref"),
+ *         @Preference(type = char.class, name = "char_pref"),
+ *         @Preference(type = int.class, name = "int_pref"),
+ *         @Preference(type = long.class, name = "long_pref"),
+ *         @Preference(type = float.class, name = "float_pref"),
+ *         @Preference(type = double.class, name = "double_pref"),
+ *         @Preference(type = String.class, name = "string_pref"),
+ *         @Preference(type = void.class, name = "void_pref")
+ *     }
+ * }
+ * public final AppPreferences extends AppPreferences$Generated {}
+ * }
+ * String Resources + *
{@code 
+ *     ...
+ *     ...
+ *     ...
+ *     ...
+ *     ...
+ *     ...
+ *     ...
+ *     ...
+ * 
+ * }
+ * Generated Code + *
{@code
+ * class AppPreferences$Generated {
+ *     protected AppPreferences$Generated {...} // throws exception
+ *
+ *     public static void init(SharedPreferences sharedPreferences, Resources resources) {...}
+ *
+ *     public static general general() {...}
+ *
+ *     public static final class general {
+ *         private general(Resources resources) {} // private constructor
+ *
+ *         public Keys keys() {}
+ *
+ *         public byte bytePref() {}
+ *         public short shortPref() {}
+ *         public char charPref() {}
+ *         public int intPref() {}
+ *         public long longPref() {}
+ *         public float floatPref() {}
+ *         public double doublePref() {}
+ *         public String stringPref() {}
+ *
+ *         public void bytePref(byte value) {}
+ *         public void shortPref(short value) {}
+ *         public void charPref(char value) {}
+ *         public void intPref(int value) {}
+ *         public void longPref(long value) {}
+ *         public void floatPref(float value) {}
+ *         public void doublePref(double value) {}
+ *         public void stringPref(String value) {}
+ *
+ *         public final class Keys {
+ *             private Keys() {} // private constructor
+ *             public String bytePref() {}
+ *             public String shortPref() {}
+ *             public String charPref() {}
+ *             public String intPref() {}
+ *             public String longPref() {}
+ *             public String floatPref() {}
+ *             public String doublePref() {}
+ *             public String stringPref() {}
+ *         }
+ *     }
+ * }
+ * }
+ */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.CLASS) +public @interface Preferences { + /** + * The fully qualified class name of the generated preferences class. + */ + String name(); + + /** + * A list of {@link PreferenceGroup}s. + */ + PreferenceGroup[] value(); + + /** + * The type of the app's {@code R} class. + */ + Class r(); + + /** + * Whether the generated class should be {@code public final} with a {@code private} constructor, + * or package-private non-{@code final} with a {@code protected} constructor. + *
+ * Having the generated class be non-{@code final} allows the syntax + *
+     * {@code @Preferences(name = "Prefs$Generated", ...)}
+     * {@code public class Prefs extends Prefs$Generated {}}
+ * Either way the generated class will throw an exception on instantiation. + */ + boolean makeFile() default false; +} diff --git a/src/main/java/eu/jonahbauer/android/preference/annotations/processor/PreferenceProcessor.java b/src/main/java/eu/jonahbauer/android/preference/annotations/processor/PreferenceProcessor.java new file mode 100644 index 0000000..a677de9 --- /dev/null +++ b/src/main/java/eu/jonahbauer/android/preference/annotations/processor/PreferenceProcessor.java @@ -0,0 +1,336 @@ +package eu.jonahbauer.android.preference.annotations.processor; + +import eu.jonahbauer.android.preference.annotations.Preference; +import eu.jonahbauer.android.preference.annotations.Preferences; +import eu.jonahbauer.android.preference.annotations.PreferenceGroup; + +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.MirroredTypeException; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Set; +import java.util.function.Function; + +@SupportedAnnotationTypes({ + "eu.jonahbauer.android.preference.annotations.Preferences" +}) +@SupportedSourceVersion(SourceVersion.RELEASE_11) +public final class PreferenceProcessor extends AbstractProcessor { + private static final String PACKAGE_DECLARATION = "package %s ;%n"; + private static final String IMPORT_DECLARATION = "import android.content.SharedPreferences;%n" + + "import android.content.res.Resources;%n" + + "import java.util.Objects;%n" + + "import %s;%n"; + private static final String CLASS_DECLARATION_START = "class %1$s {%n" + + " protected %1$s() {%n" + + " throw new IllegalStateException(\"This class is not supposed to be instantiated.\");%n" + + " }%n"; + private static final String CLASS_DECLARATION_START_FINAL = "public final class %1$s {%n" + + " private %1$s() {%n" + + " throw new IllegalStateException(\"This class is not supposed to be instantiated.\");%n" + + " }%n"; + private static final String CLASS_PREFERENCES_DECLARATION = " private static SharedPreferences sharedPreferences;%n"; + private static final String GROUP_FIELD_DECLARATION = " private static %1$s group$%2$d;%n"; + private static final String CLASS_INITIALIZER_START = " /**%n" + + " * Initialize this preference class to use the given {@link SharedPreferences}.%n" + + " * This function is supposed to be called from the applications {@code onCreate()} method.%n" + + " * @param pSharedPreferences the {@link SharedPreferences} to be used. Not {@code null}.%n" + + " * @param pResources the {@link Resources} from which the preference keys should be loaded. Not {@code null}.%n" + + " * @throws IllegalStateException if this preference class has already been initialized.%n" + + " */%n" + + " public static void init(SharedPreferences pSharedPreferences, Resources pResources) {%n" + + " if (sharedPreferences != null) {%n" + + " throw new IllegalStateException(\"Preferences have already been initialized.\");%n" + + " }%n" + + " Objects.requireNonNull(pSharedPreferences, \"SharedPreferences must not be null.\");%n" + + " Objects.requireNonNull(pResources, \"Resources must not be null.\");%n" + + " sharedPreferences = pSharedPreferences;%n"; + private static final String CLASS_INITIALIZER_FIELD = " group$%2$d = new %1$s(pResources);%n"; + private static final String CLASS_INITIALIZER_END = " }%n"; + private static final String GROUP_ACCESSOR_DECLARATION = " public static %1$s %1$s() {%n" + + " if (sharedPreferences == null) {%n" + + " throw new IllegalStateException(\"Preferences have not yet been initialized.\");%n" + + " }%n" + + " return group$%2$d;%n" + + " }%n"; + private static final String GROUP_CLASS_DECLARATION_START = " public static final class %s {%n"; + private static final String PROPERTY_KEY = " private final String key$%d;%n"; + private static final String GROUP_CLASS_CONSTRUCTOR_START = " private %s(Resources resources) {%n"; + private static final String PROPERTY_KEY_INITIALIZER = " key$%d = resources.getString(R.string.%s);%n"; + private static final String GROUP_CLASS_CONSTRUCTOR_END = " }%n"; + private static final String PROPERTY_DOCUMENTATION = " /**%n" + + " * %s%n" + + " * (default: {@code %s})%n" + + " */%n"; + private static final String PROPERTY_GETTER_START = " public %s %s() {%n"; + private static final String PROPERTY_GETTER_BODY_BOOLEAN = " return sharedPreferences.getBoolean(key$%d, %s);%n"; + private static final String PROPERTY_GETTER_BODY_INTEGER = " return sharedPreferences.getInt(key$%d, %s);%n"; + private static final String PROPERTY_GETTER_BODY_BYTE = " return (byte) sharedPreferences.getInt(key$%d, %s);%n"; + private static final String PROPERTY_GETTER_BODY_SHORT = " return (short) sharedPreferences.getInt(key$%d, %s);%n"; + private static final String PROPERTY_GETTER_BODY_CHAR = " return (char) sharedPreferences.getInt(key$%d, %s);%n"; + private static final String PROPERTY_GETTER_BODY_LONG = " return sharedPreferences.getLong(key$%d, %s);%n"; + private static final String PROPERTY_GETTER_BODY_FLOAT = " return sharedPreferences.getFloat(key$%d, %s);%n"; + private static final String PROPERTY_GETTER_BODY_DOUBLE = " return Double.longBitsToDouble(sharedPreferences.getLong(key$%d, %s));%n"; + private static final String PROPERTY_GETTER_BODY_STRING = " return sharedPreferences.getString(key$%d, %s);%n"; + private static final String PROPERTY_GETTER_END = " }%n"; + private static final String PROPERTY_SETTER_START = " public void %2$s(%1$s value) {%n"; + private static final String PROPERTY_SETTER_BODY_BOOLEAN = " sharedPreferences.edit().putBoolean(key$%d, value).apply();%n"; + private static final String PROPERTY_SETTER_BODY_INTEGER = " sharedPreferences.edit().putInt(key$%d, value).apply();%n"; + private static final String PROPERTY_SETTER_BODY_BYTE = " sharedPreferences.edit().putInt(key$%d, (int) value).apply();%n"; + private static final String PROPERTY_SETTER_BODY_SHORT = " sharedPreferences.edit().putInt(key$%d, (int) value).apply();%n"; + private static final String PROPERTY_SETTER_BODY_CHAR = " sharedPreferences.edit().putInt(key$%d, (int) value).apply();%n"; + private static final String PROPERTY_SETTER_BODY_LONG = " sharedPreferences.edit().putLong(key$%d, value).apply();%n"; + private static final String PROPERTY_SETTER_BODY_FLOAT = " sharedPreferences.edit().putFloat(key$%d, value).apply();%n"; + private static final String PROPERTY_SETTER_BODY_DOUBLE = " sharedPreferences.edit().putLong(key$%d, Double.doubleToRawLongBits(value)).apply();%n"; + private static final String PROPERTY_SETTER_BODY_STRING = " sharedPreferences.edit().putString(key$%d, value).apply();%n"; + private static final String PROPERTY_SETTER_END = " }%n"; + private static final String KEY_CLASS_FIELD_DECLARATION = " private final Keys keys = new Keys();%n"; + private static final String KEY_CLASS_ACCESSOR = " public Keys keys() {%n" + + " return keys;%n" + + " }%n"; + private static final String KEY_CLASS_DECLARATION_START = " public final class Keys {%n" + + " private Keys() {}%n"; + private static final String KEY_CLASS_PROPERTY_ACCESSOR = " public String %s() {%n" + + " return key$%d;%n" + + " }%n"; + private static final String KEY_CLASS_DECLARATION_END = " }%n"; + private static final String GROUP_CLASS_DECLARATION_END = " }%n"; + private static final String CLASS_DECLARATION_END = "}%n"; + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + var clazzes = roundEnv.getElementsAnnotatedWith(Preferences.class); + + try { + for (Element clazz : clazzes) { + var root = clazz.getAnnotation(Preferences.class); + var source = processingEnv.getFiler().createSourceFile(root.name()); + + try (var out = new PrintWriter(source.openWriter())) { + writeRootClass(out, root); + } + } + return true; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void writeRootClass(PrintWriter out, Preferences root) throws IOException { + var groups = root.value(); + var fqcn = root.name(); + if (!StringUtils.isFQCN(root.name())) { + throw new IllegalArgumentException("Illegal preference class name " + root.name() + "."); + } + var pckg = fqcn.lastIndexOf('.') != -1 ? fqcn.substring(0, fqcn.lastIndexOf('.')) : ""; + var cn = fqcn.lastIndexOf('.') != -1 ? fqcn.substring(fqcn.lastIndexOf('.') + 1) : fqcn; + var r = mirror(root, Preferences::r); + + if (!pckg.isEmpty()) out.printf(PACKAGE_DECLARATION, pckg); + out.println(); + out.printf(IMPORT_DECLARATION, r); + out.println(); + if (root.makeFile()) { + out.printf(CLASS_DECLARATION_START_FINAL, cn); + } else { + out.printf(CLASS_DECLARATION_START, cn); + } + + out.printf(CLASS_PREFERENCES_DECLARATION); + for (int i = 0; i < groups.length; i++) { + out.printf(GROUP_FIELD_DECLARATION, groups[i].name(), i); + } + out.println(); + + out.printf(CLASS_INITIALIZER_START); + for (int i = 0; i < groups.length; i++) { + out.printf(CLASS_INITIALIZER_FIELD, groups[i].name(), i); + } + out.printf(CLASS_INITIALIZER_END); + out.println(); + + for (int i = 0; i < groups.length; i++) { + out.printf(GROUP_ACCESSOR_DECLARATION, groups[i].name(), i); + } + out.println(); + + for (PreferenceGroup group : groups) { + if (!StringUtils.isJavaIdentifier(group.name())) { + throw new IllegalArgumentException("Illegal preference group name " + group.name() + "."); + } + if (!group.prefix().isEmpty() && !StringUtils.isJavaIdentifier(group.prefix())) { + throw new IllegalArgumentException("Illegal preference group prefix " + group.prefix() + "."); + } + if (!group.suffix().isEmpty() && !group.suffix().matches("\\p{javaJavaIdentifierPart}*")) { + throw new IllegalArgumentException("Illegal preference group suffix " + group.suffix() + "."); + } + writeGroupClass(out, group); + } + + out.printf(CLASS_DECLARATION_END); + } + + private void writeGroupClass(PrintWriter out, PreferenceGroup group) { + var preferences = group.value(); + + out.printf(GROUP_CLASS_DECLARATION_START, group.name()); + + // Keys + out.printf(KEY_CLASS_FIELD_DECLARATION); + for (int j = 0; j < preferences.length; j++) { + out.printf(PROPERTY_KEY, j); + } + out.println(); + + // Constructor + out.printf(GROUP_CLASS_CONSTRUCTOR_START, group.name()); + for (int i = 0; i < preferences.length; i++) { + if (!StringUtils.isJavaIdentifier(preferences[i].name())) { + throw new IllegalArgumentException("Illegal preference name " + preferences[i].name() + "."); + } + if (!StringUtils.isJavaIdentifier(group.prefix() + preferences[i].name() + group.suffix())) { + throw new IllegalArgumentException("Illegal preference name " + preferences[i].name() + "."); + } + out.printf(PROPERTY_KEY_INITIALIZER, i, group.prefix() + preferences[i].name() + group.suffix()); + } + out.printf(GROUP_CLASS_CONSTRUCTOR_END); + out.println(); + + // Key class accessor + out.printf(KEY_CLASS_ACCESSOR); + out.println(); + + // Getters + for (int j = 0; j < preferences.length; j++) { + writeGetter(out, preferences[j], j); + } + out.println(); + + // Setters + for (int j = 0; j < preferences.length; j++) { + writeSetter(out, preferences[j], j); + } + out.println(); + + // Key class + out.printf(KEY_CLASS_DECLARATION_START); + for (int j = 0; j < preferences.length; j++) { + out.printf(KEY_CLASS_PROPERTY_ACCESSOR, StringUtils.getMethodName(preferences[j].name()), j); + } + out.printf(KEY_CLASS_DECLARATION_END); + out.println(); + + out.printf(GROUP_CLASS_DECLARATION_END); + out.println(); + } + + private static void writeGetter(PrintWriter out, Preference preference, int index) { + var type = mirror(preference, Preference::type); + if (type.getKind() == TypeKind.VOID) return; + + writeDocumentation(out, preference, type); + out.printf(PROPERTY_GETTER_START, type, StringUtils.getMethodName(preference.name())); + switch (type.getKind()) { + case BOOLEAN: + out.printf(PROPERTY_GETTER_BODY_BOOLEAN, index, getDefaultValue(preference, type)); + break; + case BYTE: + out.printf(PROPERTY_GETTER_BODY_BYTE, index, getDefaultValue(preference, type)); + break; + case CHAR: + out.printf(PROPERTY_GETTER_BODY_CHAR, index, getDefaultValue(preference, type)); + break; + case SHORT: + out.printf(PROPERTY_GETTER_BODY_SHORT, index, getDefaultValue(preference, type)); + break; + case INT: + out.printf(PROPERTY_GETTER_BODY_INTEGER, index, getDefaultValue(preference, type)); + break; + case LONG: + out.printf(PROPERTY_GETTER_BODY_LONG, index, getDefaultValue(preference, type)); + break; + case FLOAT: + out.printf(PROPERTY_GETTER_BODY_FLOAT, index, getDefaultValue(preference, type)); + break; + case DOUBLE: + out.printf(PROPERTY_GETTER_BODY_DOUBLE, index, getDefaultValue(preference, type)); + break; + } + if (String.class.getName().equals(type.toString())) { + out.printf(PROPERTY_GETTER_BODY_STRING, index, getDefaultValue(preference, type)); + } + out.printf(PROPERTY_GETTER_END); + } + + private static void writeSetter(PrintWriter out, Preference preference, int index) { + var type = mirror(preference, Preference::type); + if (type.getKind() == TypeKind.VOID) return; + + writeDocumentation(out, preference, type); + out.printf(PROPERTY_SETTER_START, type, StringUtils.getMethodName(preference.name())); + switch (type.getKind()) { + case BOOLEAN: + out.printf(PROPERTY_SETTER_BODY_BOOLEAN, index); + break; + case BYTE: + out.printf(PROPERTY_SETTER_BODY_BYTE, index); + break; + case SHORT: + out.printf(PROPERTY_SETTER_BODY_SHORT, index); + break; + case CHAR: + out.printf(PROPERTY_SETTER_BODY_CHAR, index); + break; + case INT: + out.printf(PROPERTY_SETTER_BODY_INTEGER, index); + break; + case LONG: + out.printf(PROPERTY_SETTER_BODY_LONG, index); + break; + case FLOAT: + out.printf(PROPERTY_SETTER_BODY_FLOAT, index); + break; + case DOUBLE: + out.printf(PROPERTY_SETTER_BODY_DOUBLE, index); + break; + } + if (String.class.getName().equals(type.toString())) { + out.printf(PROPERTY_SETTER_BODY_STRING, index); + } + out.printf(PROPERTY_SETTER_END); + } + + private static void writeDocumentation(PrintWriter out, Preference preference, TypeMirror type) { + if (preference.description().isEmpty()) return; + out.printf(PROPERTY_DOCUMENTATION, preference.description(), getDefaultValue(preference, type)); + } + + private static String getDefaultValue(Preference preference, TypeMirror type) { + if (!Preference.NO_DEFAULT_VALUE.equals(preference.defaultValue())) { + if (String.class.getName().equals(type.toString())) { + return "\"" + StringUtils.escape(preference.defaultValue()) + "\""; + } else { + return preference.defaultValue(); + } + } else switch (type.getKind()) { + case BOOLEAN: return "false"; + case BYTE: case CHAR: case SHORT: case INT: case LONG: case FLOAT: case DOUBLE: return "0"; + case ARRAY: case NULL: case DECLARED: return "null"; + default: throw new RuntimeException(); + } + } + + private static TypeMirror mirror(S object, Function> function) { + try { + function.apply(object); + throw new RuntimeException(); + } catch (MirroredTypeException e) { + return e.getTypeMirror(); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/jonahbauer/android/preference/annotations/processor/StringUtils.java b/src/main/java/eu/jonahbauer/android/preference/annotations/processor/StringUtils.java new file mode 100644 index 0000000..d87357d --- /dev/null +++ b/src/main/java/eu/jonahbauer/android/preference/annotations/processor/StringUtils.java @@ -0,0 +1,54 @@ +package eu.jonahbauer.android.preference.annotations.processor; + +import java.util.Locale; +import java.util.regex.Pattern; + +public final class StringUtils { + private static final Pattern IDENTIFIER = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*"); + private static final Pattern FQCN = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*(\\.\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)*"); + + /** + * Checks whether the given string is an identifier as per JLS 3.8, i.e. it starts with a + * {@linkplain Character#isJavaIdentifierStart(char) Java Letter} and all the remaining characters are + * {@linkplain Character#isJavaIdentifierPart(char) Java Letters or Digits}. + * @param string the string to be checked + */ + static boolean isJavaIdentifier(String string) { + return IDENTIFIER.matcher(string).matches(); + } + + /** + * Checks whether the given string is a fully qualified class name as per JLS 6.7, i.e. a dot-seperated list of + * {@linkplain #isJavaIdentifier(String) identifiers}. + * @param string the string to be checked + */ + static boolean isFQCN(String string) { + return FQCN.matcher(string).matches(); + } + + static String getMethodName(String preferenceName) { + int index; + while ((index = preferenceName.indexOf('_')) != -1) { + if (index == preferenceName.length() - 1) preferenceName = preferenceName.substring(0, index); + preferenceName = preferenceName.substring(0, index) + + preferenceName.substring(index + 1, index + 2).toUpperCase(Locale.ROOT) + + preferenceName.substring(index + 2); + } + return preferenceName; + } + + /** + * Escapes the given string for use in a String literal in Java source code. Note that quotes are not added + * automatically. + */ + static String escape(String string) { + return string.replace("\\", "\\\\") + .replace("\t", "\\t") + .replace("\b", "\\b") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\f", "\\f") + .replace("'", "\\'") + .replace("\"", "\\\""); + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java deleted file mode 100644 index 73a1340..0000000 --- a/src/main/java/module-info.java +++ /dev/null @@ -1,2 +0,0 @@ -module eu.jonahbauer.android.preference.annotations { -} \ No newline at end of file diff --git a/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..6296911 --- /dev/null +++ b/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +eu.jonahbauer.android.preference.annotations.processor.PreferenceProcessor \ No newline at end of file