diff --git a/app-sample/build.gradle b/app-sample/build.gradle index a63d63f0..6947663f 100644 --- a/app-sample/build.gradle +++ b/app-sample/build.gradle @@ -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" } diff --git a/app-sample/src/main/java/io/noties/markwon/app/App.java b/app-sample/src/main/java/io/noties/markwon/app/App.java deleted file mode 100644 index 22d93c21..00000000 --- a/app-sample/src/main/java/io/noties/markwon/app/App.java +++ /dev/null @@ -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)); - } -} diff --git a/app-sample/src/main/java/io/noties/markwon/app/App.kt b/app-sample/src/main/java/io/noties/markwon/app/App.kt new file mode 100644 index 00000000..0ae04ee6 --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/App.kt @@ -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)) + } +} \ No newline at end of file diff --git a/app-sample/src/main/java/io/noties/markwon/app/MainActivity.java b/app-sample/src/main/java/io/noties/markwon/app/MainActivity.java deleted file mode 100644 index 301fe240..00000000 --- a/app-sample/src/main/java/io/noties/markwon/app/MainActivity.java +++ /dev/null @@ -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); - - - } -} diff --git a/app-sample/src/main/java/io/noties/markwon/app/MainActivity.kt b/app-sample/src/main/java/io/noties/markwon/app/MainActivity.kt new file mode 100644 index 00000000..be29cd6b --- /dev/null +++ b/app-sample/src/main/java/io/noties/markwon/app/MainActivity.kt @@ -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) + + /**/ + + } +} \ No newline at end of file diff --git a/app-sample/src/main/java/io/noties/markwon/app/Test.java b/app-sample/src/main/java/io/noties/markwon/app/Test.java deleted file mode 100644 index d9c61e70..00000000 --- a/app-sample/src/main/java/io/noties/markwon/app/Test.java +++ /dev/null @@ -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 { - -} diff --git a/build.gradle b/build.gradle index b92f8dd9..4e594c90 100644 --- a/build.gradle +++ b/build.gradle @@ -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", ] diff --git a/markwon-core/build.gradle b/markwon-core/build.gradle index 8b2f1be6..dab67c17 100644 --- a/markwon-core/build.gradle +++ b/markwon-core/build.gradle @@ -25,15 +25,14 @@ dependencies { compileOnly it['x-appcompat'] } - deps['test'].with { + testImplementation project(':markwon-test-span') + testImplementation deps['commons-io'] - testImplementation project(':markwon-test-span') + deps['test'].with { testImplementation it['junit'] testImplementation it['robolectric'] testImplementation it['mockito'] - - testImplementation it['commons-io'] } } diff --git a/markwon-editor/build.gradle b/markwon-editor/build.gradle index cc7ad811..884f6fb6 100644 --- a/markwon-editor/build.gradle +++ b/markwon-editor/build.gradle @@ -17,15 +17,14 @@ dependencies { api project(':markwon-core') - deps['test'].with { + testImplementation project(':markwon-test-span') + testImplementation deps['commons-io'] - testImplementation project(':markwon-test-span') + deps['test'].with { testImplementation it['junit'] testImplementation it['robolectric'] testImplementation it['mockito'] - - testImplementation it['commons-io'] } } diff --git a/markwon-ext-strikethrough/build.gradle b/markwon-ext-strikethrough/build.gradle index da9acfb0..6cd72e5e 100644 --- a/markwon-ext-strikethrough/build.gradle +++ b/markwon-ext-strikethrough/build.gradle @@ -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'] } } diff --git a/markwon-ext-tasklist/build.gradle b/markwon-ext-tasklist/build.gradle index 8a0e6cd7..315808d4 100644 --- a/markwon-ext-tasklist/build.gradle +++ b/markwon-ext-tasklist/build.gradle @@ -17,13 +17,13 @@ dependencies { api project(':markwon-core') - deps['test'].with { + testImplementation project(':markwon-test-span') + testImplementation deps['commons-io'] - testImplementation project(':markwon-test-span') + deps['test'].with { testImplementation it['junit'] testImplementation it['robolectric'] - testImplementation it['commons-io'] } } diff --git a/markwon-html/build.gradle b/markwon-html/build.gradle index 28ead2d9..4337e0d9 100644 --- a/markwon-html/build.gradle +++ b/markwon-html/build.gradle @@ -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'] } } diff --git a/markwon-image/build.gradle b/markwon-image/build.gradle index 2e0c86dd..9953b415 100644 --- a/markwon-image/build.gradle +++ b/markwon-image/build.gradle @@ -23,15 +23,14 @@ dependencies { compileOnly it['okhttp'] } - deps['test'].with { + testImplementation project(':markwon-test-span') + testImplementation deps['commons-io'] - testImplementation project(':markwon-test-span') + deps['test'].with { testImplementation it['junit'] testImplementation it['robolectric'] testImplementation it['mockito'] - - testImplementation it['commons-io'] } } diff --git a/markwon-test-span/build.gradle b/markwon-test-span/build.gradle index 0e519e25..bdf64b2e 100644 --- a/markwon-test-span/build.gradle +++ b/markwon-test-span/build.gradle @@ -16,9 +16,9 @@ android { dependencies { api deps['x-annotations'] + api deps['ix-java'] deps['test'].with { api it['junit'] - api it['ix-java'] } } diff --git a/sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonSample.java b/sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonSampleInfo.java similarity index 95% rename from sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonSample.java rename to sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonSampleInfo.java index dd825c38..234a1f1f 100644 --- a/sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonSample.java +++ b/sample-utils/annotations/io/noties/markwon/sample/annotations/MarkwonSampleInfo.java @@ -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 diff --git a/sample-utils/processor/build.gradle b/sample-utils/processor/build.gradle index 4c8e775a..d2664b3f 100644 --- a/sample-utils/processor/build.gradle +++ b/sample-utils/processor/build.gradle @@ -8,5 +8,9 @@ sourceSets { } dependencies { - implementation deps['x-annotations'] + deps.with { + implementation it['x-annotations'] + implementation it['gson'] + implementation it['commons-io'] + } } \ No newline at end of file diff --git a/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/Logger.java b/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/Logger.java new file mode 100644 index 00000000..2c6b2f43 --- /dev/null +++ b/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/Logger.java @@ -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"); + } +} diff --git a/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/MarkwonSample.java b/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/MarkwonSample.java new file mode 100644 index 00000000..927f823d --- /dev/null +++ b/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/MarkwonSample.java @@ -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 artifacts; + final Set tags; + + public MarkwonSample( + @NonNull String javaClassName, + @NonNull String id, + @NonNull String title, + @NonNull String description, + @NonNull Set artifacts, + @NonNull Set 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 + + '}'; + } +} diff --git a/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/MarkwonSampleProcessor.java b/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/MarkwonSampleProcessor.java index 7ca997b3..1fc85213 100644 --- a/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/MarkwonSampleProcessor.java +++ b/sample-utils/processor/src/main/java/io/noties/markwon/sample/processor/MarkwonSampleProcessor.java @@ -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 samples; + private boolean samplesUpdated; + @Override public Set getSupportedOptions() { return Collections.singleton(KEY_SAMPLES_FILE); @@ -35,31 +58,174 @@ public class MarkwonSampleProcessor extends AbstractProcessor { @Override public Set 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 set, RoundEnvironment roundEnvironment) { if (!roundEnvironment.processingOver()) { - final Set elements = roundEnvironment.getElementsAnnotatedWith(MarkwonSample.class); + final long begin = System.currentTimeMillis(); + final Set 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 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>() { + }.getType()); + } catch (IOException e) { + throw new Throwable(e); + } + } + + private static void writeSamples(@NonNull String path, @NonNull List 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 samples, @NonNull MarkwonSample sample) { + + final ListIterator 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; } } diff --git a/sample-utils/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors b/sample-utils/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 00000000..9f889e9a --- /dev/null +++ b/sample-utils/processor/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +io.noties.markwon.sample.processor.MarkwonSampleProcessor,isolating \ No newline at end of file