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 java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.lang.ref.SoftReference;
023import java.net.URI;
024import java.net.URL;
025import java.net.URLConnection;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collections;
029import java.util.List;
030import java.util.Map;
031import java.util.Objects;
032import java.util.concurrent.atomic.AtomicInteger;
033import java.util.logging.Level;
034import java.util.logging.Logger;
035
036import org.javamoney.moneta.spi.LoadDataInformation;
037import org.javamoney.moneta.spi.LoaderService;
038
039/**
040 * This class represent a resource that automatically is reloaded, if needed.
041 * To create this instance use: {@link LoadableResourceBuilder}
042 * @author Anatole Tresch
043 */
044public class LoadableResource {
045
046    /**
047     * The logger used.
048     */
049    private static final Logger LOG = Logger.getLogger(LoadableResource.class.getName());
050    /**
051     * Lock for this instance.
052     */
053    private final Object lock = new Object();
054    /**
055     * resource id.
056     */
057    private final String resourceId;
058    /**
059     * The remote URLs to be looked up (first wins).
060     */
061    private final List<URI> remoteResources = new ArrayList<>();
062    /**
063     * The fallback location (classpath).
064     */
065    private final URI fallbackLocation;
066    /**
067     * The cache used.
068     */
069    private final ResourceCache cache;
070    /**
071     * How many times this resource was successfully loaded.
072     */
073    private final AtomicInteger loadCount = new AtomicInteger();
074    /**
075     * How many times this resource was accessed.
076     */
077    private final AtomicInteger accessCount = new AtomicInteger();
078    /**
079     * The current data array.
080     */
081    private volatile SoftReference<byte[]> data;
082    /**
083     * THe timestamp of the last successful load.
084     */
085    private long lastLoaded;
086    /**
087     * The time to live (TTL) of cache entries in milliseconds, by default 24 h.
088     */
089    private long cacheTTLMillis = 3600000L * 24; // 24 h
090
091    /**
092     * The required update policy for this resource.
093     */
094    private final LoaderService.UpdatePolicy updatePolicy;
095    /**
096     * The resource configuration.
097     */
098    private final Map<String, String> properties;
099
100
101    LoadableResource(ResourceCache cache, LoadDataInformation loadDataInformation) {
102
103
104        Objects.requireNonNull(loadDataInformation.getResourceId(), "resourceId required");
105        Objects.requireNonNull(loadDataInformation.getProperties(), "properties required");
106        Objects.requireNonNull(loadDataInformation.getUpdatePolicy(), "updatePolicy required");
107        String val = loadDataInformation.getProperties().get("cacheTTLMillis");
108        if (val != null) {
109            this.cacheTTLMillis = Long.parseLong(val);
110        }
111        this.cache = cache;
112        this.resourceId = loadDataInformation.getResourceId();
113        this.updatePolicy = loadDataInformation.getUpdatePolicy();
114        this.properties = loadDataInformation.getProperties();
115        this.fallbackLocation = loadDataInformation.getBackupResource();
116        this.remoteResources.addAll(Arrays.asList(loadDataInformation.getResourceLocations()));
117    }
118
119    /**
120     * Get the UpdatePolicy of this resource.
121     *
122     * @return the UpdatePolicy of this resource, never null.
123     */
124    public LoaderService.UpdatePolicy getUpdatePolicy() {
125        return updatePolicy;
126    }
127
128    /**
129     * Get the configuration properties of this resource.
130     *
131     * @return the  configuration properties of this resource, never null.
132     */
133    public Map<String, String> getProperties() {
134        return properties;
135    }
136
137    /**
138     * Loads the resource, first from the remote resources, if that fails from
139     * the fallback location.
140     *
141     * @return true, if load succeeded.
142     */
143    public boolean load() {
144        if ((lastLoaded + cacheTTLMillis) <= System.currentTimeMillis()) {
145            clearCache();
146        }
147        if (!readCache()) {
148            if (shouldReadDataFromCallBack()) {
149                return loadFallback();
150            }
151        }
152        return true;
153    }
154
155        private boolean shouldReadDataFromCallBack() {
156                return LoaderService.UpdatePolicy.NEVER.equals(updatePolicy) || !loadRemote();
157        }
158
159    /**
160     * Get the resourceId.
161     *
162     * @return the resourceId
163     */
164    public final String getResourceId() {
165        return resourceId;
166    }
167
168    /**
169     * Get the remote locations.
170     *
171     * @return the remote locations, maybe empty.
172     */
173    public final List<URI> getRemoteResources() {
174        return Collections.unmodifiableList(remoteResources);
175    }
176
177    /**
178     * Return the fallback location.
179     *
180     * @return the fallback location, or null.
181     */
182    public final URI getFallbackResource() {
183        return fallbackLocation;
184    }
185
186    /**
187     * Get the number of active loads of this resource (InputStream).
188     *
189     * @return the number of successful loads.
190     */
191    public final int getLoadCount() {
192        return loadCount.get();
193    }
194
195    /**
196     * Get the number of successful accesses.
197     *
198     * @return the number of successful accesses.
199     */
200    public final int getAccessCount() {
201        return accessCount.get();
202    }
203
204    /**
205     * Get the resource data as input stream.
206     *
207     * @return the input stream.
208     */
209    public InputStream getDataStream() {
210        return new WrappedInputStream(new ByteArrayInputStream(getData()));
211    }
212
213    /**
214     * Get the timestamp of the last succesful load.
215     *
216     * @return the lastLoaded
217     */
218    public final long getLastLoaded() {
219        return lastLoaded;
220    }
221
222    /**
223     * Try to load the resource from the remote locations.
224     *
225     * @return true, on success.
226     */
227    public boolean loadRemote() {
228        for (URI itemToLoad : remoteResources) {
229            try {
230                return !load(itemToLoad, false);
231            } catch (Exception e) {
232                LOG.log(Level.INFO, "Failed to load resource: " + itemToLoad, e);
233            }
234        }
235        return true;
236    }
237
238    /**
239     * Try to load the resource from the fallback resources. This will override
240     * any remote data already loaded, and also will clear the cached data.
241     *
242     * @return true, on success.
243     */
244    public boolean loadFallback() {
245        try {
246            if (fallbackLocation == null) {
247                Logger.getLogger(getClass().getName()).warning("No fallback resource for " + this +
248                        ", loadFallback not supported.");
249                return false;
250            }
251            load(fallbackLocation, true);
252            clearCache();
253            return true;
254        } catch (Exception e) {
255            LOG.log(Level.SEVERE, "Failed to load fallback resource: " + fallbackLocation, e);
256        }
257        return false;
258    }
259
260    /**
261     * This method is called when the cached data should be removed, e.g. after an explicit fallback reload, or
262     * a clear operation.
263     */
264    protected void clearCache() {
265        if (this.cache != null) {
266            this.cache.clear(resourceId);
267        }
268    }
269
270    /**
271     * This method is called when the data should be loaded from the cache. This method abstracts the effective
272     * caching mechanism implemented. By default it tries to read a file from the current user's home directory.
273     * If the data could be read, #setData(byte[]) should be called to apply the data read.
274     *
275     * @return true, if data could be read and applied from the cache sucdcessfully.
276     */
277    protected boolean readCache() {
278        if (this.cache != null) {
279            if (this.cache.isCached(resourceId)) {
280                byte[] data = this.cache.read(resourceId);
281                if (data != null) {
282                    setData(data);
283                    return true;
284                }
285            }
286        }
287        return false;
288    }
289
290    /**
291     * This method is called after data could be successfully loaded from a non fallback resource. This method by
292     * default writes an file containing the data into the user's local home directory, so subsequent or later calls,
293     * even after a VM restart, should be able to recover this information.
294     */
295    protected void writeCache() throws IOException {
296        if (this.cache != null) {
297            byte[] data = this.data == null ? null : this.data.get();
298            if (data == null) {
299                return;
300            }
301            this.cache.write(resourceId, data);
302        }
303    }
304
305    /**
306     * Tries to load the data from the given location. The location hereby can be a remote location or a local
307     * location. Also it can be an URL pointing to a current dataset, or an url directing to fallback resources,
308     * e.g. within the cuzrrent classpath.
309     *
310     * @param itemToLoad   the target {@link URL}
311     * @param fallbackLoad true, for a fallback URL.
312     */
313    protected boolean load(URI itemToLoad, boolean fallbackLoad) {
314        InputStream is = null;
315        ByteArrayOutputStream stream = new ByteArrayOutputStream();
316        try {
317            URLConnection conn = itemToLoad.toURL().openConnection();
318            byte[] data = new byte[4096];
319            is = conn.getInputStream();
320            int read = is.read(data);
321            while (read > 0) {
322                stream.write(data, 0, read);
323                read = is.read(data);
324            }
325            setData(stream.toByteArray());
326            if (!fallbackLoad) {
327                writeCache();
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                stream.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 (shouldReadDataFromCallBack()) {
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}