diff --git a/build.gradle b/build.gradle index c21a7d2c8..3a0441356 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,9 @@ dependencies { def room_version = "2.5.0" + implementation project(':libs:annotation') + annotationProcessor project(':libs:annotation-processor') + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8' implementation "androidx.room:room-runtime:$room_version" diff --git a/libs/annotation-processor/build.gradle b/libs/annotation-processor/build.gradle new file mode 100644 index 000000000..3bad25d2b --- /dev/null +++ b/libs/annotation-processor/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java-library' +} + +repositories { + google() + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} +dependencies { + + implementation project(':libs:annotation') + + annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5' + compileOnly 'com.google.auto.service:auto-service:1.0-rc5' + +} \ No newline at end of file diff --git a/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java b/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java new file mode 100644 index 000000000..627b0271e --- /dev/null +++ b/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java @@ -0,0 +1,144 @@ +package im.conversations.android.annotation.processor; + +import com.google.auto.service.AutoService; +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.annotation.XmlPackage; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.tools.JavaFileObject; + +@AutoService(Processor.class) +@SupportedSourceVersion(SourceVersion.RELEASE_11) +@SupportedAnnotationTypes("im.conversations.android.annotation.XmlElement") +public class XmlElementProcessor extends AbstractProcessor { + + @Override + public boolean process(Set set, RoundEnvironment roundEnvironment) { + final Set elements = + roundEnvironment.getElementsAnnotatedWith(XmlElement.class); + final ImmutableMap.Builder builder = ImmutableMap.builder(); + for (final Element element : elements) { + if (element instanceof TypeElement) { + final TypeElement typeElement = (TypeElement) element; + final Id id = of(typeElement); + builder.put(id, typeElement.getQualifiedName().toString()); + } + } + final ImmutableMap maps = builder.build(); + if (maps.isEmpty()) { + return false; + } + final JavaFileObject extensionFile; + try { + extensionFile = + processingEnv + .getFiler() + .createSourceFile("im.conversations.android.xmpp.Extensions"); + } catch (final IOException e) { + throw new RuntimeException(e); + } + try (final PrintWriter out = new PrintWriter(extensionFile.openWriter())) { + out.println("package im.conversations.android.xmpp;"); + out.println("import com.google.common.collect.BiMap;"); + out.println("import com.google.common.collect.ImmutableBiMap;"); + out.println("import im.conversations.android.xmpp.ExtensionFactory;"); + out.println("import im.conversations.android.xmpp.model.Extension;"); + out.print("\n"); + out.println("public final class Extensions {"); + out.println( + "public static final BiMap>" + + " EXTENSION_CLASS_MAP;"); + out.println("static {"); + out.println( + "final var builder = new ImmutableBiMap.Builder>();"); + for (final Map.Entry entry : maps.entrySet()) { + Id id = entry.getKey(); + String clazz = entry.getValue(); + out.format( + "builder.put(new ExtensionFactory.Id(\"%s\",\"%s\"),%s.class);", + id.name, id.namespace, clazz); + out.print("\n"); + } + out.println("EXTENSION_CLASS_MAP = builder.build();"); + out.println("}"); + out.println(" private Extensions() {}"); + out.println("}"); + // writing generated file to out … + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + + private static Id of(final TypeElement typeElement) { + final XmlElement xmlElement = typeElement.getAnnotation(XmlElement.class); + PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement(); + XmlPackage xmlPackage = packageElement.getAnnotation(XmlPackage.class); + if (xmlElement == null) { + throw new IllegalStateException( + String.format( + "%s is not annotated as @XmlElement", + typeElement.getQualifiedName().toString())); + } + final String packageNamespace = xmlPackage == null ? null : xmlPackage.namespace(); + final String elementName = xmlElement.name(); + final String elementNamespace = xmlElement.namespace(); + final String namespace; + if (!Strings.isNullOrEmpty(elementNamespace)) { + namespace = elementNamespace; + } else if (!Strings.isNullOrEmpty(packageNamespace)) { + namespace = packageNamespace; + } else { + throw new IllegalStateException( + String.format( + "%s does not declare a namespace", + typeElement.getQualifiedName().toString())); + } + final String name; + if (Strings.isNullOrEmpty(elementName)) { + name = typeElement.getSimpleName().toString().toLowerCase(Locale.ROOT); + } else { + name = elementName; + } + return new Id(name, namespace); + } + + public static class Id { + public final String name; + public final String namespace; + + public Id(String name, String namespace) { + this.name = name; + this.namespace = namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id id = (Id) o; + return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, namespace); + } + } +} diff --git a/libs/annotation/build.gradle b/libs/annotation/build.gradle new file mode 100644 index 000000000..e493c42ff --- /dev/null +++ b/libs/annotation/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'java-library' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 +} \ No newline at end of file diff --git a/src/main/java/im/conversations/android/annotation/XmlElement.java b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java similarity index 90% rename from src/main/java/im/conversations/android/annotation/XmlElement.java rename to libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java index 9f3cbf7c4..68ff73635 100644 --- a/src/main/java/im/conversations/android/annotation/XmlElement.java +++ b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java @@ -5,7 +5,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE}) public @interface XmlElement { diff --git a/src/main/java/im/conversations/android/annotation/XmlPackage.java b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java similarity index 89% rename from src/main/java/im/conversations/android/annotation/XmlPackage.java rename to libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java index 7619ca155..462fc6965 100644 --- a/src/main/java/im/conversations/android/annotation/XmlPackage.java +++ b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java @@ -5,7 +5,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.SOURCE) @Target(ElementType.PACKAGE) public @interface XmlPackage { String namespace(); diff --git a/settings.gradle b/settings.gradle index 4193570fa..a08420a89 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ +include ':libs:annotation' +include ':libs:annotation-processor' rootProject.name = 'Conversations' diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index 10a23d095..33abd7b5e 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -3,7 +3,7 @@ package eu.siacs.conversations.xml; import android.util.Log; import android.util.Xml; import eu.siacs.conversations.Config; -import im.conversations.android.xmpp.Extensions; +import im.conversations.android.xmpp.ExtensionFactory; import im.conversations.android.xmpp.model.Extension; import java.io.Closeable; import java.io.IOException; @@ -106,7 +106,7 @@ public class XmlReader implements Closeable { final var attributes = currentTag.getAttributes(); final var namespace = attributes.get("xmlns"); final var name = currentTag.getName(); - final Element element = Extensions.create(name, namespace); + final Element element = ExtensionFactory.create(name, namespace); element.setAttributes(currentTag.getAttributes()); Tag nextTag = this.readTag(); if (nextTag == null) { diff --git a/src/main/java/im/conversations/android/database/CredentialStore.java b/src/main/java/im/conversations/android/database/CredentialStore.java index 126d76f18..ceaa1ed2b 100644 --- a/src/main/java/im/conversations/android/database/CredentialStore.java +++ b/src/main/java/im/conversations/android/database/CredentialStore.java @@ -201,7 +201,8 @@ public class CredentialStore { return getEncryptedFile(getCredentialStoreFile()); } - private EncryptedFile getEncryptedFile(final File file) throws GeneralSecurityException, IOException { + private EncryptedFile getEncryptedFile(final File file) + throws GeneralSecurityException, IOException { final KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC; final String mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec); return new EncryptedFile.Builder( diff --git a/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java b/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java new file mode 100644 index 000000000..7695aed29 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java @@ -0,0 +1,65 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Objects; +import eu.siacs.conversations.xml.Element; +import im.conversations.android.xmpp.model.Extension; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +public final class ExtensionFactory { + + public static Element create(final String name, final String namespace) { + final Class clazz = of(name, namespace); + if (clazz == null) { + return new Element(name, namespace); + } + final Constructor constructor; + try { + constructor = clazz.getDeclaredConstructor(); + } catch (final NoSuchMethodException e) { + throw new IllegalStateException( + String.format("%s has no default constructor", clazz.getName())); + } + try { + return constructor.newInstance(); + } catch (final IllegalAccessException + | InstantiationException + | InvocationTargetException e) { + throw new IllegalStateException( + String.format("%s has inaccessible default constructor", clazz.getName())); + } + } + + private static Class of(final String name, final String namespace) { + return Extensions.EXTENSION_CLASS_MAP.get(new Id(name, namespace)); + } + + public static Id id(final Class clazz) { + return Extensions.EXTENSION_CLASS_MAP.inverse().get(clazz); + } + + private ExtensionFactory() {} + + public static class Id { + public final String name; + public final String namespace; + + public Id(String name, String namespace) { + this.name = name; + this.namespace = namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id id = (Id) o; + return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, namespace); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Extensions.java b/src/main/java/im/conversations/android/xmpp/Extensions.java deleted file mode 100644 index 689186313..000000000 --- a/src/main/java/im/conversations/android/xmpp/Extensions.java +++ /dev/null @@ -1,135 +0,0 @@ -package im.conversations.android.xmpp; - -import com.google.common.base.Objects; -import com.google.common.base.Strings; -import com.google.common.collect.BiMap; -import com.google.common.collect.ImmutableBiMap; -import eu.siacs.conversations.xml.Element; -import im.conversations.android.annotation.XmlElement; -import im.conversations.android.annotation.XmlPackage; -import im.conversations.android.xmpp.model.Extension; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -public final class Extensions { - - // TODO these two maps can easily be generated by an annotation processor - private static final List> ELEMENTS = - Arrays.asList( - im.conversations.android.xmpp.model.blocking.Item.class, - im.conversations.android.xmpp.model.blocking.Block.class, - im.conversations.android.xmpp.model.blocking.Blocklist.class, - im.conversations.android.xmpp.model.blocking.Unblock.class, - im.conversations.android.xmpp.model.capabilties.LegacyCapabilities.class, - im.conversations.android.xmpp.model.capabilties.Capabilities.class, - im.conversations.android.xmpp.model.data.Data.class, - im.conversations.android.xmpp.model.data.Field.class, - im.conversations.android.xmpp.model.data.Value.class, - im.conversations.android.xmpp.model.disco.info.Feature.class, - im.conversations.android.xmpp.model.disco.info.Identity.class, - im.conversations.android.xmpp.model.disco.info.InfoQuery.class, - im.conversations.android.xmpp.model.disco.items.Item.class, - im.conversations.android.xmpp.model.disco.items.ItemsQuery.class, - im.conversations.android.xmpp.model.roster.Query.class, - im.conversations.android.xmpp.model.roster.Item.class, - im.conversations.android.xmpp.model.streams.Features.class, - im.conversations.android.xmpp.model.Hash.class); - - private static final BiMap> EXTENSION_CLASS_MAP; - - static { - final var builder = new ImmutableBiMap.Builder>(); - for (final Class clazz : ELEMENTS) { - builder.put(of(clazz), clazz); - } - EXTENSION_CLASS_MAP = builder.build(); - } - - private static Id of(final Class clazz) { - final XmlElement xmlElement = clazz.getAnnotation(XmlElement.class); - final Package clazzPackage = clazz.getPackage(); - final XmlPackage xmlPackage = - clazzPackage == null ? null : clazzPackage.getAnnotation(XmlPackage.class); - if (xmlElement == null) { - throw new IllegalStateException( - String.format("%s is not annotated as @XmlElement", clazz.getName())); - } - final String packageNamespace = xmlPackage == null ? null : xmlPackage.namespace(); - final String elementName = xmlElement.name(); - final String elementNamespace = xmlElement.namespace(); - final String namespace; - if (!Strings.isNullOrEmpty(elementNamespace)) { - namespace = elementNamespace; - } else if (!Strings.isNullOrEmpty(packageNamespace)) { - namespace = packageNamespace; - } else { - throw new IllegalStateException( - String.format("%s does not declare a namespace", clazz.getName())); - } - final String name; - if (Strings.isNullOrEmpty(elementName)) { - name = clazz.getSimpleName().toLowerCase(Locale.ROOT); - } else { - name = elementName; - } - return new Id(name, namespace); - } - - public static Element create(final String name, final String namespace) { - final Class clazz = of(name, namespace); - if (clazz == null) { - return new Element(name, namespace); - } - final Constructor constructor; - try { - constructor = clazz.getDeclaredConstructor(); - } catch (final NoSuchMethodException e) { - throw new IllegalStateException( - String.format("%s has no default constructor", clazz.getName())); - } - try { - return constructor.newInstance(); - } catch (final IllegalAccessException - | InstantiationException - | InvocationTargetException e) { - throw new IllegalStateException( - String.format("%s has inaccessible default constructor", clazz.getName())); - } - } - - private static Class of(final String name, final String namespace) { - return EXTENSION_CLASS_MAP.get(new Id(name, namespace)); - } - - public static Id id(final Class clazz) { - return EXTENSION_CLASS_MAP.inverse().get(clazz); - } - - private Extensions() {} - - public static class Id { - public final String name; - public final String namespace; - - public Id(String name, String namespace) { - this.name = name; - this.namespace = namespace; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Id id = (Id) o; - return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace); - } - - @Override - public int hashCode() { - return Objects.hashCode(name, namespace); - } - } -} diff --git a/src/main/java/im/conversations/android/xmpp/model/Extension.java b/src/main/java/im/conversations/android/xmpp/model/Extension.java index f0ed97339..2739e5a0b 100644 --- a/src/main/java/im/conversations/android/xmpp/model/Extension.java +++ b/src/main/java/im/conversations/android/xmpp/model/Extension.java @@ -1,15 +1,15 @@ package im.conversations.android.xmpp.model; import eu.siacs.conversations.xml.Element; -import im.conversations.android.xmpp.Extensions; +import im.conversations.android.xmpp.ExtensionFactory; public class Extension extends Element { - private Extension(final Extensions.Id id) { + private Extension(final ExtensionFactory.Id id) { super(id.name, id.namespace); } public Extension(final Class clazz) { - this(Extensions.id(clazz)); + this(ExtensionFactory.id(clazz)); } }