From e27fb69817b1417c1bc556a507b14f2700c7a736 Mon Sep 17 00:00:00 2001 From: Sergey Mashkov Date: Fri, 31 Jul 2015 15:35:34 +0300 Subject: Use Guice injector and ServiceLocator to load implementations on the fly --- src/Utilities/GuiceModule.kt | 58 ++++++++++++++++++++++ src/Utilities/ServiceLocator.kt | 105 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 src/Utilities/GuiceModule.kt create mode 100644 src/Utilities/ServiceLocator.kt (limited to 'src/Utilities') diff --git a/src/Utilities/GuiceModule.kt b/src/Utilities/GuiceModule.kt new file mode 100644 index 00000000..4ce4863d --- /dev/null +++ b/src/Utilities/GuiceModule.kt @@ -0,0 +1,58 @@ +package org.jetbrains.dokka.Utilities + +import com.google.inject.Binder +import com.google.inject.Module +import com.google.inject.Provider +import com.google.inject.name.Names +import org.jetbrains.dokka.* +import org.jetbrains.dokka.Formats.FormatDescriptor +import java.io.File + +class GuiceModule(val config: DokkaGenerator) : Module { + override fun configure(binder: Binder) { + binder.bind(javaClass()).toInstance(config) + binder.bind(javaClass()).annotatedWith(Names.named("outputDir")).toInstance(File(config.outputDir)) + + binder.bindNameAnnotated("singleFolder") + binder.bindNameAnnotated("singleFolder") + binder.bindNameAnnotated("folders") + binder.bindNameAnnotated("folders") + + // defaults + binder.bind(javaClass()).to(javaClass()) + binder.bind(javaClass()).to(javaClass()) + binder.bind(javaClass()).to(javaClass()) + + binder.bind(javaClass()).toProvider(object : Provider { + override fun get(): HtmlTemplateService = HtmlTemplateService.default("/dokka/styles/style.css") + }) + + binder.registerCategory("language") + binder.registerCategory("outline") + binder.registerCategory("format") + binder.registerCategory("generator") + + val descriptor = ServiceLocator.lookup("format", config.outputFormat, config) + + descriptor.outlineServiceClass?.let { clazz -> + binder.bind(javaClass()).to(clazz) + } + descriptor.formatServiceClass?.let { clazz -> + binder.bind(javaClass()).to(clazz) + } + binder.bind(javaClass()).to(descriptor.generatorServiceClass) + } + +} + +private inline fun Binder.registerCategory(category: String) { + ServiceLocator.allServices(category).forEach { + @suppress("UNCHECKED_CAST") + bind(javaClass()).annotatedWith(Names.named(it.name)).to(javaClass().classLoader.loadClass(it.className) as Class) + } +} + +private inline fun Binder.bindNameAnnotated(name: String) { + bind(javaClass()).annotatedWith(Names.named(name)).to(javaClass()) +} + diff --git a/src/Utilities/ServiceLocator.kt b/src/Utilities/ServiceLocator.kt new file mode 100644 index 00000000..e2ed0499 --- /dev/null +++ b/src/Utilities/ServiceLocator.kt @@ -0,0 +1,105 @@ +package org.jetbrains.dokka.Utilities + +import org.jetbrains.dokka.DokkaGenerator +import java.io.File +import java.util.Properties +import java.util.jar.JarFile +import java.util.zip.ZipEntry + +data class ServiceDescriptor(val name: String, val category: String, val description: String?, val className: String) + +class ServiceLookupException(message: String) : Exception(message) + +public object ServiceLocator { + public fun lookup(clazz: Class, category: String, implementationName: String, conf: DokkaGenerator): T { + val descriptor = lookupDescriptor(category, implementationName) + val loadedClass = javaClass.classLoader.loadClass(descriptor.className) + val constructor = loadedClass.constructors + .filter { it.parameterTypes.isEmpty() || (it.parameterTypes.size() == 1 && conf.javaClass.isInstance(it.parameterTypes[0])) } + .sortDescendingBy { it.parameterTypes.size() } + .firstOrNull() ?: throw ServiceLookupException("Class ${descriptor.className} has no corresponding constructor") + + val implementationRawType: Any = if (constructor.parameterTypes.isEmpty()) constructor.newInstance() else constructor.newInstance(constructor) + + if (!clazz.isInstance(implementationRawType)) { + throw ServiceLookupException("Class ${descriptor.className} is not a subtype of ${clazz.name}") + } + + @suppress("UNCHECKED_CAST") + return implementationRawType as T + } + + public fun lookupClass(clazz: Class, category: String, implementationName: String): Class = lookupDescriptor(category, implementationName).className.let { className -> + javaClass.classLoader.loadClass(className).let { loaded -> + if (!clazz.isAssignableFrom(loaded)) { + throw ServiceLookupException("Class $className is not a subtype of ${clazz.name}") + } + + @suppress("UNCHECKED_CAST") + val casted = loaded as Class + + casted + } + } + + private fun lookupDescriptor(category: String, implementationName: String): ServiceDescriptor { + val properties = javaClass.classLoader.getResourceAsStream("dokka/$category/$implementationName.properties")?.use { stream -> + Properties().let { properties -> + properties.load(stream) + properties + } + } ?: throw ServiceLookupException("No implementation with name $implementationName found in category $category") + + val className = properties["class"]?.toString() ?: throw ServiceLookupException("Implementation $implementationName has no class configured") + + return ServiceDescriptor(implementationName, category, properties["description"]?.toString(), className) + } + + fun allServices(category: String): List = javaClass.classLoader.getResourceAsStream("dokka/$category")?.use { stream -> + val entries = this.javaClass.classLoader.getResources("dokka/$category")?.toList() ?: emptyList() + + entries.flatMap { + when (it.protocol) { + "file" -> File(it.file).listFiles()?.filter { it.extension == "properties" }?.map { lookupDescriptor(category, it.nameWithoutExtension) } ?: emptyList() + "jar" -> { + val file = JarFile(it.file.removePrefix("file:").substringBefore("!")) + try { + val jarPath = it.file.substringAfterLast("!").removePrefix("/") + file.entries() + .asSequence() + .filter { entry -> !entry.isDirectory && entry.path == jarPath && entry.extension == "properties" } + .map { entry -> + lookupDescriptor(category, entry.fileName.substringBeforeLast(".")) + }.toList() + } finally { + file.close() + } + } + else -> emptyList() + } + } + } ?: emptyList() +} + +public inline fun ServiceLocator.lookup(category: String, implementationName: String, conf: DokkaGenerator): T = lookup(javaClass(), category, implementationName, conf) +public inline fun ServiceLocator.lookupClass(category: String, implementationName: String): Class = lookupClass(javaClass(), category, implementationName) +public inline fun ServiceLocator.lookupOrNull(category: String, implementationName: String, conf: DokkaGenerator): T? = try { + lookup(javaClass(), category, implementationName, conf) +} catch (any: Throwable) { + null +} + +fun main(args: Array) { + ServiceLocator.allServices("format").forEach { + println(it) + } +} + +private val ZipEntry.fileName: String + get() = name.substringAfterLast("/", name) + +private val ZipEntry.path: String + get() = name.substringBeforeLast("/", "").removePrefix("/") + +private val ZipEntry.extension: String? + get() = fileName.let { fn -> if ("." in fn) fn.substringAfterLast(".") else null } -- cgit From d1ff5949b5807b57eab6010175b3c6f1e7c1945c Mon Sep 17 00:00:00 2001 From: Sergey Mashkov Date: Fri, 7 Aug 2015 12:12:06 +0300 Subject: ~ minor cleanup --- src/Utilities/ServiceLocator.kt | 6 ------ 1 file changed, 6 deletions(-) (limited to 'src/Utilities') diff --git a/src/Utilities/ServiceLocator.kt b/src/Utilities/ServiceLocator.kt index e2ed0499..b3610a53 100644 --- a/src/Utilities/ServiceLocator.kt +++ b/src/Utilities/ServiceLocator.kt @@ -89,12 +89,6 @@ public inline fun ServiceLocator.lookupOrNull(category: String null } -fun main(args: Array) { - ServiceLocator.allServices("format").forEach { - println(it) - } -} - private val ZipEntry.fileName: String get() = name.substringAfterLast("/", name) -- cgit From bdd02a3112397b711f16d411869c0c610899aebf Mon Sep 17 00:00:00 2001 From: Sergey Mashkov Date: Fri, 7 Aug 2015 12:32:17 +0300 Subject: ~ Use new property access syntax --- src/Utilities/Path.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src/Utilities') diff --git a/src/Utilities/Path.kt b/src/Utilities/Path.kt index 24420ec7..fea22250 100644 --- a/src/Utilities/Path.kt +++ b/src/Utilities/Path.kt @@ -3,10 +3,7 @@ package org.jetbrains.dokka import java.io.* fun File.getRelativePath(name: File): File { - val parent = getParentFile() - - if (parent == null) - throw IOException("No common directory"); + val parent = parentFile ?: throw IOException("No common directory") val basePath = getCanonicalPath() + File.separator; val targetPath = name.getCanonicalPath(); -- cgit