| package com.github.shyiko.ktlint |
| |
| import com.github.shyiko.klob.Glob |
| import com.github.shyiko.ktlint.core.KtLint |
| import com.github.shyiko.ktlint.core.LintError |
| import com.github.shyiko.ktlint.core.ParseException |
| import com.github.shyiko.ktlint.core.Reporter |
| import com.github.shyiko.ktlint.core.ReporterProvider |
| import com.github.shyiko.ktlint.core.RuleExecutionException |
| import com.github.shyiko.ktlint.core.RuleSet |
| import com.github.shyiko.ktlint.core.RuleSetProvider |
| import com.github.shyiko.ktlint.internal.EditorConfig |
| import com.github.shyiko.ktlint.internal.IntellijIDEAIntegration |
| import com.github.shyiko.ktlint.internal.MavenDependencyResolver |
| import org.eclipse.aether.RepositoryException |
| import org.eclipse.aether.artifact.DefaultArtifact |
| import org.eclipse.aether.repository.RemoteRepository |
| import org.eclipse.aether.repository.RepositoryPolicy |
| import org.eclipse.aether.repository.RepositoryPolicy.CHECKSUM_POLICY_IGNORE |
| import org.eclipse.aether.repository.RepositoryPolicy.UPDATE_POLICY_NEVER |
| import org.jetbrains.kotlin.preprocessor.mkdirsOrFail |
| import org.kohsuke.args4j.Argument |
| import org.kohsuke.args4j.CmdLineException |
| import org.kohsuke.args4j.CmdLineParser |
| import org.kohsuke.args4j.NamedOptionDef |
| import org.kohsuke.args4j.Option |
| import org.kohsuke.args4j.OptionHandlerFilter |
| import org.kohsuke.args4j.ParserProperties |
| import org.kohsuke.args4j.spi.OptionHandler |
| import java.io.ByteArrayOutputStream |
| import java.io.File |
| import java.io.FileNotFoundException |
| import java.io.PrintStream |
| import java.io.PrintWriter |
| import java.math.BigInteger |
| import java.net.URLDecoder |
| import java.nio.file.Path |
| import java.nio.file.Paths |
| import java.security.MessageDigest |
| import java.util.ArrayList |
| import java.util.Arrays |
| import java.util.NoSuchElementException |
| import java.util.ResourceBundle |
| import java.util.Scanner |
| import java.util.ServiceLoader |
| import java.util.concurrent.ArrayBlockingQueue |
| import java.util.concurrent.Callable |
| import java.util.concurrent.Executors |
| import java.util.concurrent.Future |
| import java.util.concurrent.TimeUnit |
| import java.util.concurrent.atomic.AtomicBoolean |
| import java.util.concurrent.atomic.AtomicInteger |
| import kotlin.system.exitProcess |
| |
| object Main { |
| |
| private val DEPRECATED_FLAGS = mapOf( |
| "--ruleset-repository" to |
| "--repository", |
| "--reporter-repository" to |
| "--repository", |
| "--ruleset-update" to |
| "--repository-update", |
| "--reporter-update" to |
| "--repository-update" |
| ) |
| private val CLI_MAX_LINE_LENGTH_REGEX = Regex("(.{0,120})(?:\\s|$)") |
| |
| // todo: this should have been a command, not a flag (consider changing in 1.0.0) |
| @Option(name="--format", aliases = arrayOf("-F"), usage = "Fix any deviations from the code style") |
| private var format: Boolean = false |
| |
| @Option(name="--android", aliases = arrayOf("-a"), usage = "Turn on Android Kotlin Style Guide compatibility") |
| private var android: Boolean = false |
| |
| @Option(name="--reporter", |
| usage = "A reporter to use (built-in: plain (default), plain?group_by_file, json, checkstyle). " + |
| "To use a third-party reporter specify either a path to a JAR file on the filesystem or a" + |
| "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + |
| "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + |
| "Maven Central/JCenter/JitPack/user-provided repository)") |
| private var reporters = ArrayList<String>() |
| |
| @Option(name="--ruleset", aliases = arrayOf("-R"), |
| usage = "A path to a JAR file containing additional ruleset(s) or a " + |
| "<groupId>:<artifactId>:<version> triple pointing to a remote artifact (in which case ktlint will first " + |
| "check local cache (~/.m2/repository) and then, if not found, attempt downloading it from " + |
| "Maven Central/JCenter/JitPack/user-provided repository)") |
| private var rulesets = ArrayList<String>() |
| |
| @Option(name="--repository", aliases = arrayOf("--ruleset-repository", "--reporter-repository"), |
| usage = "An additional Maven repository (Maven Central/JCenter/JitPack are active by default) " + |
| "(value format: <id>=<url>)") |
| private var repositories = ArrayList<String>() |
| |
| @Option(name="--repository-update", aliases = arrayOf("-U", "--ruleset-update", "--reporter-update"), |
| usage = "Check remote repositories for updated snapshots") |
| private var forceUpdate: Boolean = false |
| |
| @Option(name="--limit", usage = "Maximum number of errors to show (default: show all)") |
| private var limit: Int = -1 |
| |
| @Option(name="--relative", usage = "Print files relative to the working directory " + |
| "(e.g. dir/file.kt instead of /home/user/project/dir/file.kt)") |
| private var relative: Boolean = false |
| |
| @Option(name="--verbose", aliases = arrayOf("-v"), usage = "Show error codes") |
| private var verbose: Boolean = false |
| |
| @Option(name="--stdin", usage = "Read file from stdin") |
| private var stdin: Boolean = false |
| |
| @Option(name="--version", usage = "Version", help = true) |
| private var version: Boolean = false |
| |
| @Option(name="--help", aliases = arrayOf("-h"), help = true) |
| private var help: Boolean = false |
| |
| @Option(name="--debug", usage = "Turn on debug output") |
| private var debug: Boolean = false |
| |
| // todo: make it a command in 1.0.0 (it's too late now as we might interfere with valid "lint" patterns) |
| @Option(name="--apply-to-idea", usage = "Update Intellij IDEA project settings") |
| private var apply: Boolean = false |
| @Option(name="--install-git-pre-commit-hook", usage = "Install git hook to automatically check files for style violations on commit") |
| private var installGitPreCommitHook: Boolean = false |
| @Option(name="-y", hidden = true) |
| private var forceApply: Boolean = false |
| |
| @Argument |
| private var patterns = ArrayList<String>() |
| |
| private fun CmdLineParser.usage(): String = |
| """ |
| An anti-bikeshedding Kotlin linter with built-in formatter (https://github.com/shyiko/ktlint). |
| |
| Usage: |
| ktlint <flags> [patterns] |
| java -jar ktlint <flags> [patterns] |
| |
| Examples: |
| # check the style of all Kotlin files inside the current dir (recursively) |
| # (hidden folders will be skipped) |
| ktlint |
| |
| # check only certain locations (prepend ! to negate the pattern) |
| ktlint "src/**/*.kt" "!src/**/*Test.kt" |
| |
| # auto-correct style violations |
| ktlint -F "src/**/*.kt" |
| |
| # use custom reporter |
| ktlint --reporter=plain?group_by_file |
| # multiple reporters can be specified like this |
| ktlint --reporter=plain --reporter=checkstyle,output=ktlint-checkstyle-report.xml |
| |
| Flags: |
| ${ByteArrayOutputStream().let { this.printUsage(it); it }.toString().trimEnd().split("\n") |
| .map { line -> (" " + line).replace(CLI_MAX_LINE_LENGTH_REGEX, " $1\n").trimEnd() } |
| .joinToString("\n")} |
| """.trimIndent() |
| |
| fun parseCmdLine(args: Array<String>) { |
| args.forEach { arg -> |
| if (arg.startsWith("--") && arg.contains("=")) { |
| val flag = arg.substringBefore("=") |
| val alt = DEPRECATED_FLAGS[flag] |
| if (alt != null) { |
| System.err.println("$flag flag is deprecated and will be removed in 1.0.0 (use $alt instead)") |
| } |
| } |
| } |
| val parser = object : CmdLineParser(this, ParserProperties.defaults() |
| .withShowDefaults(false) |
| .withUsageWidth(512) |
| .withOptionSorter { l, r -> |
| l.option.toString().replace("-", "").compareTo(r.option.toString().replace("-", "")) |
| }) { |
| |
| override fun printOption(out: PrintWriter, handler: OptionHandler<*>, len: Int, rb: ResourceBundle?, filter: OptionHandlerFilter?) { |
| handler.defaultMetaVariable |
| val opt = handler.option as? NamedOptionDef ?: return |
| if (opt.hidden() || opt.help()) { |
| return |
| } |
| val maxNameLength = options.map { h -> |
| val o = h.option |
| (o as? NamedOptionDef)?.let { it.name().length + 1 + (h.defaultMetaVariable ?: "").length } ?: 0 |
| }.max()!! |
| val shorthand = opt.aliases().find { it.startsWith("-") && !it.startsWith("--") } |
| val line = (if (shorthand != null) "$shorthand, " else " ") + |
| (opt.name() + " " + (handler.defaultMetaVariable ?: "")).padEnd(maxNameLength, ' ') + " " + opt.usage() |
| out.println(line) |
| } |
| } |
| try { |
| parser.parseArgument(*args) |
| } catch (err: CmdLineException) { |
| System.err.println("Error: ${err.message}\n\n${parser.usage()}") |
| exitProcess(1) |
| } |
| if (help) { println(parser.usage()); exitProcess(0) } |
| } |
| |
| @JvmStatic |
| fun main(args: Array<String>) { |
| parseCmdLine(args) |
| if (version) { |
| println(javaClass.`package`.implementationVersion) |
| exitProcess(0) |
| } |
| if (installGitPreCommitHook) { |
| installGitPreCommitHook() |
| if (!apply) { |
| exitProcess(0) |
| } |
| } |
| if (apply) { |
| applyToIDEA() |
| exitProcess(0) |
| } |
| val workDir = File(".").canonicalPath |
| val start = System.currentTimeMillis() |
| // load 3rd party ruleset(s) (if any) |
| val dependencyResolver by lazy { buildDependencyResolver() } |
| if (!rulesets.isEmpty()) { |
| loadJARs(dependencyResolver, rulesets) |
| } |
| // standard should go first |
| val rp = ServiceLoader.load(RuleSetProvider::class.java) |
| .map { it.get().id to it } |
| .sortedBy { if (it.first == "standard") "\u0000${it.first}" else it.first } |
| if (debug) { |
| rp.forEach { System.err.println("[DEBUG] Discovered ruleset \"${it.first}\"") } |
| } |
| data class R(val id: String, val config: Map<String, String>, var output: String?) |
| if (reporters.isEmpty()) { |
| reporters.add("plain") |
| } |
| val rr = this.reporters.map { reporter -> |
| val split = reporter.split(",") |
| val (reporterId, rawReporterConfig) = split[0].split("?", limit = 2) + listOf("") |
| R(reporterId, mapOf("verbose" to verbose.toString()) + parseQuery(rawReporterConfig), |
| split.getOrNull(1)?.let { if (it.startsWith("output=")) it.split("=")[1] else null }) |
| }.distinct() |
| // load reporter |
| val reporterLoader = ServiceLoader.load(ReporterProvider::class.java) |
| val reporterProviderById = reporterLoader.associate { it.id to it }.let { map -> |
| val missingReporters = rr.map { it.id }.distinct().filter { !map.containsKey(it) } |
| if (!missingReporters.isEmpty()) { |
| loadJARs(dependencyResolver, missingReporters) |
| reporterLoader.reload() |
| reporterLoader.associate { it.id to it } |
| } else map |
| } |
| if (debug) { |
| reporterProviderById.forEach { (id) -> System.err.println("[DEBUG] Discovered reporter \"$id\"") } |
| } |
| val reporter = Reporter.from(*rr.map { r -> |
| val reporterProvider = reporterProviderById[r.id] |
| if (reporterProvider == null) { |
| System.err.println("Error: reporter \"${r.id}\" wasn't found (available: ${ |
| reporterProviderById.keys.sorted().joinToString(",")})") |
| exitProcess(1) |
| } |
| if (debug) { |
| System.err.println("[DEBUG] Initializing \"${r.id}\" reporter with ${r.config}" + |
| (r.output?.let { ", output=$it" } ?: "")) |
| } |
| val output = if (r.output != null) { File(r.output).parentFile?.mkdirsOrFail(); PrintStream(r.output) } else |
| if (stdin) System.err else System.out |
| reporterProvider.get(output, r.config).let { reporter -> |
| if (r.output != null) |
| object : Reporter by reporter { |
| override fun afterAll() { |
| reporter.afterAll() |
| output.close() |
| } |
| } |
| else |
| reporter |
| } |
| }.toTypedArray()) |
| // load .editorconfig |
| val userData = ( |
| EditorConfig.of(workDir) |
| ?.also { editorConfig -> |
| if (debug) { |
| System.err.println("[DEBUG] Discovered .editorconfig (${editorConfig.path.parent})") |
| System.err.println("[DEBUG] ${editorConfig.mapKeys { it.key }} loaded from .editorconfig") |
| } |
| } |
| ?: emptyMap<String, String>() |
| ) + mapOf("android" to android.toString()) |
| data class LintErrorWithCorrectionInfo(val err: LintError, val corrected: Boolean) |
| fun lintErrorFrom(e: Exception): LintError = when (e) { |
| is ParseException -> |
| LintError(e.line, e.col, "", |
| "Not a valid Kotlin file (${e.message?.toLowerCase()})") |
| is RuleExecutionException -> { |
| if (debug) { |
| System.err.println("[DEBUG] Internal Error (${e.ruleId})") |
| e.printStackTrace(System.err) |
| } |
| LintError(e.line, e.col, "", "Internal Error (${e.ruleId}). " + |
| "Please create a ticket at https://github.com/shyiko/ktlint/issue " + |
| "(if possible, provide the source code that triggered an error)") |
| } |
| else -> throw e |
| } |
| val tripped = AtomicBoolean() |
| fun process(fileName: String, fileContent: String): List<LintErrorWithCorrectionInfo> { |
| if (debug) { |
| System.err.println("[DEBUG] Checking ${ |
| if (relative && fileName != "<text>") File(fileName).toRelativeString(File(workDir)) else fileName |
| }") |
| } |
| val result = ArrayList<LintErrorWithCorrectionInfo>() |
| if (format) { |
| val formattedFileContent = try { |
| format(fileName, fileContent, rp.map { it.second.get() }, userData) { err, corrected -> |
| if (!corrected) { |
| result.add(LintErrorWithCorrectionInfo(err, corrected)) |
| } |
| } |
| } catch (e: Exception) { |
| result.add(LintErrorWithCorrectionInfo(lintErrorFrom(e), false)) |
| tripped.set(true) |
| fileContent // making sure `cat file | ktlint --stdint > file` is (relatively) safe |
| } |
| if (stdin) { |
| println(formattedFileContent) |
| } else { |
| if (fileContent !== formattedFileContent) { |
| File(fileName).writeText(formattedFileContent, charset("UTF-8")) |
| } |
| } |
| } else { |
| try { |
| lint(fileName, fileContent, rp.map { it.second.get() }, userData) { err -> |
| tripped.set(true) |
| result.add(LintErrorWithCorrectionInfo(err, false)) |
| } |
| } catch (e: Exception) { |
| result.add(LintErrorWithCorrectionInfo(lintErrorFrom(e), false)) |
| } |
| } |
| return result |
| } |
| if (limit < 0) { |
| limit = Int.MAX_VALUE |
| } |
| val (fileNumber, errorNumber) = Pair(AtomicInteger(), AtomicInteger()) |
| fun report(fileName: String, errList: List<LintErrorWithCorrectionInfo>) { |
| fileNumber.incrementAndGet() |
| val errListLimit = minOf(errList.size, maxOf(limit - errorNumber.get(), 0)) |
| errorNumber.addAndGet(errListLimit) |
| reporter.before(fileName) |
| errList.head(errListLimit).forEach { (err, corrected) -> |
| reporter.onLintError(fileName, err, corrected) |
| } |
| reporter.after(fileName) |
| } |
| reporter.beforeAll() |
| if (stdin) { |
| report("<text>", process("<text>", String(System.`in`.readBytes()))) |
| } else { |
| val pathIterator = when { |
| patterns.isEmpty() -> |
| Glob.from("**/*.kt", "**/*.kts") |
| .iterate(Paths.get(workDir), Glob.IterationOption.SKIP_HIDDEN) |
| else -> |
| Glob.from(*patterns.map { expandTilde(it) }.toTypedArray()) |
| .iterate(Paths.get(workDir)) |
| } |
| pathIterator |
| .asSequence() |
| .takeWhile { errorNumber.get() < limit } |
| .map(Path::toFile) |
| .map { file -> |
| Callable { file to process(file.path, file.readText()) } |
| } |
| .parallel({ (file, errList) -> |
| report(if (relative) file.toRelativeString(File(workDir)) else file.path, errList) }) |
| } |
| reporter.afterAll() |
| if (debug) { |
| System.err.println("[DEBUG] ${(System.currentTimeMillis() - start) |
| }ms / $fileNumber file(s) / $errorNumber error(s)") |
| } |
| if (tripped.get()) { |
| exitProcess(1) |
| } |
| } |
| |
| fun installGitPreCommitHook() { |
| if (!File(".git").isDirectory) { |
| System.err.println(".git directory not found. " + |
| "Are you sure you are inside project root directory?") |
| exitProcess(1) |
| } |
| val hooksDir = File(".git", "hooks") |
| hooksDir.mkdirsOrFail() |
| val preCommitHookFile = File(hooksDir, "pre-commit") |
| val expectedPreCommitHook = ClassLoader.getSystemClassLoader() |
| .getResourceAsStream("ktlint-git-pre-commit-hook${if (android) "-android" else ""}.sh").readBytes() |
| // backup existing hook (if any) |
| val actualPreCommitHook = try { preCommitHookFile.readBytes() } catch (e: FileNotFoundException) { null } |
| if (actualPreCommitHook != null && !actualPreCommitHook.isEmpty() && !Arrays.equals(actualPreCommitHook, expectedPreCommitHook)) { |
| val backupFile = File(hooksDir, "pre-commit.ktlint-backup." + hex(actualPreCommitHook)) |
| System.err.println(".git/hooks/pre-commit -> $backupFile") |
| preCommitHookFile.copyTo(backupFile, overwrite = true) |
| } |
| // > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit |
| preCommitHookFile.writeBytes(expectedPreCommitHook) |
| preCommitHookFile.setExecutable(true) |
| System.err.println(".git/hooks/pre-commit installed") |
| } |
| |
| fun applyToIDEA() { |
| try { |
| val workDir = Paths.get(".") |
| if (!forceApply) { |
| val fileList = IntellijIDEAIntegration.apply(workDir, true, android) |
| System.err.println("The following files are going to be updated:\n\n\t" + |
| fileList.joinToString("\n\t") + |
| "\n\nDo you wish to proceed? [y/n]\n" + |
| "(in future, use -y flag if you wish to skip confirmation)") |
| val scanner = Scanner(System.`in`) |
| |
| val res = generateSequence { |
| try { scanner.next() } catch (e: NoSuchElementException) { null } |
| } |
| .filter { line -> !line.trim().isEmpty() } |
| .first() |
| if (!"y".equals(res, ignoreCase = true)) { |
| System.err.println("(update canceled)") |
| exitProcess(1) |
| } |
| } |
| IntellijIDEAIntegration.apply(workDir, false, android) |
| } catch (e: IntellijIDEAIntegration.ProjectNotFoundException) { |
| System.err.println(".idea directory not found. " + |
| "Are you sure you are inside project root directory?") |
| exitProcess(1) |
| } |
| System.err.println("(updated)") |
| System.err.println("\nPlease restart your IDE") |
| System.err.println("(if you experience any issues please report them at https://github.com/shyiko/ktlint)") |
| } |
| |
| fun hex(input: ByteArray) = BigInteger(MessageDigest.getInstance("SHA-256").digest(input)).toString(16) |
| |
| // a complete solution would be to implement https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html |
| // this implementation takes care only of the most commonly used case (~/) |
| fun expandTilde(path: String) = path.replaceFirst(Regex("^~"), System.getProperty("user.home")) |
| |
| fun <T> List<T>.head(limit: Int) = if (limit == size) this else this.subList(0, limit) |
| |
| fun buildDependencyResolver(): MavenDependencyResolver { |
| val mavenLocal = File(File(System.getProperty("user.home"), ".m2"), "repository") |
| mavenLocal.mkdirsOrFail() |
| val dependencyResolver = MavenDependencyResolver( |
| mavenLocal, |
| listOf( |
| RemoteRepository.Builder( |
| "central", "default", "http://repo1.maven.org/maven2/" |
| ).setSnapshotPolicy(RepositoryPolicy(false, UPDATE_POLICY_NEVER, |
| CHECKSUM_POLICY_IGNORE)).build(), |
| RemoteRepository.Builder( |
| "bintray", "default", "http://jcenter.bintray.com" |
| ).setSnapshotPolicy(RepositoryPolicy(false, UPDATE_POLICY_NEVER, |
| CHECKSUM_POLICY_IGNORE)).build(), |
| RemoteRepository.Builder( |
| "jitpack", "default", "http://jitpack.io").build() |
| ) + repositories.map { repository -> |
| val colon = repository.indexOf("=").apply { |
| if (this == -1) { throw RuntimeException("$repository is not a valid repository entry " + |
| "(make sure it's provided as <id>=<url>") } |
| } |
| val id = repository.substring(0, colon) |
| val url = repository.substring(colon + 1) |
| RemoteRepository.Builder(id, "default", url).build() |
| }, |
| forceUpdate |
| ) |
| if (debug) { |
| dependencyResolver.setTransferEventListener { e -> |
| System.err.println("[DEBUG] Transfer ${e.type.toString().toLowerCase()} ${e.resource.repositoryUrl}" + |
| e.resource.resourceName + (e.exception?.let { " (${it.message})" } ?: "")) |
| } |
| } |
| return dependencyResolver |
| } |
| |
| fun loadJARs(dependencyResolver: MavenDependencyResolver, artifacts: List<String>) { |
| (ClassLoader.getSystemClassLoader() as java.net.URLClassLoader) |
| .addURLs(artifacts.flatMap { artifact -> |
| if (debug) { |
| System.err.println("[DEBUG] Resolving $artifact") |
| } |
| val result = try { |
| dependencyResolver.resolve(DefaultArtifact(artifact)).map { it.toURI().toURL() } |
| } catch (e: IllegalArgumentException) { |
| val file = File(expandTilde(artifact)) |
| if (!file.exists()) { |
| System.err.println("Error: $artifact does not exist") |
| exitProcess(1) |
| } |
| listOf(file.toURI().toURL()) |
| } catch (e: RepositoryException) { |
| if (debug) { |
| e.printStackTrace() |
| } |
| System.err.println("Error: $artifact wasn't found") |
| exitProcess(1) |
| } |
| if (debug) { |
| result.forEach { url -> System.err.println("[DEBUG] Loading $url") } |
| } |
| result |
| }) |
| } |
| |
| fun parseQuery(query: String) = query.split("&") |
| .fold(LinkedHashMap<String, String>()) { map, s -> |
| if (!s.isEmpty()) { |
| s.split("=", limit = 2).let { e -> map.put(e[0], |
| URLDecoder.decode(e.getOrElse(1) { "true" }, "UTF-8")) } |
| } |
| map |
| } |
| |
| fun lint(fileName: String, text: String, ruleSets: Iterable<RuleSet>, userData: Map<String, String>, |
| cb: (e: LintError) -> Unit) = |
| if (fileName.endsWith(".kt", ignoreCase = true)) KtLint.lint(text, ruleSets, userData, cb) else |
| KtLint.lintScript(text, ruleSets, userData, cb) |
| |
| fun format(fileName: String, text: String, ruleSets: Iterable<RuleSet>, userData: Map<String, String>, |
| cb: (e: LintError, corrected: Boolean) -> Unit): String = |
| if (fileName.endsWith(".kt", ignoreCase = true)) KtLint.format(text, ruleSets, userData, cb) else |
| KtLint.formatScript(text, ruleSets, userData, cb) |
| |
| fun java.net.URLClassLoader.addURLs(url: Iterable<java.net.URL>) { |
| val method = java.net.URLClassLoader::class.java.getDeclaredMethod("addURL", java.net.URL::class.java) |
| method.isAccessible = true |
| url.forEach { method.invoke(this, it) } |
| } |
| |
| fun <T>Sequence<Callable<T>>.parallel(cb: (T) -> Unit, |
| numberOfThreads: Int = Runtime.getRuntime().availableProcessors()) { |
| val q = ArrayBlockingQueue<Future<T>>(numberOfThreads) |
| val pill = object : Future<T> { |
| |
| override fun isDone(): Boolean { throw UnsupportedOperationException() } |
| override fun get(timeout: Long, unit: TimeUnit): T { throw UnsupportedOperationException() } |
| override fun get(): T { throw UnsupportedOperationException() } |
| override fun cancel(mayInterruptIfRunning: Boolean): Boolean { throw UnsupportedOperationException() } |
| override fun isCancelled(): Boolean { throw UnsupportedOperationException() } |
| } |
| val consumer = Thread(Runnable { |
| while (true) { |
| val future = q.poll(Long.MAX_VALUE, TimeUnit.NANOSECONDS) |
| if (future === pill) { |
| break |
| } |
| cb(future.get()) |
| } |
| }) |
| consumer.start() |
| val executorService = Executors.newCachedThreadPool() |
| for (v in this) { |
| q.put(executorService.submit(v)) |
| } |
| q.put(pill) |
| executorService.shutdown() |
| consumer.join() |
| } |
| } |