blob: edf2989d148938118c7d1b10145350d724e3a2a6 [file] [log] [blame]
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.kotlinpoet
import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE
import java.io.ByteArrayInputStream
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStreamWriter
import java.net.URI
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.Files
import java.nio.file.Path
import javax.annotation.processing.Filer
import javax.tools.JavaFileObject
import javax.tools.JavaFileObject.Kind
import javax.tools.SimpleJavaFileObject
import javax.tools.StandardLocation
import kotlin.reflect.KClass
/**
* A Kotlin file containing top level objects like classes, objects, functions, properties, and type
* aliases.
*
* Items are output in the following order:
* - Comment
* - Annotations
* - Package
* - Imports
* - Members
*/
class FileSpec private constructor(
builder: FileSpec.Builder,
private val tagMap: TagMap = builder.buildTagMap()
) : Taggable by tagMap {
val annotations = builder.annotations.toImmutableList()
val comment = builder.comment.build()
val packageName = builder.packageName
val name = builder.name
val members = builder.members.toList()
private val memberImports = builder.memberImports.associateBy(Import::qualifiedName)
private val indent = builder.indent
@Throws(IOException::class)
fun writeTo(out: Appendable) {
// First pass: emit the entire class, just to collect the types we'll need to import.
val importsCollector = CodeWriter(NullAppendable, indent, memberImports,
columnLimit = Integer.MAX_VALUE)
emit(importsCollector)
val suggestedTypeImports = importsCollector.suggestedTypeImports()
val suggestedMemberImports = importsCollector.suggestedMemberImports()
importsCollector.close()
// Second pass: write the code, taking advantage of the imports.
val codeWriter = CodeWriter(out, indent, memberImports, suggestedTypeImports,
suggestedMemberImports)
emit(codeWriter)
codeWriter.close()
}
/** Writes this to `directory` as UTF-8 using the standard directory structure. */
@Throws(IOException::class)
fun writeTo(directory: Path) {
require(Files.notExists(directory) || Files.isDirectory(directory)) {
"path $directory exists but is not a directory."
}
var outputDirectory = directory
if (packageName.isNotEmpty()) {
for (packageComponent in packageName.split('.').dropLastWhile { it.isEmpty() }) {
outputDirectory = outputDirectory.resolve(packageComponent)
}
}
Files.createDirectories(outputDirectory)
val outputPath = outputDirectory.resolve("$name.kt")
OutputStreamWriter(Files.newOutputStream(outputPath), UTF_8).use { writer -> writeTo(writer) }
}
/** Writes this to `directory` as UTF-8 using the standard directory structure. */
@Throws(IOException::class)
fun writeTo(directory: File) = writeTo(directory.toPath())
/** Writes this to `filer`. */
@Throws(IOException::class)
fun writeTo(filer: Filer) {
val originatingElements = members.asSequence()
.filterIsInstance<OriginatingElementsHolder>()
.flatMap { it.originatingElements.asSequence() }
.toSet()
val filerSourceFile = filer.createResource(StandardLocation.SOURCE_OUTPUT,
packageName,
"$name.kt",
*originatingElements.toTypedArray()
)
try {
filerSourceFile.openWriter().use { writer -> writeTo(writer) }
} catch (e: Exception) {
try {
filerSourceFile.delete()
} catch (ignored: Exception) {
}
throw e
}
}
private fun emit(codeWriter: CodeWriter) {
if (comment.isNotEmpty()) {
codeWriter.emitComment(comment)
}
if (annotations.isNotEmpty()) {
codeWriter.emitAnnotations(annotations, inline = false)
codeWriter.emit("\n")
}
codeWriter.pushPackage(packageName)
val escapedPackageName = packageName.escapeSegmentsIfNecessary()
if (escapedPackageName.isNotEmpty()) {
codeWriter.emitCode("package·%L\n", escapedPackageName)
codeWriter.emit("\n")
}
val importedTypeNames = codeWriter.importedTypes.values.map { it.canonicalName }
val importedMemberNames = codeWriter.importedMembers.values.map { it.canonicalName }
// Aliased imports should always appear at the bottom of the imports list.
val (aliasedImports, nonAliasedImports) = memberImports.values.partition { it.alias != null }
val imports = (importedTypeNames + importedMemberNames)
.filterNot { it in memberImports.keys }
.map { it.escapeSegmentsIfNecessary() }
.plus(nonAliasedImports.map { it.toString() })
.toSortedSet()
.plus(aliasedImports.map { it.toString() }.toSortedSet())
if (imports.isNotEmpty()) {
for (import in imports) {
codeWriter.emitCode("import·%L", import)
codeWriter.emit("\n")
}
codeWriter.emit("\n")
}
members.forEachIndexed { index, member ->
if (index > 0) codeWriter.emit("\n")
when (member) {
is TypeSpec -> member.emit(codeWriter, null)
is FunSpec -> member.emit(codeWriter, null, setOf(KModifier.PUBLIC), true)
is PropertySpec -> member.emit(codeWriter, setOf(KModifier.PUBLIC))
is TypeAliasSpec -> member.emit(codeWriter)
else -> throw AssertionError()
}
}
codeWriter.popPackage()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null) return false
if (javaClass != other.javaClass) return false
return toString() == other.toString()
}
override fun hashCode() = toString().hashCode()
override fun toString() = buildString { writeTo(this) }
fun toJavaFileObject(): JavaFileObject {
val uri = URI.create((if (packageName.isEmpty())
name else
packageName.replace('.', '/') + '/' + name) + ".kt")
return object : SimpleJavaFileObject(uri, Kind.SOURCE) {
private val lastModified = System.currentTimeMillis()
override fun getCharContent(ignoreEncodingErrors: Boolean): String {
return this@FileSpec.toString()
}
override fun openInputStream(): InputStream {
return ByteArrayInputStream(getCharContent(true).toByteArray(UTF_8))
}
override fun getLastModified() = lastModified
}
}
@JvmOverloads
fun toBuilder(packageName: String = this.packageName, name: String = this.name): Builder {
val builder = Builder(packageName, name)
builder.annotations.addAll(annotations)
builder.comment.add(comment)
builder.members.addAll(this.members)
builder.indent = indent
builder.memberImports.addAll(memberImports.values)
builder.tags += tagMap.tags
return builder
}
class Builder internal constructor(
val packageName: String,
val name: String
) : Taggable.Builder<FileSpec.Builder> {
internal val comment = CodeBlock.builder()
internal val memberImports = sortedSetOf<Import>()
internal var indent = DEFAULT_INDENT
internal val members = mutableListOf<Any>()
override val tags = mutableMapOf<KClass<*>, Any>()
val annotations = mutableListOf<AnnotationSpec>()
/**
* Add an annotation to the file.
*
* The annotation must either have a [`file` use-site target][AnnotationSpec.UseSiteTarget.FILE]
* or not have a use-site target specified (in which case it will be changed to `file`).
*/
fun addAnnotation(annotationSpec: AnnotationSpec) = apply {
when (annotationSpec.useSiteTarget) {
FILE -> annotations += annotationSpec
null -> annotations += annotationSpec.toBuilder().useSiteTarget(FILE).build()
else -> error(
"Use-site target ${annotationSpec.useSiteTarget} not supported for file annotations.")
}
}
fun addAnnotation(annotation: ClassName) =
addAnnotation(AnnotationSpec.builder(annotation).build())
fun addAnnotation(annotation: Class<*>) = addAnnotation(annotation.asClassName())
fun addAnnotation(annotation: KClass<*>) = addAnnotation(annotation.asClassName())
fun addComment(format: String, vararg args: Any) = apply {
comment.add(format.replace(' ', '·'), *args)
}
fun addType(typeSpec: TypeSpec) = apply {
members += typeSpec
}
fun addFunction(funSpec: FunSpec) = apply {
require(!funSpec.isConstructor && !funSpec.isAccessor) {
"cannot add ${funSpec.name} to file $name"
}
members += funSpec
}
fun addProperty(propertySpec: PropertySpec) = apply {
members += propertySpec
}
fun addTypeAlias(typeAliasSpec: TypeAliasSpec) = apply {
members += typeAliasSpec
}
fun addImport(constant: Enum<*>) = addImport(
(constant as java.lang.Enum<*>).getDeclaringClass().asClassName(), constant.name)
fun addImport(`class`: Class<*>, vararg names: String) = apply {
require(names.isNotEmpty()) { "names array is empty" }
addImport(`class`.asClassName(), names.toList())
}
fun addImport(`class`: KClass<*>, vararg names: String) = apply {
require(names.isNotEmpty()) { "names array is empty" }
addImport(`class`.asClassName(), names.toList())
}
fun addImport(className: ClassName, vararg names: String) = apply {
require(names.isNotEmpty()) { "names array is empty" }
addImport(className, names.toList())
}
fun addImport(`class`: Class<*>, names: Iterable<String>) =
addImport(`class`.asClassName(), names)
fun addImport(`class`: KClass<*>, names: Iterable<String>) =
addImport(`class`.asClassName(), names)
fun addImport(className: ClassName, names: Iterable<String>) = apply {
require("*" !in names) { "Wildcard imports are not allowed" }
for (name in names) {
memberImports += Import(className.canonicalName + "." + name)
}
}
fun addImport(packageName: String, vararg names: String) = apply {
require(names.isNotEmpty()) { "names array is empty" }
addImport(packageName, names.toList())
}
fun addImport(packageName: String, names: Iterable<String>) = apply {
require("*" !in names) { "Wildcard imports are not allowed" }
for (name in names) {
memberImports += if (packageName.isNotEmpty()) {
Import("$packageName.$name")
} else {
Import(name)
}
}
}
fun addAliasedImport(`class`: Class<*>, `as`: String) =
addAliasedImport(`class`.asClassName(), `as`)
fun addAliasedImport(`class`: KClass<*>, `as`: String) =
addAliasedImport(`class`.asClassName(), `as`)
fun addAliasedImport(className: ClassName, `as`: String) = apply {
memberImports += Import(className.canonicalName, `as`)
}
fun addAliasedImport(className: ClassName, memberName: String, `as`: String) = apply {
memberImports += Import("${className.canonicalName}.$memberName", `as`)
}
fun addAliasedImport(memberName: MemberName, `as`: String) = apply {
memberImports += Import(memberName.canonicalName, `as`)
}
fun indent(indent: String) = apply {
this.indent = indent
}
fun build(): FileSpec {
for (annotationSpec in annotations) {
if (annotationSpec.useSiteTarget != FILE) {
error(
"Use-site target ${annotationSpec.useSiteTarget} not supported for file annotations.")
}
}
return FileSpec(this)
}
}
companion object {
@JvmStatic fun get(packageName: String, typeSpec: TypeSpec): FileSpec {
val fileName = typeSpec.name
?: throw IllegalArgumentException("file name required but type has no name")
return builder(packageName, fileName).addType(typeSpec).build()
}
@JvmStatic fun builder(packageName: String, fileName: String) = Builder(packageName, fileName)
}
}
internal const val DEFAULT_INDENT = " "