ANSSRegionsFactory.java

package gov.usgs.earthquake.geoserve;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.json.Json;
import javax.json.JsonObject;

import gov.usgs.earthquake.qdm.Regions;
import gov.usgs.util.FileUtils;
import gov.usgs.util.StreamUtils;
import gov.usgs.util.XmlUtils;

/**
 * Class to manage ANSS Authoritative Region updates.
 *
 * Simplest usage: ANSSRegionsFactory.getFactory().getRegions()
 *
 * Regions are not fetched until {@link #startup()} (or {@link #fetchRegions()})
 * is called.
 */
public class ANSSRegionsFactory {

  /** logging object */
  public static final Logger LOGGER = Logger.getLogger(ANSSRegionsFactory.class.getName());

  /** milliseconds per day */
  public static final long MILLISECONDS_PER_DAY = 86400000L;

  /** path to write regions.json */
  public static final String DEFAULT_REGIONS_JSON = "regions.json";

  /** global factory object */
  private static ANSSRegionsFactory SINGLETON;

  /** service used to load regions */
  private GeoserveLayersService geoserveLayersService;

  /** path to local regions file */
  private File localRegions = new File(DEFAULT_REGIONS_JSON);

  /** the current regions object */
  private Regions regions;

  /** shutdown hook registered by startup */
  private Thread shutdownHook;

  /** timer used to auto fetch region updates */
  private Timer updateTimer = new Timer();

  /**
   * Use default GeoserveLayersService.
   */
  public ANSSRegionsFactory() {
    this(new GeoserveLayersService());
  }

  /**
   * Use custom GeoserveLayersService.
   *
   * @param geoserveLayersService to use
   */
  public ANSSRegionsFactory(final GeoserveLayersService geoserveLayersService) {
    this.geoserveLayersService = geoserveLayersService;
  }

  /**
   * Get the global ANSSRegionsFactory, creating and starting if needed.
   *
   * @return ANSSRegionsFactory
   */
  public static synchronized ANSSRegionsFactory getFactory() {
    return getFactory(true);
  }

  /**
   * @param startup if Factory should be created and started, if needed
   * @return ANSSRegionsFactory
   */
  public static synchronized ANSSRegionsFactory getFactory(final boolean startup) {
    if (SINGLETON == null) {
      SINGLETON = new ANSSRegionsFactory();
      if (startup) {
        SINGLETON.startup();
      }
    }
    return SINGLETON;
  }

  /**
   * Set the global ANSSRegionsFactory, shutting down any existing factory if
   * needed.
   *
   * @param factory to set
   */
  public static synchronized void setFactory(final ANSSRegionsFactory factory) {
    if (SINGLETON != null) {
      SINGLETON.shutdown();
    }
    SINGLETON = factory;
  }

  /**
   * Download regions from geoserve.
   *
   * Writes out to "regions.json" in current working directory and, if unable to
   * update, reads in local copy.
   */
  public void fetchRegions() {
    try {
      // try loading from geoserve
      this.regions = loadFromGeoserve();
    } catch (Exception e) {
      LOGGER.log(Level.WARNING, "Error fetching ANSS Regions from geoserve", e);
      try {
        if (this.regions == null) {
          // fall back to local cache
          this.regions = loadFromFile();
        }
      } catch (Exception e2) {
        LOGGER.log(Level.WARNING, "Error fetching ANSS Regions from local file", e);
      }
    }
  }

  /**
   * Read regions from local regions file.
   *
   * @return Regions
   * @throws IOException if error occurs
   */
  protected Regions loadFromFile() throws IOException {
    try (InputStream in = StreamUtils.getInputStream(this.localRegions)) {
      JsonObject json = Json.createReader(in).readObject();
      Regions regions = new RegionsJSON().parseRegions(json);
      // regions loaded
      LOGGER.fine("Loaded ANSS Authoritative Regions from " + this.localRegions + ", last modified="
          + XmlUtils.formatDate(new Date(this.localRegions.lastModified())));
      return regions;
    }
  }

  /**
   * Read regions from geoserve service.
   *
   * @return Regions
   * @throws IOException if error occurs
   */
  protected Regions loadFromGeoserve() throws IOException {
    LOGGER.fine("Fetching ANSS Authoritative Regions from Geoserve");
    JsonObject json = this.geoserveLayersService.getLayer("anss");
    Regions regions = new RegionsJSON().parseRegions(json);
    LOGGER.finer("Loaded ANSS Authoritative Regions from Geoserve");
    try {
      saveToFile(this.localRegions, json);
    } catch (IOException e) {
      // log for now, since saving is value added
      LOGGER.log(Level.INFO, "Error saving local regions", e);
    }
    return regions;
  }

  /**
   * Store json to local regions file.
   *
   * @param regionsFile to store to
   * @param json        json response to store locally.
   * @throws IOException if IO error occurs
   */
  protected void saveToFile(final File regionsFile, final JsonObject json) throws IOException {
    LOGGER.fine("Storing ANSS Authoritative Regions to " + regionsFile);
    // save regions if needed later
    FileUtils.writeFileThenMove(new File(regionsFile.toString() + ".temp"), regionsFile, json.toString().getBytes());
    LOGGER.finer("Stored ANSS Regions to " + regionsFile);
  }

  /**
   * Start updating regions.
   */
  public void startup() {
    if (this.shutdownHook != null) {
      // already started
      return;
    }

    // do initial fetch
    fetchRegions();

    // schedule periodic fetch
    long now = new Date().getTime();
    long nextMidnight = MILLISECONDS_PER_DAY - (now % MILLISECONDS_PER_DAY);
    updateTimer.scheduleAtFixedRate(new TimerTask() {
      @Override
      public void run() {
        fetchRegions();
      }
    },
        // firstt time at midnight
        nextMidnight,
        // once per day
        MILLISECONDS_PER_DAY);

    // register shutdown hook
    this.shutdownHook = new Thread(() -> {
      // stop periodic fetch
      this.updateTimer.cancel();
    });
    Runtime.getRuntime().addShutdownHook(this.shutdownHook);
  }

  /**
   * Stop updating regions.
   */
  public void shutdown() {
    if (this.shutdownHook == null) {
      // not started or already stopped
      return;
    }

    // stop periodic fetch
    this.updateTimer.cancel();

    // remove shutdown hook
    Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
    this.shutdownHook = null;
  }

  /**
   * Get the service.
   *
   * @return geoserveLayersService
   */
  public GeoserveLayersService getGeoserveLayersService() {
    return this.geoserveLayersService;
  }

  /**
   * Set the service.
   *
   * @param service GeoserveLayersService to set
   */
  public void setGeoserveLayersService(final GeoserveLayersService service) {
    this.geoserveLayersService = service;
  }

  /**
   * Get the local regions file.
   *
   * @return localRegions
   */
  public File getLocalRegions() {
    return this.localRegions;
  }

  /**
   * Set the local regions file.
   *
   * @param localRegions file to set
   */
  public void setLocalRegions(final File localRegions) {
    this.localRegions = localRegions;
  }

  /**
   * Get the most recently fetched Regions.
   *
   * @return regions
   */
  public Regions getRegions() {
    return this.regions;
  }

}