001/**
002 * Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil and others by the @author tag.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005 * use this file except in compliance with the License. You may obtain a copy of
006 * the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013 * License for the specific language governing permissions and limitations under
014 * the License.
015 */
016package org.javamoney.moneta.loader.internal;
017
018import org.javamoney.moneta.spi.LoaderService;
019
020import java.io.ByteArrayInputStream;
021import java.io.ByteArrayOutputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.lang.ref.SoftReference;
025import java.net.URI;
026import java.net.URL;
027import java.net.URLConnection;
028import java.util.*;
029import java.util.concurrent.atomic.AtomicInteger;
030import java.util.logging.Level;
031import java.util.logging.Logger;
032
033/**
034 * This class represent a resource that automatically is reloaded, if needed.
035 *
036 * @author Anatole Tresch
037 */
038public class LoadableResource {
039
040    /**
041     * The logger used.
042     */
043    private static final Logger LOG = Logger.getLogger(LoadableResource.class.getName());
044    /**
045     * Lock for this instance.
046     */
047    private final Object LOCK = new Object();
048    /**
049     * resource id.
050     */
051    private String resourceId;
052    /**
053     * The remote URLs to be looked up (first wins).
054     */
055    private List<URI> remoteResources = new ArrayList<>();
056    /**
057     * The fallback location (classpath).
058     */
059    private URI fallbackLocation;
060    /**
061     * The cache used.
062     */
063    private ResourceCache cache;
064    /**
065     * How many times this resource was successfully loaded.
066     */
067    private AtomicInteger loadCount = new AtomicInteger();
068    /**
069     * How many times this resource was accessed.
070     */
071    private AtomicInteger accessCount = new AtomicInteger();
072    /**
073     * The current data array.
074     */
075    private volatile SoftReference<byte[]> data;
076    /**
077     * THe timestamp of the last successful load.
078     */
079    private long lastLoaded;
080    /**
081     * The time to live (TTL) of cache entries in milliseconds, by default 24 h.
082     */
083    private long cacheTTLMillis = 3600000L * 24; // 24 h
084
085    /**
086     * The required update policy for this resource.
087     */
088    private LoaderService.UpdatePolicy updatePolicy;
089    /**
090     * The resource configuration.
091     */
092    private Map<String, String> properties;
093
094
095    /**
096     * Create a new instance.
097     *
098     * @param resourceId       The dataId.
099     * @param cache            The cache to be used for storing remote data locally.
100     * @param properties       The configuration properties.
101     * @param fallbackLocation teh fallback ULR, not null.
102     * @param locations        the remote locations, not null (but may be empty!)
103     */
104    public LoadableResource(String resourceId, ResourceCache cache, LoaderService.UpdatePolicy updatePolicy,
105                            Map<String, String> properties, URI fallbackLocation, URI... locations) {
106        Objects.requireNonNull(resourceId, "resourceId required");
107        Objects.requireNonNull(properties, "properties required");
108        Objects.requireNonNull(updatePolicy, "updatePolicy required");
109        String val = properties.get("cacheTTLMillis");
110        if (val != null) {
111            this.cacheTTLMillis = Long.parseLong(val);
112        }
113        this.cache = cache;
114        this.resourceId = resourceId;
115        this.updatePolicy = updatePolicy;
116        this.properties = properties;
117        this.fallbackLocation = fallbackLocation;
118        this.remoteResources.addAll(Arrays.asList(locations));
119    }
120
121    /**
122     * Get the UpdatePolicy of this resource.
123     *
124     * @return the UpdatePolicy of this resource, never null.
125     */
126    public LoaderService.UpdatePolicy getUpdatePolicy() {
127        return updatePolicy;
128    }
129
130    /**
131     * Get the configuration properties of this resource.
132     *
133     * @return the  configuration properties of this resource, never null.
134     */
135    public Map<String, String> getProperties() {
136        return properties;
137    }
138
139    /**
140     * Loads the resource, first from the remote resources, if that fails from
141     * the fallback location.
142     *
143     * @return true, if load succeeded.
144     */
145    public boolean load() {
146        if ((lastLoaded + cacheTTLMillis) <= System.currentTimeMillis()) {
147            clearCache();
148        }
149        if (!readCache()) {
150            if (!loadRemote()) {
151                return loadFallback();
152            }
153        }
154        return true;
155    }
156
157    /**
158     * Get the resourceId.
159     *
160     * @return the resourceId
161     */
162    public final String getResourceId() {
163        return resourceId;
164    }
165
166    /**
167     * Get the remote locations.
168     *
169     * @return the remote locations, maybe empty.
170     */
171    public final List<URI> getRemoteResources() {
172        return Collections.unmodifiableList(remoteResources);
173    }
174
175    /**
176     * Return the fallback location.
177     *
178     * @return the fallback location, or null.
179     */
180    public final URI getFallbackResource() {
181        return fallbackLocation;
182    }
183
184    /**
185     * Get the number of active loads of this resource (InputStream).
186     *
187     * @return the number of successful loads.
188     */
189    public final int getLoadCount() {
190        return loadCount.get();
191    }
192
193    /**
194     * Get the number of successful accesses.
195     *
196     * @return the number of successful accesses.
197     */
198    public final int getAccessCount() {
199        return accessCount.get();
200    }
201
202    /**
203     * Get the resource data as input stream.
204     *
205     * @return the input stream.
206     */
207    public InputStream getDataStream() {
208        return new WrappedInputStream(new ByteArrayInputStream(getData()));
209    }
210
211    /**
212     * Get the timestamp of the last succesful load.
213     *
214     * @return the lastLoaded
215     */
216    public final long getLastLoaded() {
217        return lastLoaded;
218    }
219
220    /**
221     * Try to load the resource from the remote locations.
222     *
223     * @return true, on success.
224     */
225    public boolean loadRemote() {
226        for (URI itemToLoad : remoteResources) {
227            try {
228                return load(itemToLoad, false);
229            } catch (Exception e) {
230                LOG.log(Level.INFO, "Failed to load resource: " + itemToLoad, e);
231            }
232        }
233        return false;
234    }
235
236    /**
237     * Try to load the resource from the fallback resources. This will override
238     * any remote data already loaded, and also will clear the cached data.
239     *
240     * @return true, on success.
241     */
242    public boolean loadFallback() {
243        try {
244            if (fallbackLocation == null) {
245                Logger.getLogger(getClass().getName()).warning("No fallback resource for " + this +
246                        ", loadFallback not supported.");
247                return false;
248            }
249            load(fallbackLocation, true);
250            clearCache();
251            return true;
252        } catch (Exception e) {
253            LOG.log(Level.SEVERE, "Failed to load fallback resource: " + fallbackLocation, e);
254        }
255        return false;
256    }
257
258    /**
259     * This method is called when the cached data should be removed, e.g. after an explicit fallback reload, or
260     * a clear operation.
261     */
262    protected void clearCache() {
263        if (this.cache != null) {
264            this.cache.clear(resourceId);
265        }
266    }
267
268    /**
269     * This method is called when the data should be loaded from the cache. This method abstracts the effective
270     * caching mechanism implemented. By default it tries to read a file from the current user's home directory.
271     * If the data could be read, #setData(byte[]) should be called to apply the data read.
272     *
273     * @return true, if data could be read and applied from the cache sucdcessfully.
274     */
275    protected boolean readCache() {
276        if (this.cache != null) {
277            if (this.cache.isCached(resourceId)) {
278                byte[] data = this.cache.read(resourceId);
279                if (data != null) {
280                    setData(data);
281                    return true;
282                }
283            }
284        }
285        return false;
286    }
287
288    /**
289     * This method is called after data could be successfully loaded from a non fallback resource. This method by
290     * default writes an file containing the data into the user's local home directory, so subsequent or later calls,
291     * even after a VM restart, should be able to recover this information.
292     */
293    protected void writeCache() {
294        if (this.cache != null) {
295            byte[] data = this.data == null ? null : this.data.get();
296            if (data == null) {
297                return;
298            }
299            this.cache.write(resourceId, data);
300        }
301    }
302
303    /**
304     * Tries to load the data from the given location. The location hereby can be a remote location or a local
305     * location. Also it can be an URL pointing to a current dataset, or an url directing to fallback resources,
306     * e.g. within the cuzrrent classpath.
307     *
308     * @param itemToLoad   the target {@link URL}
309     * @param fallbackLoad true, for a fallback URL.
310     */
311    protected boolean load(URI itemToLoad, boolean fallbackLoad) {
312        InputStream is = null;
313        ByteArrayOutputStream bos = new ByteArrayOutputStream();
314        try {
315            URLConnection conn = itemToLoad.toURL().openConnection();
316            byte[] data = new byte[4096];
317            is = conn.getInputStream();
318            int read = is.read(data);
319            while (read > 0) {
320                bos.write(data, 0, read);
321                read = is.read(data);
322            }
323            setData(bos.toByteArray());
324            if (!fallbackLoad) {
325                writeCache();
326            }
327            if (!fallbackLoad) {
328                lastLoaded = System.currentTimeMillis();
329                loadCount.incrementAndGet();
330            }
331            return true;
332        } catch (Exception e) {
333            LOG.log(Level.INFO, "Failed to load resource input for " + resourceId + " from " + itemToLoad, e);
334        } finally {
335            if (Objects.nonNull(is)) {
336                try {
337                    is.close();
338                } catch (Exception e) {
339                    LOG.log(Level.INFO, "Error closing resource input for " + resourceId, e);
340                }
341            }
342            if (Objects.nonNull(bos)) {
343                try {
344                    bos.close();
345                } catch (IOException e) {
346                    LOG.log(Level.INFO, "Error closing resource input for " + resourceId, e);
347                }
348            }
349        }
350        return false;
351    }
352
353    /**
354     * Get the resource data. This will trigger a full load, if the resource is
355     * not loaded, e.g. for LAZY resources.
356     *
357     * @return the data to load.
358     */
359    public final byte[] getData() {
360        return getData(true);
361    }
362
363    protected byte[] getData(boolean loadIfNeeded) {
364        byte[] result = this.data == null ? null : this.data.get();
365        if (result == null && loadIfNeeded) {
366            accessCount.incrementAndGet();
367            byte[] currentData = this.data == null ? null : this.data.get();
368            if (Objects.isNull(currentData)) {
369                synchronized (LOCK) {
370                    currentData = this.data == null ? null : this.data.get();
371                    if (Objects.isNull(currentData)) {
372                        if (!loadRemote()) {
373                            loadFallback();
374                        }
375                    }
376                }
377            }
378            currentData = this.data == null ? null : this.data.get();
379            if (Objects.isNull(currentData)) {
380                throw new IllegalStateException("Failed to load remote as well as fallback resources for " + this);
381            }
382            return currentData.clone();
383        }
384        return result;
385    }
386
387    protected final void setData(byte[] bytes) {
388        this.data = new SoftReference<>(bytes);
389    }
390
391
392    public void unload() {
393        synchronized (LOCK) {
394            int count = accessCount.decrementAndGet();
395            if (count == 0) {
396                this.data = null;
397            }
398        }
399    }
400
401    /**
402     * Explicitly override the resource wih the fallback context and resets the
403     * load counter.
404     *
405     * @return true on success.
406     * @throws IOException
407     */
408    public boolean resetToFallback() throws IOException {
409        if (loadFallback()) {
410            loadCount.set(0);
411            return true;
412        }
413        return false;
414    }
415
416    @Override
417    public String toString() {
418        return "LoadableResource [resourceId=" + resourceId + ", fallbackLocation=" +
419                fallbackLocation + ", remoteResources=" + remoteResources +
420                ", loadCount=" + loadCount + ", accessCount=" + accessCount + ", lastLoaded=" + lastLoaded + ']';
421    }
422
423    /**
424     * InputStream , that helps managing the load count.
425     *
426     * @author Anatole
427     */
428    private final class WrappedInputStream extends InputStream {
429
430        private InputStream wrapped;
431
432        public WrappedInputStream(InputStream wrapped) {
433            this.wrapped = wrapped;
434        }
435
436        @Override
437        public int read() throws IOException {
438            return wrapped.read();
439        }
440
441        @Override
442        public void close() throws IOException {
443            try {
444                wrapped.close();
445                super.close();
446            } finally {
447                unload();
448            }
449        }
450
451    }
452
453}