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

net.yapbam.remote.AbstractRemoteResource Maven / Gradle / Ivy

There is a newer version: 1.9.1
Show newest version
package net.yapbam.remote;

import java.net.*;
import java.io.*;

import net.yapbam.remote.Cache;
import net.yapbam.util.StreamUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.*;
import java.util.*;

/**
 * An abstract resource synchronized with an Internet data source.
 * 

A local cache file is used for storing exchange rates to reduce network latency and allow offline mode. *
Instance observers are notified of every data change. *
This class is compatible with Java Desktop and Android. * @param the data type managed by this class. * @version 1.01 2013-12-30 * @author Jean-Marc Astesana (based on an original Currency converter code from Thomas Knierim) */ public abstract class AbstractRemoteResource extends Observable { private Logger logger; private Proxy proxy; private Cache cache; private T data; private long lastTryCacheRefresh; private boolean isSynchronized; /** * Constructor. *
The instance is initialized using the cached data, no remote access is done. * This guarantees a (relatively) short execution time and allows it to be executed on the UI thread. *
Once initialized this instance should be update with the {@link #update()} method * (on a background thread in a gui context). *
If access to cache fails or if cache is corrupted, the instance is created as if there is no cache. * @param proxy The proxy to use to get the data (Proxy.NoProxy to not use any proxy) * @param cache A cache instance, or null to use no cache * @see #update() */ protected AbstractRemoteResource(Proxy proxy, Cache cache) { this.proxy = proxy; this.data = null; this.cache = cache==null?new MemoryCache():cache; try { // Try to read the cache file this.data = parse(this.cache, false); this.isSynchronized = !isDataExpired(); } catch (Exception e) { // Cache parsing failed, maybe cache file is not present or is corrupted. // We will call update without try/catch clause to throw exceptions if data can't be read. getLogger().warn("Parse failed", e); } this.lastTryCacheRefresh = this.cache.getTimeStamp(); } /** Gets a logger. *
This logger is used by the class to log events. * @return a Logger */ protected Logger getLogger() { if (logger==null) { logger = LoggerFactory.getLogger(getClass()); } return logger; } /** * Gets the time stamp of the data as ms since January 1, 1970, 00:00:00 GMT. *
This is used by {@link #isDataExpired()} to determine if server should be asked for new data * @return a positive long or a negative number if the data structure has not yet been initialized. * @see #getRefreshTimeStamp() */ public long getTimeStamp() { return data==null ? -1 : this.data.getTimeStamp(); } /** Gets the last successful refresh date as ms since January 1, 1970, 00:00:00 GMT. *
Note this can be different of {@link #getTimeStamp()}. For instance, last time we contact the server * (let say at 10 o'clock) it returned data that was updated some time ago (let say at 9 o'clock). * In such a case, this method will return 10 o'clock and {@link #getTimeStamp()} 9 o'clock. * @return a positive long */ public long getRefreshTimeStamp() { return cache.getTimeStamp(); } /** Tests whether this converter is synchronized with web server. * @return true if the rates are up to date * @see #update() */ public boolean isSynchronized() { return this.isSynchronized; } /** * Makes the cache up to date. *
If it is not, downloads again cache file and parse data into internal data structure. *
After this method is called, {@link #isSynchronized()} always return true (even if server has not been called). * @return true if the web server was called. * @throws IOException if an error occurs while querying the server * @throws ParseException If an error occurs while parsing the XML cache file. * @see #forcedUpdate() * @see #isDataExpired() */ public boolean update() throws IOException, ParseException { boolean connect = isDataExpired(); if (connect) { forcedUpdate(); } this.isSynchronized = true; return connect; } /** * Forces the cache to be refreshed. *
Always downloads again cache file and parse data into internal data structure. * @throws IOException If cache file cannot be read/written or if URL cannot be opened. * @throws ParseException If an error occurs while parsing the XML cache file. * @see #update() */ public void forcedUpdate() throws IOException, ParseException { long start = System.currentTimeMillis(); refreshCacheFile(); getLogger().debug("refresh cache: {}ms",Long.toString(System.currentTimeMillis()-start)); start = System.currentTimeMillis(); T parsedData = parse(cache, true); getLogger().debug("parse: {}ms",Long.toString(System.currentTimeMillis()-start)); start = System.currentTimeMillis(); cache.commit(); getLogger().debug("commit: {}ms",Long.toString(System.currentTimeMillis()-start)); // If everything goes right, update the data and inform observers this.data = parsedData; setChanged(); notifyObservers(); } /** * Checks whether data needs to be updated. *
This method is called by {@link #update()} before calling the server. If this method returns false, * the server is not called. Please note that {@link #forcedUpdate()} does not use this method and always calls the server. *
The default implementation suppose that data is published once a day except during week-end. *
You can override this method in order to change this behavior. *
Remember that:
    *
  • {@link #getTimeStamp()} can return a negative number. In such a case, that method should return true.
  • *
  • It is a good practice to use {@link #getLastRefreshTimeStamp()} in order to limit calls to the remote server. *
    For example, you can return false if the last server call was less than a minute.
  • *
* @return true if data needs to be updated, false otherwise. */ protected boolean isDataExpired() { if (getTimeStamp() < 0) { return true; } // If we connect to server since less than one minute ... do nothing // This could happen if server doesn't refresh its rates since the last time we // updated the cache file (and more than the "standard" cache expiration time defined below) if (System.currentTimeMillis() - lastTryCacheRefresh < 60000) { return false; } final int tolerance = 12; Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); //$NON-NLS-1$ long hoursOld = (cal.getTimeInMillis() - getTimeStamp()) / (1000 * 60 * 60); cal.setTimeInMillis(getTimeStamp()); // hypothetical: data is never published on Saturday and Sunday int hoursValid = 24 + tolerance; if (cal.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY) { hoursValid = 72; } else if (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY) { hoursValid = 48; } return hoursOld > hoursValid; } /** * Gets the last data refresh attempt time stamp as ms since January 1, 1970, 00:00:00 GMT. *
This is used by {@link #isDataExpired()} to determine if server should be asked for new data. * @return a positive long. * @see #isDataExpired() * @see #getRefreshTimeStamp() */ protected final long getLastRefreshTimeStamp() { return lastTryCacheRefresh; } protected abstract URL getSourceURL(); /** * (Re-)Downloads the data to the temporary cache. * @throws IOException If URL cannot be opened, or if a read/write error occurs. */ private void refreshCacheFile() throws IOException { lastTryCacheRefresh = System.currentTimeMillis(); getLogger().trace("Connecting to {}", getSourceURL()); InputStream in = getSourceStream(); try { synchronized (cache) { OutputStream out = cache.getOutputStream(); try { StreamUtils.copy(in, out, new byte[10240]); } finally { out.flush(); out.close(); } } } finally { in.close(); } } private InputStream getSourceStream() throws IOException { URL url = getSourceURL(); if (url==null) { throw new FileNotFoundException(); } URLConnection connection = url.openConnection(proxy); if (connection instanceof HttpURLConnection) { HttpURLConnection ct = (HttpURLConnection) connection; int errorCode = ct.getResponseCode(); if (errorCode != HttpURLConnection.HTTP_OK) { throw new IOException(MessageFormat.format("Http Error {1} when opening {0}", url, errorCode)); //$NON-NLS-1$ } } return connection.getInputStream(); } /** * Parses cache file and create internal data structures containing exchange rates. *
Be aware that cache may be empty. In such a case, parse should return an empty T instance * @param cache The cache the parser will read. * @param tmp true to parse the tmp cache, false to parse the official cache * @return the parsed RemoteData * @throws ParseException If XML file cannot be parsed. * @throws IOException if connection to the URL or writing to cache file fails. * @see Cache */ protected abstract T parse(Cache cache, boolean tmp) throws ParseException, IOException; protected T getData() { return this.data; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy