Prepare annotation processor

This commit is contained in:
Dimitry Ivanov 2020-06-12 15:47:09 +03:00
parent cd7aae7c9e
commit ba85ea0e98
20 changed files with 338 additions and 90 deletions

View File

@ -1,4 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
@ -13,16 +15,6 @@ android {
versionName version
resConfig 'en'
javaCompileOptions {
annotationProcessorOptions {
arguments = [
// cannot cast GString...
// cannot use `-`
'markwon.samples.file': "\"${projectDir}/samples.json\"".toString()
]
}
}
}
dexOptions {
@ -40,11 +32,17 @@ android {
java.srcDirs += '../sample-utils/annotations'
}
}
}
kapt {
arguments {
arg('markwon.samples.file', "${projectDir}/samples.json".toString())
}
}
dependencies {
kapt project(':sample-utils:processor')
implementation 'io.noties:debug:5.1.0'
annotationProcessor project(':sample-utils:processor')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@ -1,16 +0,0 @@
package io.noties.markwon.app;
import android.app.Application;
import io.noties.debug.AndroidLogDebugOutput;
import io.noties.debug.Debug;
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
Debug.init(new AndroidLogDebugOutput(true));
}
}

View File

@ -0,0 +1,14 @@
package io.noties.markwon.app
import android.app.Application
import io.noties.debug.AndroidLogDebugOutput
import io.noties.debug.Debug
class App : Application() {
override fun onCreate() {
super.onCreate()
Debug.init(AndroidLogDebugOutput(BuildConfig.DEBUG))
}
}

View File

@ -1,16 +0,0 @@
package io.noties.markwon.app;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
public class MainActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

View File

@ -0,0 +1,14 @@
package io.noties.markwon.app
import android.app.Activity
import android.os.Bundle
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/**/
}
}

View File

@ -1,15 +0,0 @@
package io.noties.markwon.app;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSample;
@MarkwonSample(
id = "202006163161416",
title = "The first sample title",
description = "This is description",
artifacts = {MarkwonArtifact.CORE, MarkwonArtifact.EDITOR},
tags = "none"
)
public class Test {
}

View File

@ -1,4 +1,5 @@
buildscript {
ext.kotlin_version = '1.3.72'
repositories {
google()
jcenter()
@ -7,6 +8,7 @@ buildscript {
// on `3.5.3` tests are not run from CLI
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -80,7 +82,10 @@ ext {
'dagger' : "com.google.dagger:dagger:$daggerVersion",
'picasso' : 'com.squareup.picasso:picasso:2.71828',
'glide' : 'com.github.bumptech.glide:glide:4.9.0',
'coil' : 'io.coil-kt:coil:0.10.1'
'coil' : 'io.coil-kt:coil:0.10.1',
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
'gson' : 'com.google.code.gson:gson:2.8.6',
'commons-io' : 'commons-io:commons-io:2.6'
]
deps['annotationProcessor'] = [
@ -91,8 +96,6 @@ ext {
deps['test'] = [
'junit' : 'junit:junit:4.12',
'robolectric' : 'org.robolectric:robolectric:3.8',
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
'commons-io' : 'commons-io:commons-io:2.6',
'mockito' : 'org.mockito:mockito-core:2.21.0',
'commonmark-test-util': "com.atlassian.commonmark:commonmark-test-util:$commonMarkVersion",
]

View File

@ -25,15 +25,14 @@ dependencies {
compileOnly it['x-appcompat']
}
deps['test'].with {
testImplementation project(':markwon-test-span')
testImplementation deps['commons-io']
deps['test'].with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['mockito']
testImplementation it['commons-io']
}
}

View File

@ -17,15 +17,14 @@ dependencies {
api project(':markwon-core')
deps['test'].with {
testImplementation project(':markwon-test-span')
testImplementation deps['commons-io']
deps['test'].with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['mockito']
testImplementation it['commons-io']
}
}

View File

@ -19,6 +19,9 @@ dependencies {
deps.with {
api it['commonmark-strikethrough']
// NB! ix-java dependency to be used in tests
testImplementation it['ix-java']
}
deps.test.with {
@ -26,7 +29,6 @@ dependencies {
testImplementation it['junit']
testImplementation it['mockito']
testImplementation it['robolectric']
testImplementation it['ix-java']
}
}

View File

@ -17,13 +17,13 @@ dependencies {
api project(':markwon-core')
deps['test'].with {
testImplementation project(':markwon-test-span')
testImplementation deps['commons-io']
deps['test'].with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['commons-io']
}
}

View File

@ -23,12 +23,13 @@ dependencies {
// it to be consistent with markdown (please note that we do not use markwon plugin
// for that in case if different implementation is used)
compileOnly it['commonmark-strikethrough']
testImplementation it['ix-java']
}
deps.test.with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['ix-java']
}
}

View File

@ -23,15 +23,14 @@ dependencies {
compileOnly it['okhttp']
}
deps['test'].with {
testImplementation project(':markwon-test-span')
testImplementation deps['commons-io']
deps['test'].with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['mockito']
testImplementation it['commons-io']
}
}

View File

@ -16,9 +16,9 @@ android {
dependencies {
api deps['x-annotations']
api deps['ix-java']
deps['test'].with {
api it['junit']
api it['ix-java']
}
}

View File

@ -7,7 +7,7 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface MarkwonSample {
public @interface MarkwonSampleInfo {
/**
* Actual format is not important, but this key must be set in order to persist sample.
* This key should not change during lifetime of sample

View File

@ -8,5 +8,9 @@ sourceSets {
}
dependencies {
implementation deps['x-annotations']
deps.with {
implementation it['x-annotations']
implementation it['gson']
implementation it['commons-io']
}
}

View File

@ -0,0 +1,22 @@
package io.noties.markwon.sample.processor;
import androidx.annotation.NonNull;
import javax.annotation.processing.Messager;
import javax.tools.Diagnostic;
class Logger {
private final Messager messager;
Logger(@NonNull Messager messager) {
this.messager = messager;
}
void error(@NonNull String message, Object... args) {
messager.printMessage(Diagnostic.Kind.ERROR, "\n[Markwon] " + String.format(message, args) + "\n\u00a0");
}
void info(@NonNull String message, Object... args) {
messager.printMessage(Diagnostic.Kind.NOTE, "\n[Markwon] " + String.format(message, args) + "\n\u00a0");
}
}

View File

@ -0,0 +1,73 @@
package io.noties.markwon.sample.processor;
import androidx.annotation.NonNull;
import java.util.Set;
import io.noties.markwon.sample.annotations.MarkwonArtifact;
@SuppressWarnings("WeakerAccess")
public class MarkwonSample {
// represents full (package + class) name to be use in reflective lookup
final String javaClassName;
final String id;
final String title;
final String description;
final Set<MarkwonArtifact> artifacts;
final Set<String> tags;
public MarkwonSample(
@NonNull String javaClassName,
@NonNull String id,
@NonNull String title,
@NonNull String description,
@NonNull Set<MarkwonArtifact> artifacts,
@NonNull Set<String> tags
) {
this.javaClassName = javaClassName;
this.id = id;
this.title = title;
this.description = description;
this.artifacts = artifacts;
this.tags = tags;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MarkwonSample sample = (MarkwonSample) o;
if (!javaClassName.equals(sample.javaClassName)) return false;
if (!id.equals(sample.id)) return false;
if (!title.equals(sample.title)) return false;
if (!description.equals(sample.description)) return false;
if (!artifacts.equals(sample.artifacts)) return false;
return tags.equals(sample.tags);
}
@Override
public int hashCode() {
int result = javaClassName.hashCode();
result = 31 * result + id.hashCode();
result = 31 * result + title.hashCode();
result = 31 * result + description.hashCode();
result = 31 * result + artifacts.hashCode();
result = 31 * result + tags.hashCode();
return result;
}
@Override
public String toString() {
return "MarkwonSample{" +
"javaClassName='" + javaClassName + '\'' +
", id='" + id + '\'' +
", title='" + title + '\'' +
", description='" + description + '\'' +
", artifacts=" + artifacts +
", tags=" + tags +
'}';
}
}

View File

@ -2,27 +2,50 @@ package io.noties.markwon.sample.processor;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import io.noties.markwon.sample.annotations.MarkwonSample;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
public class MarkwonSampleProcessor extends AbstractProcessor {
private static final String KEY_SAMPLES_FILE = "markwon.samples.file";
private static final DateFormat ID_DATEFORMAT = new SimpleDateFormat("YYYYMMDDHHmmss", Locale.ROOT);
private Messager messager;
private Logger logger;
private String samplesFilePath;
private List<MarkwonSample> samples;
private boolean samplesUpdated;
@Override
public Set<String> getSupportedOptions() {
return Collections.singleton(KEY_SAMPLES_FILE);
@ -35,31 +58,174 @@ public class MarkwonSampleProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(MarkwonSample.class.getName());
return Collections.singleton(MarkwonSampleInfo.class.getName());
}
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
messager = processingEnvironment.getMessager();
logger = new Logger(processingEnvironment.getMessager());
samplesFilePath = processingEnvironment.getOptions().get(KEY_SAMPLES_FILE);
try {
// create mutable copy
samples = new ArrayList<>(readCurrentSamples(samplesFilePath));
} catch (Throwable t) {
logger.error(t.getMessage());
throw new RuntimeException(t);
}
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (!roundEnvironment.processingOver()) {
final Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(MarkwonSample.class);
final long begin = System.currentTimeMillis();
final Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(MarkwonSampleInfo.class);
if (elements != null) {
for (Element element : elements) {
process(element);
}
if (samplesUpdated) {
logger.info("samples updated, writing at path: %s", samplesFilePath);
try {
writeSamples(samplesFilePath, samples);
} catch (Throwable t) {
logger.error(t.getMessage());
throw new RuntimeException(t);
}
}
}
final long end = System.currentTimeMillis();
logger.info("processing took: %d ms", end - begin);
}
return false;
}
private void process(@NonNull Element element) {
messager.printMessage(Diagnostic.Kind.WARNING, samplesFilePath, element);
try {
final MarkwonSample sample = parse((TypeElement) element);
final boolean updated = updateSamples(samples, sample);
if (updated) {
logger.info("updated sample: '%s'", sample.javaClassName);
}
samplesUpdated = samplesUpdated || updated;
} catch (Throwable t) {
logger.error(t.getMessage());
throw new RuntimeException(t);
}
}
@NonNull
private static List<MarkwonSample> readCurrentSamples(@NonNull String path) throws Throwable {
final File file = new File(path);
if (!file.exists()) {
// the very first one, no need to create file at this point, just return empty list
return Collections.emptyList();
}
try (
InputStream inputStream = FileUtils.openInputStream(file);
Reader reader = new InputStreamReader(inputStream)
) {
return new Gson()
.fromJson(reader, new TypeToken<List<MarkwonSample>>() {
}.getType());
} catch (IOException e) {
throw new Throwable(e);
}
}
private static void writeSamples(@NonNull String path, @NonNull List<MarkwonSample> samples) throws Throwable {
final File file = new File(path);
if (!file.exists()) {
try {
if (!file.createNewFile()) {
throw new Throwable("Cannot create new file at: " + path);
}
} catch (IOException e) {
throw new Throwable("Cannot create new file at: " + path);
}
}
// sort based on id (it is date)
// new items come first (DESC order)
Collections.sort(samples, (lhs, rhs) -> rhs.id.compareTo(lhs.id));
final String json = new GsonBuilder()
.setPrettyPrinting()
.create()
.toJson(samples);
FileUtils.write(file, json, StandardCharsets.UTF_8);
}
@NonNull
private static MarkwonSample parse(@NonNull TypeElement element) throws Throwable {
final MarkwonSampleInfo info = element.getAnnotation(MarkwonSampleInfo.class);
if (info == null) {
throw new Throwable("Cannot obtain `MarkwonSampleInfo` annotation");
}
final String id = info.id();
final MarkwonSample sample = new MarkwonSample(
element.getQualifiedName().toString(),
id,
info.title(),
info.description(),
new HashSet<>(Arrays.asList(info.artifacts())),
new HashSet<>(Arrays.asList(info.tags()))
);
try {
ID_DATEFORMAT.parse(id);
} catch (ParseException e) {
throw new Throwable(String.format("sample: '%s', id does not match pattern: '%s'",
sample.javaClassName,
id)
);
}
return sample;
}
// returns boolean indicating if samples were updated
private static boolean updateSamples(@NonNull List<MarkwonSample> samples, @NonNull MarkwonSample sample) {
final ListIterator<MarkwonSample> iterator = samples.listIterator();
boolean found = false;
boolean updated = false;
while (iterator.hasNext()) {
final MarkwonSample existing = iterator.next();
// check for id
if (existing.id.equals(sample.id)) {
// if not the same -> replace
if (!existing.equals(sample)) {
iterator.set(sample);
updated = true;
}
found = true;
break;
}
}
if (!found) {
samples.add(sample);
}
// if not found (inserted new) or updated (found and was different)
return !found || updated;
}
}

View File

@ -0,0 +1 @@
io.noties.markwon.sample.processor.MarkwonSampleProcessor,isolating