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

sjsonnet.Importer.scala Maven / Gradle / Ivy

package sjsonnet

import java.io.{BufferedInputStream, BufferedReader, ByteArrayInputStream, File, FileInputStream, FileReader, InputStream, RandomAccessFile, Reader, StringReader}
import java.nio.file.Files
import java.security.MessageDigest
import scala.collection.mutable
import fastparse.{IndexedParserInput, Parsed, ParserInput}

import java.nio.charset.StandardCharsets


/** Resolve and read imported files */
abstract class Importer {
  def resolve(docBase: Path, importName: String): Option[Path]
  def read(path: Path): Option[ResolvedFile]

  def resolveAndRead(docBase: Path, importName: String): Option[(Path, ResolvedFile)] = for {
    path <- resolve(docBase, importName)
    txt <- read(path)
  } yield (path, txt)

  def resolveAndReadOrFail(value: String, pos: Position)(implicit ev: EvalErrorScope): (Path, ResolvedFile) =
    resolveAndRead(pos.fileScope.currentFile.parent(), value)
      .getOrElse(Error.fail("Couldn't import file: " + pprint.Util.literalize(value), pos))
}

object Importer {
  val empty: Importer = new Importer {
    def resolve(docBase: Path, importName: String): Option[Path] = None
    def read(path: Path): Option[ResolvedFile] = None
  }
}

case class FileParserInput(file: File) extends ParserInput {

  private[this] val bufferedFile = new BufferedRandomAccessFile(file.getAbsolutePath, 1024 * 8)

  private lazy val fileLength = file.length.toInt

  override def apply(index: Int): Char = {
    bufferedFile.readChar(index)
  }

  override def dropBuffer(index: Int): Unit = {}

  override def slice(from: Int, until: Int): String = {
    bufferedFile.readString(from, until)
  }

  override def length: Int = fileLength

  override def innerLength: Int = length

  override def isReachable(index: Int): Boolean = index < length

  override def checkTraceable(): Unit = {}

  private[this] lazy val lineNumberLookup: Array[Int] = {
    val lines = mutable.ArrayBuffer[Int]()
    val bufferedStream = new BufferedInputStream(new FileInputStream(file))
    var byteRead: Int = 0
    var currentPosition = 0

    while ({ byteRead = bufferedStream.read(); byteRead != -1 }) {
      if (byteRead == '\n') {
        lines += currentPosition + 1
      }
      currentPosition += 1
    }

    bufferedStream.close()

    lines.toArray
  }

  def prettyIndex(index: Int): String = {
    val line = lineNumberLookup.indexWhere(_ > index) match {
      case -1 => lineNumberLookup.length - 1
      case n => math.max(0, n - 1)
    }
    val col = index - lineNumberLookup(line)
    s"${line + 1}:${col + 1}"
  }
}

class BufferedRandomAccessFile(fileName: String, bufferSize: Int) {

  // The file is opened in read-only mode
  private val file = new RandomAccessFile(fileName, "r")

  private val buffer = new Array[Byte](bufferSize)

  private var bufferStart: Long = -1

  private var bufferEnd: Long = -1

  private val fileLength: Long = file.length()

  private def fillBuffer(position: Long): Unit = {
    if (file.getFilePointer() != position) {
      file.seek(position)
    }
    val bytesRead = file.read(buffer, 0, bufferSize)
    bufferStart = position
    bufferEnd = position + bytesRead
  }

  def readChar(index: Long): Char = {
    if (index >= fileLength) {
      throw new IndexOutOfBoundsException(s"Index $index is out of bounds for file of length $fileLength")
    }
    if (index < bufferStart || index >= bufferEnd) {
      fillBuffer(index)
    }
    buffer((index - bufferStart).toInt).toChar
  }

  def readString(from: Long, until: Long): String = {
    if (!(from < fileLength && until <= fileLength && from <= until)) {
      throw new IndexOutOfBoundsException(s"Invalid range: $from-$until for file of length $fileLength")
    }
    val length = (until - from).toInt

    if (from >= bufferStart && until <= bufferEnd) {
      // Range is within the buffer
      new String(buffer, (from - bufferStart).toInt, length, StandardCharsets.UTF_8)
    } else {
      // Range is outside the buffer
      val stringBytes = new Array[Byte](length)
      file.seek(from)
      file.readFully(stringBytes, 0, length)
      new String(stringBytes, StandardCharsets.UTF_8)
    }
  }

  def close(): Unit = {
    file.close()
  }
}

trait ResolvedFile {
  /**
   * Get an efficient parser input for this resolved file. Large files will be read from disk
   * (buffered reads), while small files will be served from memory.
   */
  def getParserInput(): ParserInput

  // Use this to read the file as a string. This is generally used for `importstr`
  def readString(): String

  // Get a content hash of the file suitable for detecting changes in a given file.
  def contentHash(): String
}

case class StaticResolvedFile(content: String) extends ResolvedFile {
  def getParserInput(): ParserInput = IndexedParserInput(content)

  def readString(): String = content

  // We just cheat, the content hash can be the content itself for static imports
  lazy val contentHash: String = content
}

class CachedImporter(parent: Importer) extends Importer {
  val cache = mutable.HashMap.empty[Path, ResolvedFile]

  def resolve(docBase: Path, importName: String): Option[Path] = parent.resolve(docBase, importName)

  def read(path: Path): Option[ResolvedFile] = cache.get(path) match {
    case s @ Some(x) =>
      if(x == null) None else s
    case None =>
      val x = parent.read(path)
      cache.put(path, x.getOrElse(null))
      x
  }
}

class CachedResolver(
  parentImporter: Importer,
  val parseCache: ParseCache,
  strictImportSyntax: Boolean,
  internedStrings: mutable.HashMap[String, String],
  internedStaticFieldSets: mutable.HashMap[Val.StaticObjectFieldSet, java.util.LinkedHashMap[String, java.lang.Boolean]]) extends CachedImporter(parentImporter) {

  def parse(path: Path, content: ResolvedFile)(implicit ev: EvalErrorScope): Either[Error, (Expr, FileScope)] = {
    parseCache.getOrElseUpdate((path, content.contentHash.toString), {
      val parsed = fastparse.parse(content.getParserInput(), new Parser(path, strictImportSyntax, internedStrings, internedStaticFieldSets).document(_)) match {
        case f @ Parsed.Failure(_, _, _) =>
          val traced = f.trace()
          val pos = new Position(new FileScope(path), traced.index)
          Left(new ParseError(traced.msg).addFrame(pos))
        case Parsed.Success(r, _) => Right(r)
      }
      parsed.flatMap { case (e, fs) => process(e, fs) }
    })
  }

  def process(expr: Expr, fs: FileScope): Either[Error, (Expr, FileScope)] = Right((expr, fs))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy