Fantom

 

//
// Copyright (c) 2011, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//    3 May 11  Brian Frank  Creation
//

using concurrent

**
** FileRepoDb maintains a mutable cache of the current database
** of PodSpecs.  It handles implementation of all the actor messaging.
**
internal class FileRepoDb
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  new make(FileRepo repo)
  {
    this.repo = repo
    this.dir  = repo.dir
    this.log  = Log.get("fanr")
    // this.log.level = LogLevel.debug
  }

//////////////////////////////////////////////////////////////////////////
// Dispatch
//////////////////////////////////////////////////////////////////////////

  Obj? dispatch(FileRepoMsg msg)
  {
    switch (msg.id)
    {
      case FileRepoMsg.load:     return load
      case FileRepoMsg.find:     return find(msg.a, msg.b)
      case FileRepoMsg.query:    return query(msg.a, msg.b)
      case FileRepoMsg.publish:  return publish(msg.a)
      case FileRepoMsg.refresh:  return refresh
      default:                   throw Err("Unknown msg: $msg")
    }
  }

//////////////////////////////////////////////////////////////////////////
// Load
//////////////////////////////////////////////////////////////////////////

  private Obj? load()
  {
    t1 := Duration.now
    log.debug("FileRepoDb.loading...")

    dir.listDirs.each |podDir| { loadPod(podDir) }

    t2 := Duration.now
    log.debug("FileRepoDb.loaded ($podDirs.size pods loaded in ${(t2-t1).toLocale})")
    return null
  }

  private Void loadPod(File dir)
  {
    // look thru directory to find current version
    Version? curVer := null
    File? curFile := null
    dir.listFiles.each |file|
    {
      n := file.name
      dash := n.indexr("-")
      if (!n.endsWith(".pod") || dash == null) return
      ver := Version.fromStr(n[dash+1..-5], false)
      if (ver == null) return
      if (curVer == null || ver > curVer)
      {
        curVer = ver
        curFile = file
      }
    }

    // if we found a current version, load it
    if (curFile != null)
    {
      try
      {
        curSpec := PodSpec.load(curFile)
        podDirs[curSpec.name] = PodDir(dir, curSpec)
      }
      catch (Err e)
      {
        log.err("Corrupt pod: $curFile", e)
      }
    }
  }

  private Void loadAll(PodDir podDir)
  {
    // if already loaded, we can bail
    if (podDir.all != null) return

    // load all files
    podDir.all = PodSpec[,]
    podDir.dir.list.each |file|
    {
      if (file.ext != "pod") return
      try
        podDir.all.add(PodSpec.load(file))
      catch (Err e)
        log.err("Corrupt pod: $file", e)
    }

    // sort by highest to lowest version
    podDir.sortAll
  }

  private Obj? refresh()
  {
    podDirs.clear
    return load
  }

//////////////////////////////////////////////////////////////////////////
// Find
//////////////////////////////////////////////////////////////////////////

  private PodSpec? find(Str name, Version? ver)
  {
    // lookup dir record for name
    dir := podDirs[name]
    if (dir == null) return null

    // if version null, then find latest one
    if (ver == null) return dir.cur

    // ensure all pod versions are fully loaded
    loadAll(dir)

    // find exact version match
    return dir.all.find |pod| { pod.version == ver }
  }

//////////////////////////////////////////////////////////////////////////
// Query
//////////////////////////////////////////////////////////////////////////

  private Unsafe query(Str query, Int numVersions)
  {
    // sanity checks
    if (numVersions < 1) throw ArgErr("numVersions < 1")

    // parse the query
    q := Query.fromStr(query)

    // match
    acc := PodSpec[,]
    podDirs.each |podDir|
    {
      // check if we don't match on name we can short circuit
      if (!q.includeName(podDir.cur)) return

      // if numVersions is one and we match against cur,
      // we can avoid any lazy loading of all the pods
      if (numVersions == 1 && q.include(podDir.cur))
      {
        acc.add(podDir.cur)
        return
      }

      // ensure all pod versions are fully loaded
      loadAll(podDir)

      // check every pod until we hit our limit
      matches := 0
      for (i:=0; i<podDir.all.size; ++i)
      {
        spec := podDir.all[i]
        if (q.include(spec))
        {
          acc.add(spec)
          matches++
          if (matches >= numVersions) break
        }
      }
    }
    return Unsafe(acc)
  }

//////////////////////////////////////////////////////////////////////////
// Publish
//////////////////////////////////////////////////////////////////////////

  private PodSpec publish(File inputFile)
  {
    // verify its valid pod and parse its meta as PodSpec
    spec := PodSpec.load(inputFile)

    // get dest file in our db and verify it doesnt already exist
    dbFile := repo.specToFile(spec)
    if (dbFile.exists) throw Err("Pod already published: $spec")

    // copy it
    inputFile.copyTo(dbFile)

    // re-read spec using correct dbFile
    spec = PodSpec.load(dbFile)

    // check if we need to update our data structures
    podDir := podDirs[spec.name]
    if (podDir == null)
    {
      // add new pod
      podDirs[spec.name] = PodDir(dbFile.parent, spec)
    }
    else
    {
      // update existing pod cur/all
      if (spec.version > podDir.cur.version) podDir.cur = spec
      if (podDir.all != null) { podDir.all.add(spec); podDir.sortAll }
    }

    // return spec
    return spec
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  const FileRepo repo
  const File dir
  const Log log
  private Str:PodDir podDirs := [:]
}

**************************************************************************
** PodDir
**************************************************************************

internal class PodDir
{
  new make(File dir, PodSpec cur) { this.dir = dir; this.cur = cur }
  const File dir   // directory used to store pod files
  PodSpec cur      // current version
  PodSpec[]? all   // lazily loaded all versions

  // sort by highest to lowest version
  Void sortAll() { all.sortr |a, b| { a.version <=> b.version } }
}

**************************************************************************
** FileRepoMsg
**************************************************************************

internal const class FileRepoMsg
{
  const static Int load     := 0  //
  const static Int find     := 1  // a=name, b=version
  const static Int query    := 2  // a=query, b=numVersions
  const static Int versions := 3  // a=Str
  const static Int publish  := 4  // a=File
  const static Int refresh  := 5  //

  new make(Int id, Obj? a := null, Obj? b := null) { this.id = id; this.a = a; this.b = b}

  const Int id
  const Obj? a
  const Obj? b
}