Skip to content

Commit

Permalink
Implement simple plugin to generate client
Browse files Browse the repository at this point in the history
Fix #4
  • Loading branch information
Dmitry Antonyuk committed Sep 19, 2019
1 parent 7bd2b0c commit ec3e8d9
Show file tree
Hide file tree
Showing 17 changed files with 525 additions and 10 deletions.
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ rootProject.buildFileName = "build.gradle.kts"
include("voorhees-core")
include("voorhees-client")
include("voorhees-server")
include("voorhees-gradle-plugin")
28 changes: 28 additions & 0 deletions voorhees-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
`java-library`
id("java-gradle-plugin")
}

gradlePlugin {
plugins {
create("voorheesPlugin") {
id = "voorhees"
displayName = "Plugin for generating Voorhees clients"
description = "Gradle plugin that helps to generate and publish client library for Voorhees services"
implementationClass = "com.hylamobile.voorhees.gradle.VoorheesPlugin"
}
}
}

dependencies {
compileOnly(gradleApi())
api("org.reflections:reflections:0.9.11")
api("net.bytebuddy:byte-buddy:1.10.1")

testCompileOnly(gradleTestKit())
testImplementation("junit:junit:4.12")
}

repositories {
jcenter()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.hylamobile.voorhees.client.annotation

/**
* Just a fake annotation to not depend on voorhees-client
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class JsonRpcService(val location: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.hylamobile.voorhees.gradle

import com.hylamobile.voorhees.server.annotation.DontExpose
import com.hylamobile.voorhees.server.annotation.JsonRpcService as ServerJsonRpcService
import com.hylamobile.voorhees.client.annotation.JsonRpcService as ClientJsonRpcService
import net.bytebuddy.ByteBuddy
import net.bytebuddy.description.annotation.AnnotationDescription
import net.bytebuddy.description.modifier.Visibility
import org.reflections.Reflections
import org.reflections.util.ClasspathHelper
import java.io.File
import java.lang.reflect.*
import java.net.URL
import java.net.URLClassLoader

class ClientGenerator(
private val packageToScan: String,
private val classPath: Array<URL>,
private val genDir: File) {

private val namingPattern = "%s.%s"

private val classesToGenerate: MutableSet<Class<*>> = mutableSetOf()

init {
genDir.apply {
if (!exists()) {
mkdirs()
}

check(isDirectory) { "${genDir.canonicalPath} is not a directory" }
}
}

fun generate() {
val buildClassLoader = URLClassLoader(classPath, ClasspathHelper.contextClassLoader())
val serviceClasses = Reflections(packageToScan, buildClassLoader)
.getTypesAnnotatedWith(ServerJsonRpcService::class.java)

for (serviceClass in serviceClasses) {
val location = serviceClass.getAnnotation(ServerJsonRpcService::class.java).locations.get(0)
val jsonRpcServiceAnno = AnnotationDescription.Builder
.ofType(ClientJsonRpcService::class.java)
.define("location", location)
.build()

var remoteInterface = ByteBuddy().makeInterface()
.name(namingPattern.format(serviceClass.`package`.name, serviceClass.simpleName))
.annotateType(jsonRpcServiceAnno)
.merge(Visibility.PUBLIC)

for (method in serviceClass.publicMethods) {
if (method.isAnnotationPresent(DontExpose::class.java)) continue

remoteInterface = remoteInterface
.defineMethod(method.name, method.genericReturnType, Visibility.PUBLIC)
.withParameters(*method.genericParameterTypes).withoutCode()

gatherClassesFromMethod(method)
}

remoteInterface.make().saveIn(genDir)
}

classesToGenerate.forEach {
ByteBuddy().rebase(it).make().saveIn(genDir)
}
}

private fun gatherClassesFromMethod(method: Method) {
classesFrom(method.genericReturnType)
method.genericParameterTypes.asSequence().forEach(this::classesFrom)
}

private fun classesFrom(type: Type?) {
if (classesToGenerate.contains(type)) return

when (type) {
is Class<*> -> when {
type.isArray -> classesFrom(type.componentType)
type.name.startsWith(packageToScan) -> {
classesToGenerate.add(type)
classesFrom(type.genericSuperclass)
type.genericInterfaces.forEach(this::classesFrom)
type.publicMethods.forEach(this::gatherClassesFromMethod)
}
}
is GenericArrayType -> classesFrom(type.genericComponentType)
is TypeVariable<*> -> {
type.genericDeclaration.typeParameters.forEach(this::classesFrom)
type.bounds.asSequence().forEach(this::classesFrom)
}
}
}

private val Class<*>.publicMethods
get() = declaredMethods.filter { Modifier.isPublic(it.modifiers) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,28 @@ import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.compile.AbstractCompile
import java.io.File

class GenerateClientTask : DefaultTask() {
open class GenerateClientTask : DefaultTask() {

@InputDirectory
fun getBuildDir(): File = project.buildDir
private val extension = project.extensions.getByType(VoorheesExtension::class.java)

@OutputDirectory
fun getGenDir(): File = project.buildDir
@get:InputDirectory
val buildDir: File
get() = project.buildDir

@get:OutputDirectory
val genDir: File
get() = File(buildDir, "classes/voorhees")

@TaskAction
fun apply() {
val classPath = project.tasks
.flatMap { if (it is AbstractCompile) listOf(it.destinationDir) else listOf() }
.map { it.toURI().toURL() }
.toTypedArray()

ClientGenerator(extension.packageToScan, classPath, genDir).generate()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.hylamobile.voorhees.gradle

import org.gradle.api.Project
import org.gradle.api.internal.provider.DefaultProvider
import org.gradle.api.provider.Property

open class VoorheesExtension(private val project: Project) {
var packageToScan: String = ""
val group = stringProperty { project.group.toString() }
val artifact = stringProperty { project.name + "-client" }
val version = stringProperty { project.version.toString() }

private fun stringProperty(defaultValue: () -> String): Property<String> =
project.objects.property(String::class.java).convention(DefaultProvider(defaultValue))
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,83 @@
package com.hylamobile.voorhees.gradle

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.type.ArtifactTypeDefinition
import org.gradle.api.component.SoftwareComponentFactory
import org.gradle.api.internal.artifacts.ArtifactAttributes
import org.gradle.api.internal.artifacts.dependencies.DefaultExternalModuleDependency
import org.gradle.api.internal.artifacts.dsl.LazyPublishArtifact
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.jvm.tasks.Jar
import java.io.File
import javax.inject.Inject
import org.gradle.api.plugins.internal.JavaConfigurationVariantMapping
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.internal.publication.DefaultMavenPublication

class VoorheesPlugin : Plugin<Project> {
const val CLIENT_NAME = "JsonRpcClient"

override fun apply(project: Project) {
project.tasks.register("generateJsonRpcClient", GenerateClientTask::class.java) {
task -> task.dependsOn("compileJava")
private fun capitalize(s: String) =
s[0].toUpperCase() + s.substring(1)

class VoorheesPlugin @Inject constructor(
private val softwareComponentFactory: SoftwareComponentFactory) : Plugin<ProjectInternal> {

override fun apply(project: ProjectInternal) {
val extension = project.extensions.create("voorhees", VoorheesExtension::class.java, project)

val generateTask = project.tasks.register("generate${capitalize(CLIENT_NAME)}", GenerateClientTask::class.java) { task ->
task.dependsOn("classes")
task.group = "jsonrpc"
task.description = "Generate JSON RPC client classes"
}

val jarTask = project.tasks.register("jar${capitalize(CLIENT_NAME)}", Jar::class.java) { task ->
task.dependsOn(generateTask)
task.group = "jsonrpc"
task.description = "Build a jar out of JSON RPC client classes"

task.archiveFileName.set("${extension.artifact.get()}-${extension.version.get()}.jar")
task.destinationDirectory.set(File(project.buildDir, "libs"))

task.from(generateTask.get().genDir)
}

if (project.pluginManager.hasPlugin("maven-publish")) {
val library = softwareComponentFactory.adhoc("${CLIENT_NAME}Library")
val elements = project.configurations.create("jsonRpcElements") { conf ->
conf.dependencies.add(
DefaultExternalModuleDependency("com.hylamobile", "voorhees-client", "1.0.0"))
conf.outgoing.apply {
artifacts.add(LazyPublishArtifact(jarTask))
attributes.attribute(ArtifactAttributes.ARTIFACT_FORMAT, ArtifactTypeDefinition.JAR_TYPE)
}
}

library.addVariantsFromConfiguration(
elements, JavaConfigurationVariantMapping("compile", false))

project.components.add(library)

project.plugins.withId("maven-publish") {
val publishing = project.extensions.getByType(PublishingExtension::class.java)
publishing.publications.create(CLIENT_NAME, MavenPublication::class.java) { publication ->
check(publication is DefaultMavenPublication)

publication.from(library)
publication.mavenProjectIdentity.apply {
artifactId.set(extension.artifact)
groupId.set(extension.group)
version.set(extension.version)
}
}

project.tasks.register("publish${capitalize(CLIENT_NAME)}") { task ->
task.dependsOn("publish${capitalize(CLIENT_NAME)}PublicationToMavenRepository")
task.group = "jsonrpc"
task.description = "Publish JSON RPC client to Maven repository"
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.hylamobile.voorhees.server.annotation

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class DontExpose
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.hylamobile.voorhees.server.annotation

/**
* Just a fake annotation to not depend on voorhees-server
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class JsonRpcService(val locations: Array<String>)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.hylamobile.voorhees.gradle.test;

import com.hylamobile.voorhees.gradle.test.beans.GenericBean;
import com.hylamobile.voorhees.gradle.test.beans.InputBean;
import com.hylamobile.voorhees.gradle.test.beans.OutputBean;
import com.hylamobile.voorhees.gradle.test.beans.RecursiveBean1;
import com.hylamobile.voorhees.server.annotation.JsonRpcService;

import java.util.List;
import java.util.Map;

@JsonRpcService(locations = "/api")
public class RemoteService {

public OutputBean handle(InputBean bean) {
return null;
}

public GenericBean<OutputBean> handleGenerics(GenericBean<InputBean> input) {
return null;
}

public List<OutputBean> handleCollections(Map<String, InputBean> input) {
return null;
}

public RecursiveBean1 handleRecursive() {
return null;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.hylamobile.voorhees.gradle.test.beans;

public class GenericBean<T> {

private T underlying;

public T getUnderlying() {
return underlying;
}

public void setUnderlying(T underlying) {
this.underlying = underlying;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.hylamobile.voorhees.gradle.test.beans;

public class InputBean {

private String stringVar;
private String stringVal;
private int intVar;
private int intVal;
private int[] intArray;
private TestEnum enumVar;

public String getStringVar() {
return stringVar;
}

public void setStringVar(String stringVar) {
this.stringVar = stringVar;
}

public String getStringVal() {
return stringVal;
}

public int getIntVar() {
return intVar;
}

public void setIntVar(int intVar) {
this.intVar = intVar;
}

public int getIntVal() {
return intVal;
}

public int[] getIntArray() {
return intArray;
}

public void setIntArray(int[] intArray) {
this.intArray = intArray;
}

public TestEnum getEnumVar() {
return enumVar;
}

public void setEnumVar(TestEnum enumVar) {
this.enumVar = enumVar;
}
}
Loading

0 comments on commit ec3e8d9

Please sign in to comment.