001/*
002 *  Copyright (c) 2012, 2013, Credit Suisse (Anatole Tresch), Werner Keil.
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of 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,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.javamoney.moneta.loader.internal;
017
018import java.io.ByteArrayInputStream;
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.URL;
023import java.net.URLConnection;
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.LoaderService.UpdatePolicy;
035
036/**
037 * This class represent a resource that automatically is reloaded, if needed.
038 * 
039 * @author Anatole Tresch
040 */
041public class LoadableResource {
042        /** The logger used. */
043        private static final Logger LOG = Logger.getLogger(LoadableResource.class
044                        .getName());
045        /** Lock for this instance. */
046        private final Object LOCK = new Object();
047        /** resource id. */
048        private String resourceId;
049        /** The {@link UpdatePolicy}. */
050        private UpdatePolicy updatePolicy;
051        /** The remote URLs to be looked up (first wins). */
052        private List<URL> remoteResources = new ArrayList<>();
053        /** The fallback location (classpath). */
054        private URL fallbackLocation;
055        /** The cached resource URL. */
056        private URL cachedResource;
057        /** How many times this resource was successfully loaded. */
058        private AtomicInteger loadCount = new AtomicInteger();
059        /** How many times this resource was accessed. */
060        private AtomicInteger accessCount = new AtomicInteger();
061        /** The current data array. */
062        private volatile byte[] data;
063        /** THe timestamp of the last successful load. */
064        private long lastLoaded;
065        /** The registration config. */
066        private Map<String, String> updateConfig;
067
068        /**
069         * Create a new instance.
070         * 
071         * @param resourceId
072         *            The dataId.
073         * @param updatePolicy
074         *            The {@link UpdatePolicy}, not null.
075         * @param fallbackLocation
076         *            teh fallback ULR, not null.
077         * @param locations
078         *            the remote locations, not null (but may be empty!)
079         */
080        public LoadableResource(String resourceId, UpdatePolicy updatePolicy,
081                        URL fallbackLocation, URL... locations) {
082                Objects.requireNonNull(resourceId, "resourceId required");
083                Objects.requireNonNull(fallbackLocation, "classpathDefault required");
084                Objects.requireNonNull(updatePolicy, "UpdatePolicy required");
085                this.resourceId = resourceId;
086                this.fallbackLocation = fallbackLocation;
087                this.remoteResources.addAll(Arrays.asList(locations));
088                this.updatePolicy = updatePolicy;
089        }
090
091        /**
092         * Loads the resource, first from the remote resources, if that fails from
093         * the fallback location.
094         * 
095         * @return true, if load succeeded.
096         */
097        public boolean load() {
098                if (!loadRemote()) {
099                        return loadFallback();
100                }
101                return true;
102        }
103
104        /**
105         * Get the resourceId.
106         * 
107         * @return the resourceId
108         */
109        public final String getResourceId() {
110                return resourceId;
111        }
112
113        /**
114         * Get the {@link UpdatePolicy}.
115         * 
116         * @return the updatePolicy
117         */
118        public UpdatePolicy getUpdatePolicy() {
119                return updatePolicy;
120        }
121
122        /**
123         * Get the remote locations.
124         * 
125         * @return the remote locations, maybe empty.
126         */
127        public final List<URL> getRemoteResources() {
128                return Collections.unmodifiableList(remoteResources);
129        }
130
131        /**
132         * Return the fallback location.
133         * 
134         * @return the fallback location
135         */
136        public final URL getFallbackResource() {
137                return fallbackLocation;
138        }
139
140        /**
141         * Get the URL of the locally cached resource.
142         * 
143         * @return the cachedResource
144         */
145        public final URL getCachedResource() {
146                return cachedResource;
147        }
148
149        /**
150         * Get the number of active loads of this resource (InputStream).
151         * 
152         * @return the number of successful loads.
153         */
154        public final int getLoadCount() {
155                return loadCount.get();
156        }
157
158        /**
159         * Get the number of successful accesses.
160         * 
161         * @return the number of successful accesses.
162         */
163        public final int getAccessCount() {
164                return accessCount.get();
165        }
166
167        /**
168         * Get the resource data. This will trigger a full load, if the resource is
169         * not loaded, e.g. for LAZY resources.
170         * 
171         * @return the data to load.
172         */
173        public final byte[] getData() {
174                accessCount.incrementAndGet();
175                if (this.data == null) {
176                        synchronized (LOCK) {
177                                if (this.data == null) {
178                                        if (!loadRemote()) {
179                                                loadFallback();
180                                        }
181                                }
182                                if (this.data == null) {
183                                        throw new IllegalStateException(
184                                                        "Failed to load remote as well as fallback resources for "
185                                                                        + this);
186                                }
187                        }
188                }
189                return data.clone();
190        }
191
192        /**
193         * Get the resource data as input stream.
194         * 
195         * @return the input stream.
196         */
197        public InputStream getDataStream() {
198                return new WrappedInputStream(new ByteArrayInputStream(getData()));
199        }
200
201        /**
202         * Get the timestamp of the last succesful load.
203         * 
204         * @return the lastLoaded
205         */
206        public final long getLastLoaded() {
207                return lastLoaded;
208        }
209
210        /**
211         * Try to load the resource from the remote locations.
212         * 
213         * @return true, on success.
214         */
215        public boolean loadRemote() {
216                for (URL itemToLoad : remoteResources) {
217                        try {
218                                load(itemToLoad, false);
219                                return true;
220                        } catch (Exception e) {
221                                LOG.log(Level.INFO, "Failed to load resource: " + itemToLoad, e);
222                        }
223                }
224                return false;
225        }
226
227        /**
228         * Try to load the resource from the faööback resources. This will override
229         * any remote data already loaded.
230         * 
231         * @return true, on success.
232         */
233        public boolean loadFallback() {
234                try {
235                        load(fallbackLocation, true);
236                        return true;
237                } catch (Exception e) {
238                        LOG.log(Level.SEVERE, "Failed to load fallback resource: "
239                                        + fallbackLocation, e);
240                }
241                return false;
242        }
243
244        /**
245         * Load the data.
246         * 
247         * @param itemToLoad
248         *            the target {@link URL}
249         * @param fallbackLoad
250         *            true, for a fallback URL.
251         * @throws IOException
252         *             if load fails.
253         */
254        private void load(URL itemToLoad, boolean fallbackLoad) throws IOException {
255                InputStream is = null;
256                ByteArrayOutputStream bos = new ByteArrayOutputStream();
257                try {
258                        URLConnection conn = itemToLoad.openConnection();
259                        byte[] data = new byte[4096];
260                        is = conn.getInputStream();
261                        int read = is.read(data);
262                        while (read > 0) {
263                                bos.write(data, 0, read);
264                                read = is.read(data);
265                        }
266                        this.data = bos.toByteArray();
267                } finally {
268                        if (is != null) {
269                                try {
270                                        is.close();
271                                } catch (Exception e) {
272                                        LOG.log(Level.SEVERE, "Error closing resource input for "
273                                                        + resourceId, e);
274                                }
275                        }
276                        if (bos != null) {
277                                bos.close();
278                        }
279                }
280                if (!fallbackLoad) {
281                        lastLoaded = System.currentTimeMillis();
282                        loadCount.incrementAndGet();
283                }
284        }
285
286        /**
287         * Unloads the data.
288         */
289        public void unload() {
290                synchronized (LOCK) {
291                        int count = accessCount.decrementAndGet();
292                        if (count == 0) {
293                                this.data = null;
294                        }
295                }
296        }
297
298        /**
299         * InputStream , that helps managing the load count.
300         * 
301         * @author Anatole
302         * 
303         */
304        private final class WrappedInputStream extends InputStream {
305
306                private InputStream wrapped;
307
308                public WrappedInputStream(InputStream wrapped) {
309                        this.wrapped = wrapped;
310                }
311
312                @Override
313                public int read() throws IOException {
314                        return wrapped.read();
315                }
316
317                @Override
318                public void close() throws IOException {
319                        try {
320                                wrapped.close();
321                                super.close();
322                        } finally {
323                                unload();
324                        }
325                }
326
327        }
328
329        /**
330         * Explcitly override the resource wih the fallback context and resets the
331         * load counter.
332         * 
333         * @return true on success.
334         * @throws IOException
335         */
336        public boolean reset() throws IOException {
337                if (loadFallback()) {
338                        loadCount.set(0);
339                        return true;
340                }
341                return false;
342        }
343
344        /**
345         * Access the registration config.
346         * 
347         * @return the config, not null.
348         */
349        public Map<String, String> getUpdateConfig() {
350                return this.updateConfig;
351        }
352
353        @Override
354        public String toString() {
355                return "LoadableResource [resourceId=" + resourceId + ", updatePolicy="
356                                + updatePolicy + ", fallbackLocation=" + fallbackLocation
357                                + ", remoteResources=" + remoteResources + ", cachedResource="
358                                + cachedResource + ", loadCount=" + loadCount
359                                + ", accessCount=" + accessCount + ", lastLoaded=" + lastLoaded
360                                + ", updateConfig=" + updateConfig + "]";
361        }
362        
363        
364
365}