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.internal.loader;
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 final String resourceId;
052    /**
053     * The remote URLs to be looked up (first wins).
054     */
055    private final List<URI> remoteResources = new ArrayList<>();
056    /**
057     * The fallback location (classpath).
058     */
059    private final URI fallbackLocation;
060    /**
061     * The cache used.
062     */
063    private final ResourceCache cache;
064    /**
065     * How many times this resource was successfully loaded.
066     */
067    private final AtomicInteger loadCount = new AtomicInteger();
068    /**
069     * How many times this resource was accessed.
070     */
071    private final 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 final LoaderService.UpdatePolicy updatePolicy;
089    /**
090     * The resource configuration.
091     */
092    private final 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 true;
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() throws IOException {
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            try {
343                bos.close();
344            } catch (IOException e) {
345                LOG.log(Level.INFO, "Error closing resource input for " + resourceId, e);
346            }
347        }
348        return false;
349    }
350
351    /**
352     * Get the resource data. This will trigger a full load, if the resource is
353     * not loaded, e.g. for LAZY resources.
354     *
355     * @return the data to load.
356     */
357    public final byte[] getData() {
358        return getData(true);
359    }
360
361    protected byte[] getData(boolean loadIfNeeded) {
362        byte[] result = this.data == null ? null : this.data.get();
363        if (result == null && loadIfNeeded) {
364            accessCount.incrementAndGet();
365            byte[] currentData = this.data == null ? null : this.data.get();
366            if (Objects.isNull(currentData)) {
367                synchronized (lock) {
368                    currentData = this.data == null ? null : this.data.get();
369                    if (Objects.isNull(currentData)) {
370                        if (loadRemote()) {
371                            loadFallback();
372                        }
373                    }
374                }
375            }
376            currentData = this.data == null ? null : this.data.get();
377            if (Objects.isNull(currentData)) {
378                throw new IllegalStateException("Failed to load remote as well as fallback resources for " + this);
379            }
380            return currentData.clone();
381        }
382        return result;
383    }
384
385    protected final void setData(byte[] bytes) {
386        this.data = new SoftReference<>(bytes);
387    }
388
389
390    public void unload() {
391        synchronized (lock) {
392            int count = accessCount.decrementAndGet();
393            if (count == 0) {
394                this.data = null;
395            }
396        }
397    }
398
399    /**
400     * Explicitly override the resource wih the fallback context and resets the
401     * load counter.
402     *
403     * @return true on success.
404     * @throws IOException
405     */
406    public boolean resetToFallback() {
407        if (loadFallback()) {
408            loadCount.set(0);
409            return true;
410        }
411        return false;
412    }
413
414    @Override
415    public String toString() {
416        return "LoadableResource [resourceId=" + resourceId + ", fallbackLocation=" +
417                fallbackLocation + ", remoteResources=" + remoteResources +
418                ", loadCount=" + loadCount + ", accessCount=" + accessCount + ", lastLoaded=" + lastLoaded + ']';
419    }
420
421    /**
422     * InputStream , that helps managing the load count.
423     *
424     * @author Anatole
425     */
426    private final class WrappedInputStream extends InputStream {
427
428        private final InputStream wrapped;
429
430        WrappedInputStream(InputStream wrapped) {
431            this.wrapped = wrapped;
432        }
433
434        @Override
435        public int read() throws IOException {
436            return wrapped.read();
437        }
438
439        @Override
440        public void close() throws IOException {
441            try {
442                wrapped.close();
443                super.close();
444            } finally {
445                unload();
446            }
447        }
448
449    }
450
451}