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

com.twitter.scrooge.frontend.Importer.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2011 Twitter, 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.twitter.scrooge.frontend

import java.io.File
import java.util.zip.{ZipFile, ZipEntry}
import scala.io.Source

// an imported file should have its own path available for nested importing.
case class FileContents(importer: Importer, data: String, thriftFilename: Option[String])

// an Importer turns a filename into its string contents.
trait Importer extends (String => Option[FileContents]) {
  private[scrooge] def canonicalPaths: Seq[String] // for tests
  def lastModified(filename: String): Option[Long]
  def +:(head: Importer): Importer = MultiImporter(Seq(head, this))
  /**
   * @param filename a filename to resolve.
   * @return the absolute path used to resolve it. Used to cache importer results.
   */
  private[scrooge] def getResolvedPath(filename: String): Option[String] = None
}

object Importer {
  /**
   * @param path Path of a directory or a zip/jar file
   * @return the importer that will look into the directory or zip/jar
   *         file specified by path
   */
  def apply(path: String): Importer =
    apply(new File(path).getCanonicalFile)

  /**
   * @param file File of a directory or a zip/jar file
   * @return the importer that will look into the directory or zip/jar
   *         file specified by path
   */
  def apply(file: File): Importer = {
    if (file.isDirectory)
      DirImporter(file)
    else if (file.isFile &&
      (file.getName.toLowerCase.endsWith(".jar") ||
        file.getName.toLowerCase.endsWith(".zip")))
      // A more accurate way to determine whether it's a zip file is to call
      //     new ZipFile(file)
      // and see if there is an exception. But we decide to use the filename
      // because more often than not it indicates user's intention.
      ZipImporter(file)
    else
      NullImporter
  }

  def apply(paths: Seq[String]): Importer =
    MultiImporter(paths map { Importer(_) })
}

case class DirImporter(dir: File) extends Importer {
  private[scrooge] def canonicalPaths = Seq(dir.getCanonicalPath) // for test

  private[this] def resolve(filename: String): Option[(File, Importer)] = {
    val file = {
      val file = new File(filename)
      if (file.isAbsolute)
        file
      else
        new File(dir, filename)
    }
    if (file.canRead) {
      // if the file does not exactly locate in dir, we need to add a new relative path
      val importer =
        if (file.getParentFile.getCanonicalPath == dir.getCanonicalPath)
          this
        else
          Importer(file.getParentFile) +: this
      Some(file, importer)
    } else None
  }

  def lastModified(filename: String): Option[Long] =
    resolve(filename) map { case (file, _) => file.lastModified }

  def apply(filename: String): Option[FileContents] =
    resolve(filename) map { case (file, importer) =>
      FileContents(importer, Source.fromFile(file, "UTF-8").mkString, Some(file.getName))
    }

  override private[scrooge] def getResolvedPath(filename: String): Option[String] = {
    resolve(filename).map { case (file, _) => file.getCanonicalPath }
  }
}

// jar files are just zip files, so this will work with both .jar and .zip files
case class ZipImporter(file: File) extends Importer {
  private[this] val zipFile = new ZipFile(file)

  lazy val canonicalPaths = Seq(file.getCanonicalPath)

  private def resolve(filename: String): Option[ZipEntry] =
    Option(zipFile.getEntry(filename))

  // uses the lastModified time of the zip/jar file
  def lastModified(filename: String): Option[Long] =
    resolve(filename) map { _ => file.lastModified }

  def apply(filename: String): Option[FileContents] =
    resolve(filename) map { entry =>
      FileContents(this, Source.fromInputStream(zipFile.getInputStream(entry), "UTF-8").mkString, Some(entry.getName))
    }

  private[this] def canResolve(filename: String): Boolean = resolve(filename).isDefined

  override private[scrooge] def getResolvedPath(filename: String): Option[String] = {
    if (canResolve(filename))
      Some(new File(file, filename).getCanonicalPath)
    else
      None
  }
}

case class MultiImporter(importers: Seq[Importer]) extends Importer {
  lazy val canonicalPaths = importers flatMap { _.canonicalPaths }

  private def first[A](f: Importer => Option[A]): Option[A] =
    importers.foldLeft[Option[A]](None) { (accum, next) => accum orElse f(next) }

  def lastModified(filename: String): Option[Long] =
    first[Long] { _.lastModified(filename) }

  def apply(filename: String): Option[FileContents] =
    first[FileContents] { _(filename) } map {
      case FileContents(importer, data, thriftFilename) =>
        // take the importer that found the file and prepend it to importer list returned,
        // that way it is used first when finding relative imports
        FileContents(importer +: this, data, thriftFilename)
    }

  override def +:(head: Importer): Importer = MultiImporter(head +: importers)

  override private[scrooge] def getResolvedPath(filename: String): Option[String] =
    first[String] { _.getResolvedPath(filename) }
}

object NullImporter extends Importer {
  val canonicalPaths = Nil
  def lastModified(filename: String) = None
  def apply(filename: String) = None
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy