All Downloads are FREE. Search and download functionalities are using the official Maven repository.

name.remal.gradle_plugins.dsl.artifact.Artifact.kt Maven / Gradle / Ivy

There is a newer version: 1.9.2
Show newest version
package name.remal.gradle_plugins.dsl.artifact

import com.google.common.collect.MultimapBuilder
import com.google.common.collect.SetMultimap
import name.remal.emptyStream
import name.remal.gradle_plugins.dsl.cache.BaseCache
import name.remal.gradle_plugins.dsl.utils.PathMatcher
import name.remal.gradle_plugins.dsl.utils.readJavaModuleName
import name.remal.loadProperties
import name.remal.nullIfEmpty
import name.remal.plus
import name.remal.storeAsString
import name.remal.uncheckedCast
import name.remal.use
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassReader.SKIP_CODE
import org.objectweb.asm.ClassReader.SKIP_DEBUG
import org.objectweb.asm.ClassReader.SKIP_FRAMES
import org.objectweb.asm.Opcodes.ACC_ANNOTATION
import org.objectweb.asm.Type.getType
import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.ClassNode
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.nio.charset.StandardCharsets.UTF_8
import java.util.Properties
import java.util.zip.ZipException
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
import kotlin.collections.set
import kotlin.text.isNotEmpty
import kotlin.text.trim

class Artifact(file: File) : BaseHasEntries(), Comparable {

    val file: File = file.absoluteFile

    override val entryNames: Set by lazy {
        val result = sortedSetOf()
        if (file.isFile) {
            try {
                ZipFile(file).use { zipFile ->
                    val entries = zipFile.entries()
                    while (entries.hasMoreElements()) {
                        val entry = entries.nextElement()
                        if (entry.isDirectory) continue
                        result += entry.name
                    }
                }

            } catch (e: ZipException) {
                // do nothing
            }

        } else if (file.isDirectory) {
            file.walk().filter { file != it }.filter(File::isFile).forEach { resourceFile ->
                result += resourceFile.relativeTo(file).path.replace(File.separatorChar, '/')
            }
        }
        return@lazy result.toSet()
    }

    override fun openStream(entryName: String): InputStream {
        if (file.isFile) {
            val zipFile = ZipFile(file)
            val entry = zipFile.getEntry(entryName) ?: throw ArtifactEntryNotFoundException("Artifact entry not found: $entryName")
            val inputStream = zipFile.getInputStream(entry)!!
            return object : BufferedInputStream(inputStream) {
                override fun close() {
                    super.close()
                    zipFile.close()
                }
            }

        } else if (file.isDirectory) {
            try {
                return File(file, entryName).inputStream()
            } catch (e: FileNotFoundException) {
                throw ArtifactEntryNotFoundException("Artifact entry not found: $entryName", e)
            }

        } else {
            throw ArtifactFileNotFoundException("Artifact file not found: $file")
        }
    }

    @Suppress("ComplexMethod")
    override fun forEachEntry(pattern: String?, action: (entry: HasEntries.Entry) -> Unit) {
        val matcher = pattern.nullIfEmpty()?.let { PathMatcher(it) }
        if (file.isFile) {
            FileInputStream(file).use { fileInput ->
                ZipInputStream(fileInput).use { zipInput ->
                    while (true) {
                        val entry = zipInput.nextEntry ?: break
                        if (entry.isDirectory) continue
                        val entryName = entry.name
                        if (matcher == null || matcher.matches(entryName)) {
                            action(HasEntries.Entry(entryName, { zipInput }))
                        }
                    }
                }
            }

        } else if (file.isDirectory) {
            file.walk().filter { file != it }.forEach { entryFile ->
                val entryName = entryFile.relativeTo(file).invariantSeparatorsPath
                if (matcher == null || matcher.matches(entryName)) {
                    if (entryFile.isFile) {
                        var streamToClose: InputStream? = null
                        try {
                            action(HasEntries.Entry(entryName, { FileInputStream(entryFile).also { streamToClose = it } }))
                        } finally {
                            streamToClose?.close()
                        }
                    }
                }
            }
        }
    }

    val javaModuleName: String? by lazy { readJavaModuleName(file) }

