From c9d59e9ae85f76e021d53c77ef18bfce0ff7ec7c 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/Formats/FormatDescriptor.kt | 11 +++ src/Formats/HtmlFormatService.kt | 6 +- src/Formats/JekyllFormatService.kt | 4 +- src/Formats/KotlinWebsiteFormatService.kt | 4 +- src/Formats/MarkdownFormatService.kt | 4 +- src/Formats/StandardFormats.kt | 47 ++++++++++++ src/Formats/YamlOutlineService.kt | 3 +- src/Generation/FileGenerator.kt | 37 ++++++---- src/Generation/Generator.kt | 15 ++++ src/Locations/FoldersLocationService.kt | 5 +- src/Locations/LocationService.kt | 2 + src/Locations/SingleFolderLocationService.kt | 6 +- src/Model/DocumentationNode.kt | 1 - src/Utilities/GuiceModule.kt | 58 +++++++++++++++ src/Utilities/ServiceLocator.kt | 105 +++++++++++++++++++++++++++ src/main.kt | 37 +++------- 16 files changed, 294 insertions(+), 51 deletions(-) create mode 100644 src/Formats/FormatDescriptor.kt create mode 100644 src/Formats/StandardFormats.kt create mode 100644 src/Generation/Generator.kt create mode 100644 src/Utilities/GuiceModule.kt create mode 100644 src/Utilities/ServiceLocator.kt (limited to 'src') diff --git a/src/Formats/FormatDescriptor.kt b/src/Formats/FormatDescriptor.kt new file mode 100644 index 00000000..beff730f --- /dev/null +++ b/src/Formats/FormatDescriptor.kt @@ -0,0 +1,11 @@ +package org.jetbrains.dokka.Formats + +import org.jetbrains.dokka.FormatService +import org.jetbrains.dokka.Generator +import org.jetbrains.dokka.OutlineFormatService + +public interface FormatDescriptor { + val formatServiceClass: Class? + val outlineServiceClass: Class? + val generatorServiceClass: Class +} \ No newline at end of file diff --git a/src/Formats/HtmlFormatService.kt b/src/Formats/HtmlFormatService.kt index 8e38a32c..e810ef7f 100644 --- a/src/Formats/HtmlFormatService.kt +++ b/src/Formats/HtmlFormatService.kt @@ -1,10 +1,12 @@ package org.jetbrains.dokka +import com.google.inject.Inject +import com.google.inject.name.Named import java.io.File -public open class HtmlFormatService(locationService: LocationService, +public open class HtmlFormatService @Inject constructor(@Named("folders") locationService: LocationService, signatureGenerator: LanguageService, - val templateService: HtmlTemplateService = HtmlTemplateService.default()) + val templateService: HtmlTemplateService) : StructuredFormatService(locationService, signatureGenerator, "html"), OutlineFormatService { override public fun formatText(text: String): String { return text.htmlEscape() diff --git a/src/Formats/JekyllFormatService.kt b/src/Formats/JekyllFormatService.kt index 98958293..75684ac2 100644 --- a/src/Formats/JekyllFormatService.kt +++ b/src/Formats/JekyllFormatService.kt @@ -1,6 +1,8 @@ package org.jetbrains.dokka -public open class JekyllFormatService(locationService: LocationService, +import com.google.inject.Inject + +public open class JekyllFormatService @Inject constructor(locationService: LocationService, signatureGenerator: LanguageService) : MarkdownFormatService(locationService, signatureGenerator) { diff --git a/src/Formats/KotlinWebsiteFormatService.kt b/src/Formats/KotlinWebsiteFormatService.kt index 71eb7753..25491cb3 100644 --- a/src/Formats/KotlinWebsiteFormatService.kt +++ b/src/Formats/KotlinWebsiteFormatService.kt @@ -1,6 +1,8 @@ package org.jetbrains.dokka -public class KotlinWebsiteFormatService(locationService: LocationService, +import com.google.inject.Inject + +public class KotlinWebsiteFormatService @Inject constructor(locationService: LocationService, signatureGenerator: LanguageService) : JekyllFormatService(locationService, signatureGenerator) { private var needHardLineBreaks = false diff --git a/src/Formats/MarkdownFormatService.kt b/src/Formats/MarkdownFormatService.kt index ba7d8f7b..29a9f5c4 100644 --- a/src/Formats/MarkdownFormatService.kt +++ b/src/Formats/MarkdownFormatService.kt @@ -1,7 +1,9 @@ package org.jetbrains.dokka +import com.google.inject.Inject -public open class MarkdownFormatService(locationService: LocationService, + +public open class MarkdownFormatService @Inject constructor(locationService: LocationService, signatureGenerator: LanguageService) : StructuredFormatService(locationService, signatureGenerator, "md") { override public fun formatBreadcrumbs(items: Iterable): String { diff --git a/src/Formats/StandardFormats.kt b/src/Formats/StandardFormats.kt new file mode 100644 index 00000000..1d5ffe13 --- /dev/null +++ b/src/Formats/StandardFormats.kt @@ -0,0 +1,47 @@ +package org.jetbrains.dokka.Formats + +import org.jetbrains.dokka.* + +class HtmlFormatDescriptor : FormatDescriptor { + override val formatServiceClass: Class + get() = javaClass() + + override val outlineServiceClass: Class + get() = javaClass() + + override val generatorServiceClass: Class + get() = javaClass() +} + +class KotlinWebsiteFormatDescriptor : FormatDescriptor { + override val formatServiceClass: Class + get() = javaClass() + + override val outlineServiceClass: Class + get() = javaClass() + + override val generatorServiceClass: Class + get() = javaClass() +} + +class JekyllFormatDescriptor : FormatDescriptor { + override val formatServiceClass: Class + get() = javaClass() + + override val outlineServiceClass: Class? + get() = null + + override val generatorServiceClass: Class + get() = javaClass() +} + +class MarkdownFormatDescriptor : FormatDescriptor { + override val formatServiceClass: Class + get() = javaClass() + + override val outlineServiceClass: Class? + get() = null + + override val generatorServiceClass: Class + get() = javaClass() +} diff --git a/src/Formats/YamlOutlineService.kt b/src/Formats/YamlOutlineService.kt index cdab4eeb..7968824c 100644 --- a/src/Formats/YamlOutlineService.kt +++ b/src/Formats/YamlOutlineService.kt @@ -1,8 +1,9 @@ package org.jetbrains.dokka +import com.google.inject.Inject import java.io.File -class YamlOutlineService(val locationService: LocationService, +class YamlOutlineService @Inject constructor(val locationService: LocationService, val languageService: LanguageService) : OutlineFormatService { override fun getOutlineFileName(location: Location): File = File("${location.path}.yml") diff --git a/src/Generation/FileGenerator.kt b/src/Generation/FileGenerator.kt index 40645b51..08a885ab 100644 --- a/src/Generation/FileGenerator.kt +++ b/src/Generation/FileGenerator.kt @@ -1,36 +1,41 @@ package org.jetbrains.dokka +import com.google.inject.Inject +import java.io.File import java.io.FileOutputStream +import java.io.IOException import java.io.OutputStreamWriter -public class FileGenerator(val signatureGenerator: LanguageService, - val locationService: FileLocationService, +public class FileGenerator @Inject constructor(val locationService: FileLocationService, val formatService: FormatService, - val outlineService: OutlineFormatService?) { + @Inject(optional = true) val outlineService: OutlineFormatService?) : Generator { - public fun buildPage(node: DocumentationNode): Unit = buildPages(listOf(node)) - public fun buildOutline(node: DocumentationNode): Unit = buildOutlines(listOf(node)) + override fun buildPages(nodes: Iterable) { + val specificLocationService = locationService.withExtension(formatService.extension) - public fun buildPages(nodes: Iterable) { - for ((location, items) in nodes.groupBy { locationService.location(it) }) { + for ((location, items) in nodes.groupBy { specificLocationService.location(it) }) { val file = location.file - file.parentFile?.mkdirs() - FileOutputStream(file).use { - OutputStreamWriter(it, Charsets.UTF_8).use { - it.write(formatService.format(location, items)) + file.parentFile?.mkdirsOrFail() + try { + FileOutputStream(file).use { + OutputStreamWriter(it, Charsets.UTF_8).use { + it.write(formatService.format(location, items)) + } } + } catch (e: Throwable) { + println(e) } buildPages(items.flatMap { it.members }) } } - public fun buildOutlines(nodes: Iterable) { + override fun buildOutlines(nodes: Iterable) { if (outlineService == null) { return } for ((location, items) in nodes.groupBy { locationService.location(it) }) { val file = outlineService.getOutlineFileName(location) - file.parentFile?.mkdirs() + file.parentFile?.mkdirsOrFail() FileOutputStream(file).use { OutputStreamWriter(it, Charsets.UTF_8).use { it.write(outlineService.formatOutline(location, items)) @@ -38,4 +43,10 @@ public class FileGenerator(val signatureGenerator: LanguageService, } } } +} + +private fun File.mkdirsOrFail() { + if (!mkdirs() && !exists()) { + throw IOException("Failed to create directory $this") + } } \ No newline at end of file diff --git a/src/Generation/Generator.kt b/src/Generation/Generator.kt new file mode 100644 index 00000000..7dcabb0b --- /dev/null +++ b/src/Generation/Generator.kt @@ -0,0 +1,15 @@ +package org.jetbrains.dokka + +public interface Generator { + fun buildPages(nodes: Iterable) + fun buildOutlines(nodes: Iterable) + + final fun buildAll(nodes: Iterable) { + buildPages(nodes) + buildOutlines(nodes) + } + + final fun buildPage(node: DocumentationNode): Unit = buildPages(listOf(node)) + final fun buildOutline(node: DocumentationNode): Unit = buildOutlines(listOf(node)) + final fun buildAll(node: DocumentationNode): Unit = buildAll(listOf(node)) +} diff --git a/src/Locations/FoldersLocationService.kt b/src/Locations/FoldersLocationService.kt index e85c2c98..8a0cf6be 100644 --- a/src/Locations/FoldersLocationService.kt +++ b/src/Locations/FoldersLocationService.kt @@ -1,9 +1,12 @@ package org.jetbrains.dokka +import com.google.inject.Inject +import com.google.inject.name.Named import java.io.File public fun FoldersLocationService(root: String): FoldersLocationService = FoldersLocationService(File(root), "") -public class FoldersLocationService(val root: File, val extension: String) : FileLocationService { +public class FoldersLocationService @Inject constructor(@Named("outputDir") val root: File, val extension: String) : FileLocationService { + override fun withExtension(newExtension: String): FileLocationService { return if (extension.isEmpty()) FoldersLocationService(root, newExtension) else this } diff --git a/src/Locations/LocationService.kt b/src/Locations/LocationService.kt index a43054ce..4f587361 100644 --- a/src/Locations/LocationService.kt +++ b/src/Locations/LocationService.kt @@ -55,6 +55,8 @@ public interface LocationService { public interface FileLocationService: LocationService { + override fun withExtension(newExtension: String): FileLocationService = this + override fun location(node: DocumentationNode): FileLocation = location(node.path.map { it.name }, node.members.any()) override fun location(qualifiedName: List, hasMembers: Boolean): FileLocation } diff --git a/src/Locations/SingleFolderLocationService.kt b/src/Locations/SingleFolderLocationService.kt index c26d2b34..049636c0 100644 --- a/src/Locations/SingleFolderLocationService.kt +++ b/src/Locations/SingleFolderLocationService.kt @@ -1,10 +1,12 @@ package org.jetbrains.dokka +import com.google.inject.Inject +import com.google.inject.name.Named import java.io.File public fun SingleFolderLocationService(root: String): SingleFolderLocationService = SingleFolderLocationService(File(root), "") -public class SingleFolderLocationService(val root: File, val extension: String) : FileLocationService { - override fun withExtension(newExtension: String): LocationService = +public class SingleFolderLocationService @Inject constructor(@Named("outputDir") val root: File, val extension: String) : FileLocationService { + override fun withExtension(newExtension: String): FileLocationService = SingleFolderLocationService(root, newExtension) override fun location(qualifiedName: List, hasMembers: Boolean): FileLocation { diff --git a/src/Model/DocumentationNode.kt b/src/Model/DocumentationNode.kt index 34181b6a..468cb878 100644 --- a/src/Model/DocumentationNode.kt +++ b/src/Model/DocumentationNode.kt @@ -143,7 +143,6 @@ fun DocumentationNode.findOrCreatePackageNode(packageName: String, packageConten fun DocumentationNode.append(child: DocumentationNode, kind: DocumentationReference.Kind) { addReferenceTo(child, kind) - @suppress("NON_EXHAUSTIVE_WHEN") when (kind) { DocumentationReference.Kind.Detail -> child.addReferenceTo(this, DocumentationReference.Kind.Owner) DocumentationReference.Kind.Member -> child.addReferenceTo(this, DocumentationReference.Kind.Owner) 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()).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..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 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])) } + .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 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 } diff --git a/src/main.kt b/src/main.kt index e7c11fac..a534ffdd 100644 --- a/src/main.kt +++ b/src/main.kt @@ -1,5 +1,6 @@ package org.jetbrains.dokka +import com.google.inject.Guice import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.psi.PsiFile @@ -7,6 +8,7 @@ import com.intellij.psi.PsiJavaFile import com.intellij.psi.PsiManager import com.sampullara.cli.Args import com.sampullara.cli.Argument +import org.jetbrains.dokka.Utilities.GuiceModule import org.jetbrains.kotlin.cli.common.arguments.ValueDescription import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity @@ -17,6 +19,7 @@ import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot import org.jetbrains.kotlin.config.CommonConfigurationKeys import org.jetbrains.kotlin.utils.PathUtil import java.io.File +import kotlin.util.measureTimeMillis class DokkaArguments { @set:Argument(value = "src", description = "Source file or directory (allows many paths separated by the system path separator)") @@ -141,9 +144,9 @@ class DokkaGenerator(val logger: DokkaLogger, fun generate() { val environment = createAnalysisEnvironment() - logger.info("Module: ${moduleName}") + logger.info("Module: $moduleName") logger.info("Output: ${File(outputDir).absolutePath}") - logger.info("Sources: ${environment.sources.joinToString()}") + logger.info("Sources: ${environment.sources.join()}") logger.info("Classpath: ${environment.classpath.joinToString()}") logger.info("Analysing sources and libraries... ") @@ -155,34 +158,12 @@ class DokkaGenerator(val logger: DokkaLogger, val timeAnalyse = System.currentTimeMillis() - startAnalyse logger.info("done in ${timeAnalyse / 1000} secs") - val startBuild = System.currentTimeMillis() - val signatureGenerator = KotlinLanguageService() - val locationService = FoldersLocationService(outputDir) - val templateService = HtmlTemplateService.default("/dokka/styles/style.css") - - val (formatter, outlineFormatter) = when (outputFormat) { - "html" -> { - val htmlFormatService = HtmlFormatService(locationService, signatureGenerator, templateService) - htmlFormatService to htmlFormatService - } - "markdown" -> MarkdownFormatService(locationService, signatureGenerator) to null - "jekyll" -> JekyllFormatService(locationService.withExtension("html"), signatureGenerator) to null - "kotlin-website" -> KotlinWebsiteFormatService(locationService.withExtension("html"), signatureGenerator) to - YamlOutlineService(locationService, signatureGenerator) - else -> { - logger.error("Unrecognized output format ${outputFormat}") - null to null - } + val timeBuild = measureTimeMillis { + logger.info("Generating pages... ") + Guice.createInjector(GuiceModule(this)).getInstance(javaClass()).buildAll(documentation) } - if (formatter == null) return - - val generator = FileGenerator(signatureGenerator, locationService.withExtension(formatter.extension), - formatter, outlineFormatter) - logger.info("Generating pages... ") - generator.buildPage(documentation) - generator.buildOutline(documentation) - val timeBuild = System.currentTimeMillis() - startBuild logger.info("done in ${timeBuild / 1000} secs") + Disposer.dispose(environment) } -- cgit