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