    private val annotationsInfo: AnnotationsInfo by lazy {
        AnnotationsInfoPersistenceCache.read()?.get(file)?.let { return@lazy it }
        return@lazy AnnotationsInfo().also { result ->
            forEachEntry("**/*.class") { classEntry ->
                val classNode = ClassNode().also {
                    val bytecode = classEntry.inputStream.readBytes()
                    ClassReader(bytecode).accept(it, SKIP_CODE or SKIP_DEBUG or SKIP_FRAMES)
                }

                if (0 == (classNode.access and ACC_ANNOTATION)) return@forEachEntry

                result.annotationClassNames.add(classNode.name.replace('/', '.'))

                emptyStream()
                    .plus(classNode.visibleAnnotations?.stream() ?: emptyStream())
                    .plus(classNode.invisibleAnnotations?.stream() ?: emptyStream())
                    .forEach { annotationNode ->
                        val annotationClassName = getType(annotationNode.desc).className
                        result.annotationsMapping.put(annotationClassName, classNode.name.replace('/', '.'))
                    }
            }

            AnnotationsInfoPersistenceCache.write(
                (AnnotationsInfoPersistenceCache.read()?.toMutableMap() ?: mutableMapOf())
                    .also { it[file] = result }
            )
        }
    }

    override val annotationClassNames: Set by lazy { annotationsInfo.annotationClassNames.toSet() }

    override val annotationsMapping: Map> by lazy {
        annotationsInfo.annotationsMapping
            .asMap().uncheckedCast>>()
            .toMap()
    }

    private val absolutePath: String = this.file.path
    override fun toString(): String = absolutePath
    override fun equals(other: Any?) = other is Artifact && absolutePath == other.absolutePath
    override fun hashCode() = absolutePath.hashCode()
    override fun compareTo(other: Artifact) = file.compareTo(other.file)

}


private data class AnnotationsInfo(
    val annotationClassNames: MutableSet = sortedSetOf(),
    val annotationsMapping: SetMultimap = MultimapBuilder.treeKeys().treeSetValues().build()
)


private object AnnotationsInfoPersistenceCache : BaseCache, Map>("artifacts-annotation-info.properties", 1) {

    private const val lastModifiedKey = "last-modified"
    private const val annotationClassNamesKey = "annotation-class-names"
    private const val annotationsMappingKey = "annotations-mapping"

    override val serializer: (value: Map) -> ByteArray? = serializer@{ cacheValue ->
        return@serializer Properties().also { props ->
            cacheValue.forEach { file, info ->
                val filePath = file.path
                props["$filePath:$lastModifiedKey"] = file.lastModified().toString()
                if (info.annotationClassNames.isNotEmpty()) {
                    props["$filePath:$annotationClassNamesKey"] = info.annotationClassNames.joinToString(";")
                }
                if (!info.annotationsMapping.isEmpty) {
                    props["$filePath:$annotationsMappingKey"] = info.annotationsMapping.asMap()
                        .filter { it.value.isNotEmpty() }
                        .mapValues { it.value.joinToString(",") }
                        .map { it.key + '=' + it.value }
                        .joinToString(";")
                }
            }
        }.storeAsString().toByteArray(UTF_8)
    }

    override val deserializer: (bytes: ByteArray) -> Map? = deserializer@{ bytes ->
        if (bytes.isEmpty()) {
            return@deserializer null

        } else {
            val fileProps = mutableMapOf>()
            val props = loadProperties(ByteArrayInputStream(bytes))
            props.forEach { key, value ->
                if (key == null || value == null) return@forEach
                val filePath = key.toString().substringBeforeLast(':')
                val field = key.toString().substringAfterLast(':')
                fileProps.computeIfAbsent(File(filePath), { mutableMapOf() })[field] = value.toString()
            }
            return@deserializer fileProps
                .filter { it.key.lastModified().toString() == it.value[lastModifiedKey] }
                .mapValues {
                    AnnotationsInfo().apply {
                        it.value[annotationClassNamesKey]
                            ?.split(';')
                            ?.map(String::trim)
                            ?.filter(String::isNotEmpty)
                            ?.forEach { annotationClassNames.add(it) }
                        it.value[annotationsMappingKey]
                            ?.split(';')
                            ?.map(String::trim)
                            ?.filter(String::isNotEmpty)
                            ?.map { it.substringBefore('=', "") to it.substringAfter('=', "") }
                            ?.filter { it.first.isNotEmpty() && it.second.isNotEmpty() }
                            ?.map { it.first to it.second.split(',').map(String::trim).filter(String::isNotEmpty) }
                            ?.forEach { annotationsMapping.putAll(it.first, it.second) }
                    }
                }
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy