diff options
author | Sergey Mashkov <sergey.mashkov@jetbrains.com> | 2015-07-31 15:35:34 +0300 |
---|---|---|
committer | Dmitry Jemerov <yole@jetbrains.com> | 2015-10-29 13:21:47 +0100 |
commit | c9d59e9ae85f76e021d53c77ef18bfce0ff7ec7c (patch) | |
tree | d5e86994c762318008f7beaf781cdeafde573802 /src/Utilities | |
parent | ff77b8e0ad0b5089e940227dfdd94ba21cfc6bd8 (diff) | |
download | dokka-c9d59e9ae85f76e021d53c77ef18bfce0ff7ec7c.tar.gz dokka-c9d59e9ae85f76e021d53c77ef18bfce0ff7ec7c.tar.bz2 dokka-c9d59e9ae85f76e021d53c77ef18bfce0ff7ec7c.zip |
Use Guice injector and ServiceLocator to load implementations on the fly
Diffstat (limited to 'src/Utilities')
-rw-r--r-- | src/Utilities/GuiceModule.kt | 58 | ||||
-rw-r--r-- | src/Utilities/ServiceLocator.kt | 105 |
2 files changed, 163 insertions, 0 deletions
diff --git a/src/Utilities/GuiceModule.kt b/src/Utilities/GuiceModule.kt new file mode 100644 index 00000000..57bad468 --- /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<DokkaGenerator>()).toInstance(config) + binder.bind(javaClass<File>()).annotatedWith(Names.named("outputDir")).toInstance(File(config.outputDir)) + + binder.bindNameAnnotated<LocationService, SingleFolderLocationService>("singleFolder") + binder.bindNameAnnotated<FileLocationService, SingleFolderLocationService>("singleFolder") + binder.bindNameAnnotated<LocationService, FoldersLocationService>("folders") + binder.bindNameAnnotated<FileLocationService, FoldersLocationService>("folders") + + // defaults + binder.bind(javaClass<LocationService>()).to(javaClass<FoldersLocationService>()) + binder.bind(javaClass<FileLocationService>()).to(javaClass<FoldersLocationService>()) + binder.bind(javaClass<LanguageService>()).to(javaClass<KotlinLanguageService>()) + + binder.bind(javaClass<HtmlTemplateService>()).toProvider(object : Provider<HtmlTemplateService> { + override fun get(): HtmlTemplateService = HtmlTemplateService.default("/dokka/styles/style.css") + }) + + binder.registerCategory<LanguageService>("language") + binder.registerCategory<OutlineFormatService>("outline") + binder.registerCategory<FormatService>("format") + binder.registerCategory<Generator>("generator") + + val descriptor = ServiceLocator.lookup<FormatDescriptor>("format", config.outputFormat, config) + + descriptor.outlineServiceClass?.let { clazz -> + binder.bind(javaClass<OutlineFormatService>()).to(clazz) + } + descriptor.formatServiceClass?.let { clazz -> + binder.bind(javaClass<FormatService>()).to(clazz) + } + binder.bind(javaClass<Generator>()).to(descriptor.generatorServiceClass) + } + +} + +private inline fun <reified T: Any> Binder.registerCategory(category: String) { + ServiceLocator.allServices(category).forEach { + @Suppress("UNCHECKED_CAST") + bind(javaClass<T>()).annotatedWith(Names.named(it.name)).to(javaClass<T>().classLoader.loadClass(it.className) as Class<T>) + } +} + +private inline fun <reified Base : Any, reified T : Base> Binder.bindNameAnnotated(name: String) { + bind(javaClass<Base>()).annotatedWith(Names.named(name)).to(javaClass<T>()) +} + diff --git a/src/Utilities/ServiceLocator.kt b/src/Utilities/ServiceLocator.kt new file mode 100644 index 00000000..bc04238f --- /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.* +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 <T : Any> lookup(clazz: Class<T>, 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])) } + .sortedByDescending { 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 <T> lookupClass(clazz: Class<T>, category: String, implementationName: String): Class<T> = 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<T> + + 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<ServiceDescriptor> = 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<ServiceDescriptor>() + } + } + } ?: emptyList() +} + +public inline fun <reified T : Any> ServiceLocator.lookup(category: String, implementationName: String, conf: DokkaGenerator): T = lookup(javaClass<T>(), category, implementationName, conf) +public inline fun <reified T : Any> ServiceLocator.lookupClass(category: String, implementationName: String): Class<T> = lookupClass(javaClass<T>(), category, implementationName) +public inline fun <reified T : Any> ServiceLocator.lookupOrNull(category: String, implementationName: String, conf: DokkaGenerator): T? = try { + lookup(javaClass<T>(), category, implementationName, conf) +} catch (any: Throwable) { + null +} + +fun main(args: Array<String>) { + 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 } |