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

fm.common.ReloadableResource.scala Maven / Gradle / Ivy

/*
 * Copyright 2014 Frugal Mechanic (http://frugalmechanic.com)
 *
 * 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 fm.common

import java.util.{Timer, TimerTask}
import scala.ref.WeakReference
import scala.util.Try

object ReloadableResource {
  private val timer: Timer = new Timer("ReloadableResource Check", true /* isDaemon */)

  private class ResourceCheckTimerTask[T](resource: ReloadableResource[T], logger: Logger) extends TimerTask {
    private[this] val ref: WeakReference[ReloadableResource[T]] = WeakReference(resource)
    
    /** This should be the lastModified date of the currently loaded resource */
    private[this] var lastModified: Long = lookupLastModified(resource).getOrElse{ -1 }

    def run(): Unit = {
      ref.get match {
        case None => cancel() // The reference has been cleared so cancel the task
        case Some(r) => lookupLastModified(r).foreach { doReload(r, _) }
      }
    }
    
    private def lookupLastModified(r: ReloadableResource[T]): Option[Long] = Try{ r.lookupLastModified() }.toOption
    
    private def doReload(r: ReloadableResource[T], currentLastModified: Long): Unit = {
      if (currentLastModified == lastModified) return
      
      logger.info("Detected Updated Resource, Reloading...")
      if (r.reload()) {
        // We only update the lastModified if the resource was successfully reloaded.
        // This still isn't very fullproof and should really be modified so that reload
        // returns an Option[Long] which is the lastModified of the reloaded resource.
        lastModified = currentLastModified
        logger.info("Reload Successful.")
      } else {
        logger.info("Reload Failed.")
      }
    }
  }
  
}

abstract class ReloadableResource[T] extends Logging {
  import ReloadableResource.ResourceCheckTimerTask
  
  /** Load the resource from it's primary source */
  protected def loadFromPrimary(): Option[T]
  
  /** Load the resource from it's backup source (if any) */
  protected def loadFromBackup(): Option[T]
  
  /** A backup backup resource that will be used if the files and backup cannot be loaded */
  protected def defaultResource: Option[T]
  
  /** The Last Modified time of the resource (can be set to System.currentTimeMillis to always reload) */
  protected def lookupLastModified(): Long

  @volatile private[this] var _current: T = null.asInstanceOf[T]
  
  private lazy val init: Unit = {
    _current = loadResource()
  }
  
  /** Get the current version of the resource */
  final def apply(): T = {
    init
    _current
  }
  
  /**
   * Attempt to reload the current resource.  If there is a problem the existing version will be left in place
   * 
   * Returns true if the resource was successfully updated
   * 
   * TODO: This should probably return an Option[Long] which is the last modified time of the reloaded resource
   */
  final def reload(): Boolean = tryLoadResource(tryBackup = false) match {
    case None => false
    case Some(resource) => 
      _current = resource
      true
  }
  
  /**
   * Clear the reference to the current version of the resource.
   * 
   * NOTE: Calling apply() after this will return null
   */
  final def clear(): Unit = {
    _current = null.asInstanceOf[T]
  }
  
  /** Directly load the resource and return the result.  Doesn't touch the current resource in this class. */
  final def loadResource(): T = (tryLoadResource(tryBackup = true) orElse defaultResource).getOrElse{ throw new Exception("Unable to load resource") }

  private[this] var timerTask: ResourceCheckTimerTask[T] = null
  
  final def isAutoUpdateCheckEnabled: Boolean = null != timerTask
  
  /**
   * Enable checking and automatic reload of the resource if the external file is updated
   */
  final def enableAutoUpdateCheck(delaySeconds: Int = 300, periodSeconds: Int = 300): Unit = {
    require(null == timerTask, "TimerTask already enabled!")
    timerTask = new ResourceCheckTimerTask(this, logger)
    ReloadableResource.timer.schedule(timerTask, delaySeconds.toLong * 1000L, periodSeconds.toLong * 1000L)
  }
  
  /** Disable the auto update checks */
  final def disableAutoUpdateCheck(): Unit = {
    Option(timerTask).foreach{ _.cancel() }
    timerTask = null
  }
  
  private def tryLoadResource(tryBackup: Boolean): Option[T] = {
    
    try {
      val (millis, result): (Long, Option[T]) = Util.time{ loadFromPrimary() }
      if (result.isDefined) {
        logger.info(s"Loaded resource from primary source ($millis ms)")
        return result
      }
    } catch {
      case ex: Exception => logger.error("Exception Loading Resource from primary source", ex)
    }
    
    if (!tryBackup) return None
    
    try {
      val (millis, result): (Long, Option[T]) = Util.time{ loadFromBackup() }
      if (result.isDefined) {
        logger.info(s"Loaded resource from backup source ($millis ms)")
        return result
      }
    } catch {
      case ex: Exception => logger.error("Exception Loading Resource from backup source", ex)
    }

    None
  }
  
  override def finalize(): Unit = {
    disableAutoUpdateCheck()
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy