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;
    }

}