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

com.uber.crumb.core.CrumbManager.kt Maven / Gradle / Ivy

/*
 * Copyright (c) 2018. Uber Technologies
 *
 * 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.
 */

@file:Suppress("UNCHECKED_CAST")

package com.uber.crumb.core

import com.sun.tools.javac.processing.JavacProcessingEnvironment
import com.uber.crumb.core.CrumbManager.Companion.OPTION_EXTRA_LOCATIONS
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import java.net.URISyntaxException
import java.net.URL
import java.net.URLClassLoader
import java.util.zip.ZipFile
import javax.annotation.processing.ProcessingEnvironment
import javax.tools.JavaFileManager
import javax.tools.StandardLocation

/**
 * A utility class that helps adding build specific objects to the jar file and their extraction later on. This
 * specifically works by reading/writing metadata in the Crumb `META-INF` location of jars from the classpath.
 *
 * Optionally, a colon-delimited list of extra locations to search for in loading can be specified via specifying the
 * [OPTION_EXTRA_LOCATIONS] option in the given [env].
 *
 * Adapted from the [Android DataBinding library](https://android.googlesource.com/platform/frameworks/data-binding/+/master).
 */
class CrumbManager(private val env: ProcessingEnvironment,
    private val crumbLog: CrumbLog) {

  companion object {
    /**
     * A colon-delimited list of extra locations to search in loading.
     */
    const val OPTION_EXTRA_LOCATIONS = "crumb.options.extraLocations"
    private const val CRUMB_PREFIX = "META-INF/com.uber.crumb/"
  }

  /**
   * This loads a given [Set]<[T]> from the Crumb `META-INF` store that matches the given [nameFilter].
   *
   * @param nameFilter a name filter to match on. Conventionally, one could use a "known" file extension used for file
   *                   names in [store].
   * @return the loaded [Set]<[T]>, or an empty set if none were found.
   */
  @Throws(IOException::class)
  fun  load(nameFilter: (String) -> Boolean): Set {
    val fileManager = (env as JavacProcessingEnvironment).context.get(JavaFileManager::class.java)
    val classLoader = fileManager.getClassLoader(StandardLocation.CLASS_PATH)
    check(classLoader is URLClassLoader) {
      "Classloader must be an instance of URLClassLoader. $classLoader"
    }

    val urlClassLoader = classLoader as URLClassLoader
    val objects = mutableSetOf()
    val extraLocations = env.options[OPTION_EXTRA_LOCATIONS]
        ?.split(":")
        ?.map { URL("file:$it") }
        ?: emptyList()
    for (url in (urlClassLoader.urLs + extraLocations)) {
      crumbLog.d("Checking url %s for Crumb data", url)
      try {
        val file = File(url.toURI())
        if (!file.exists()) {
          crumbLog.d("Cannot load file for %s", url)
          continue
        }
        if (file.isDirectory) {
          // probably exported classes dir.
          loadFromDirectory(nameFilter, file, objects)
        } else {
          // assume it is a zip file
          loadFomZipFile(nameFilter, file, objects)
        }
      } catch (e: IOException) {
        crumbLog.d("Cannot open zip file from %s", url)
      } catch (e: URISyntaxException) {
        crumbLog.d("Cannot open zip file from %s", url)
      }
    }
    return objects
  }

  @Throws(IOException::class)
  private fun  loadFromDirectory(nameFilter: (String) -> Boolean,
      from: File,
      into: MutableSet) {
    from.walkTopDown()
        .filter { nameFilter(it.name) }
        .forEach { file ->
          try {
            file.inputStream().use {
              try {
                val item = fromInputStream(it)
                item?.let {
                  into += (item as T)
                  crumbLog.d("Loaded item %s from file", item)
                }
              } catch (e: IOException) {
                crumbLog.e(e, "Could not merge in crumbs from %s", file.absolutePath)
              }
            }
          } catch (e: ClassNotFoundException) {
            crumbLog.e(e, "Could not read Crumb file. %s",
                file.absolutePath)
          }
        }
  }

  @Throws(IOException::class)
  private fun  loadFomZipFile(nameFilter: (String) -> Boolean,
      from: File,
      into: MutableSet) {
    val zipFile = ZipFile(from)
    zipFile.entries()
        .asSequence()
        .filter { nameFilter(it.name) }
        .forEach { entry ->
          try {
            zipFile.getInputStream(entry).use {
              try {
                val item = fromInputStream(it)
                crumbLog.d("loaded item $item from zip file")
                item?.let {
                  into += (item as T)
                }
              } catch (e: IOException) {
                crumbLog.e(e, "Could not merge in Crumb file from %s", from.absolutePath)
              }
            }
          } catch (e: ClassNotFoundException) {
            crumbLog.e(e, "Could not read Crumb file. %s", from.absolutePath)
          }
        }
  }

  @Throws(IOException::class, ClassNotFoundException::class)
  private fun fromInputStream(inputStream: InputStream): Serializable? {
    ObjectInputStream(inputStream).use { return it.readObject() as Serializable }
  }

  /**
   * This writes a given [Serializable] [objectToWrite] to the Crumb `META-INF` store.
   *
   * @param packageName the package name to use for the file in writing.
   * @param fileName the file name to use in writing.
   * @param objectToWrite the [Serializable] object to write.
   */
  @Throws(IOException::class)
  fun store(
      packageName: String,
      fileName: String,
      objectToWrite: Serializable) {
    try {
      env.filer.createResource(
          StandardLocation.CLASS_OUTPUT,
          "",
          "$CRUMB_PREFIX$packageName/$fileName")
          .openOutputStream()
          .use { ios ->
            ObjectOutputStream(ios).use { oos ->
              oos.writeObject(objectToWrite)
              crumbLog.d("Wrote Crumb file %s %s", packageName, fileName)
            }
          }
    } catch (e: IOException) {
      crumbLog.e(e, "Could not write to Crumb file: %s", fileName)
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy