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.IOException;
019import java.io.InputStream;
020import java.net.URL;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.GregorianCalendar;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.Timer;
028import java.util.TimerTask;
029import java.util.concurrent.Callable;
030import java.util.concurrent.ConcurrentHashMap;
031import java.util.concurrent.ExecutorService;
032import java.util.concurrent.Executors;
033import java.util.concurrent.Future;
034import java.util.logging.Level;
035import java.util.logging.Logger;
036
037import javax.money.spi.Bootstrap;
038
039import org.javamoney.moneta.spi.LoaderService;
040
041/**
042 * This class provides a mechanism to register resources, that may be updated
043 * regularly. The implementation, based on the {@link UpdatePolicy}
044 * loads/updates the resources from arbitrary locations and stores it to the
045 * interal file cache.
046 * 
047 * @author Anatole Tresch
048 */
049public class DefaultLoaderService implements LoaderService {
050        /** Logger used. */
051        private static final Logger LOG = Logger
052                        .getLogger(DefaultLoaderService.class.getName());
053        /** The data resources managed by this instance. */
054        private Map<String, LoadableResource> resources = new ConcurrentHashMap<>();
055        /** The registered {@link LoaderListener} instances. */
056        private Map<String, List<LoaderListener>> listenersMap = new ConcurrentHashMap<>();
057        /**
058         * The local resource cache, to allow keeping current data on the local
059         * system.
060         */
061        private ResourceCache resourceCache = loadResourceCache();
062        /** The thread pool used for loading of data, triggered by the timer. */
063        private ExecutorService executors = Executors.newCachedThreadPool();
064        /**
065         * The configurator reading the initial loads from the javamoney.properties.
066         */
067        private LoaderConfigurator configurator = new LoaderConfigurator(this);
068        /** The timer used for schedules. */
069        private volatile Timer timer = new Timer();
070
071        /**
072         * Constructor, initializing from config.
073         */
074        public DefaultLoaderService() {
075                // read config
076                configurator.load();
077        }
078
079        /**
080         * Loads the cache to be used.
081         * 
082         * @return the cache to be used, not null.
083         */
084        private ResourceCache loadResourceCache() {
085                try {
086                        this.resourceCache = Bootstrap
087                                        .getService(ResourceCache.class, null);
088                } catch (Exception e) {
089                        LOG.log(Level.SEVERE, "Error loading ResourceCache instance.", e);
090                }
091                if (this.resourceCache == null) {
092                        LOG.fine("No ResourceCache loaded, using default.");
093                        this.resourceCache = new DefaultResourceCache();
094                }
095                return this.resourceCache;
096        }
097
098        // /**
099        // * Get the resource cache.
100        // *
101        // * @return the {@link ResourceCache} instance used.
102        // */
103        // public ResourceCache getResourceCache() {
104        // return this.resourceCache;
105        // }
106
107        /**
108         * Removes a resource managed.
109         * 
110         * @param resourceId
111         *            the resource id.
112         */
113        public void unload(String resourceId) {
114                LoadableResource res = this.resources.get(resourceId);
115                if (res != null) {
116                        res.unload();
117                }
118        }
119
120        /*
121         * (non-Javadoc)
122         * 
123         * @see
124         * org.javamoney.moneta.spi.LoaderService#registerData(java.lang.String,
125         * org.javamoney.moneta.spi.LoaderService.UpdatePolicy, java.util.Map,
126         * java.net.URL, java.net.URL[])
127         */
128        @Override
129        public void registerData(String resourceId, UpdatePolicy updatePolicy,
130                        Map<String, String> properties, URL backupResource,
131                        URL... resourceLocations) {
132                if (resources.containsKey(resourceId)) {
133                        throw new IllegalArgumentException("Resource : " + resourceId
134                                        + " already registered.");
135                }
136                LoadableResource res = new LoadableResource(resourceId, updatePolicy,
137                                backupResource, resourceLocations);
138                this.resources.put(resourceId, res);
139                switch (updatePolicy) {
140                case NEVER:
141                        loadDataLocal(resourceId);
142                        break;
143                case ONSTARTUP:
144                        loadDataAsync(resourceId);
145                        break;
146                case SCHEDULED:
147                        addScheduledLoad(res);
148                        break;
149                case LAZY:
150                default:
151                        break;
152                }
153        }
154
155        /*
156         * (non-Javadoc)
157         * 
158         * @see
159         * org.javamoney.moneta.spi.LoaderService#getUpdateConfiguration(java.lang
160         * .String)
161         */
162        @Override
163        public Map<String, String> getUpdateConfiguration(String resourceId) {
164                LoadableResource res = this.resources.get(resourceId);
165                if (res != null) {
166                        return res.getUpdateConfig();
167                }
168                return null;
169        }
170
171        /*
172         * (non-Javadoc)
173         * 
174         * @see
175         * org.javamoney.moneta.spi.LoaderService#isResourceRegistered(java.lang.String)
176         */
177        @Override
178        public boolean isResourceRegistered(String dataId) {
179                return this.resources.containsKey(dataId);
180        }
181
182        /*
183         * (non-Javadoc)
184         * 
185         * @see org.javamoney.moneta.spi.LoaderService#getResourceIds()
186         */
187        @Override
188        public Set<String> getResourceIds() {
189                return this.resources.keySet();
190        }
191
192        /*
193         * (non-Javadoc)
194         * 
195         * @see org.javamoney.moneta.spi.LoaderService#getData(java.lang.String)
196         */
197        @Override
198        public InputStream getData(String resourceId) throws IOException {
199                LoadableResource res = this.resources.get(resourceId);
200                if (res != null) {
201                        res.getDataStream();
202                }
203                throw new IllegalArgumentException("No such resource: " + resourceId);
204        }
205
206        /*
207         * (non-Javadoc)
208         * 
209         * @see org.javamoney.moneta.spi.LoaderService#loadData(java.lang.String)
210         */
211        @Override
212        public boolean loadData(String resourceId) {
213                return loadDataSynch(resourceId);
214        }
215
216        /*
217         * (non-Javadoc)
218         * 
219         * @see
220         * org.javamoney.moneta.spi.LoaderService#loadDataAsync(java.lang.String)
221         */
222        @Override
223        public Future<Boolean> loadDataAsync(final String resourceId) {
224                return executors.submit(new Callable<Boolean>() {
225                        @Override
226                        public Boolean call() {
227                                return loadDataSynch(resourceId);
228                        }
229                });
230        }
231
232        /*
233         * (non-Javadoc)
234         * 
235         * @see
236         * org.javamoney.moneta.spi.LoaderService#loadDataLocal(java.lang.String)
237         */
238        @Override
239        public boolean loadDataLocal(String resourceId) {
240                LoadableResource res = this.resources.get(resourceId);
241                if (res != null) {
242                        try {
243                                if (res.loadFallback()) {
244                                        triggerListeners(resourceId, res.getDataStream());
245                                        return true;
246                                }
247                        } catch (Exception e) {
248                                LOG.log(Level.SEVERE, "Failed to load resource locally: "
249                                                + resourceId, e);
250                        }
251                } else {
252                        throw new IllegalArgumentException("No such resource: "
253                                        + resourceId);
254                }
255                return false;
256        }
257
258        /**
259         * Reload data for a resource synchronously.
260         * 
261         * @param resourceId
262         *            the resource id, not null.
263         * @return true, if loading succeeded.
264         */
265        private boolean loadDataSynch(String resourceId) {
266                LoadableResource res = this.resources.get(resourceId);
267                if (res != null) {
268                        try {
269                                if (res.load()) {
270                                        triggerListeners(resourceId, res.getDataStream());
271                                        return true;
272                                }
273                        } catch (Exception e) {
274                                LOG.log(Level.SEVERE, "Failed to load resource: " + resourceId,
275                                                e);
276                        }
277                } else {
278                        throw new IllegalArgumentException("No such resource: "
279                                        + resourceId);
280                }
281                return false;
282        }
283
284        /*
285         * (non-Javadoc)
286         * 
287         * @see org.javamoney.moneta.spi.LoaderService#resetData(java.lang.String)
288         */
289        @Override
290        public void resetData(String dataId) throws IOException {
291                LoadableResource res = this.resources.get(dataId);
292                if (res != null) {
293                        if (res.reset()) {
294                                triggerListeners(dataId, res.getDataStream());
295                        }
296                } else {
297                        throw new IllegalArgumentException("No such resource: " + dataId);
298                }
299        }
300
301        /**
302         * Trigger the listeners registered for the given dataId.
303         * 
304         * @param dataId
305         *            the data id, not null.
306         * @param is
307         *            the InputStream, containing the latest data.
308         */
309        private void triggerListeners(String dataId, InputStream is) {
310                List<LoaderListener> listeners = getListeners("");
311                synchronized (listeners) {
312                        for (LoaderListener ll : listeners) {
313                                try {
314                                        ll.newDataLoaded(dataId, is);
315                                } catch (Exception e) {
316                                        LOG.log(Level.SEVERE, "Error calling LoadListener: " + ll,
317                                                        e);
318                                }
319                        }
320                }
321                if (!(dataId == null || dataId.isEmpty())) {
322                        listeners = getListeners(dataId);
323                        synchronized (listeners) {
324                                for (LoaderListener ll : listeners) {
325                                        try {
326                                                ll.newDataLoaded(dataId, is);
327                                        } catch (Exception e) {
328                                                LOG.log(Level.SEVERE, "Error calling LoadListener: "
329                                                                + ll, e);
330                                        }
331                                }
332                        }
333                }
334        }
335
336        /*
337         * (non-Javadoc)
338         * 
339         * @see
340         * org.javamoney.moneta.spi.LoaderService#addLoaderListener(org.javamoney
341         * .moneta.spi.LoaderService.LoaderListener, java.lang.String[])
342         */
343        @Override
344        public void addLoaderListener(LoaderListener l, String... dataIds) {
345                if (dataIds.length == 0) {
346                        List<LoaderListener> listeners = getListeners("");
347                        synchronized (listeners) {
348                                listeners.add(l);
349                        }
350                } else {
351                        for (String dataId : dataIds) {
352                                List<LoaderListener> listeners = getListeners(dataId);
353                                synchronized (listeners) {
354                                        listeners.add(l);
355                                }
356                        }
357                }
358        }
359
360        /**
361         * Evaluate the {@link LoaderListener} instances, listening fo a dataId
362         * given.
363         * 
364         * @param dataId
365         *            The dataId, not null
366         * @return the according listeners
367         */
368        private List<LoaderListener> getListeners(String dataId) {
369                if (dataId == null) {
370                        dataId = "";
371                }
372                List<LoaderListener> listeners = this.listenersMap.get(dataId);
373                if (listeners == null) {
374                        synchronized (listenersMap) {
375                                listeners = this.listenersMap.get(dataId);
376                                if (listeners == null) {
377                                        listeners = Collections
378                                                        .synchronizedList(new ArrayList<LoaderListener>());
379                                        this.listenersMap.put(dataId, listeners);
380                                }
381                        }
382                }
383                return listeners;
384        }
385
386        /*
387         * (non-Javadoc)
388         * 
389         * @see
390         * org.javamoney.moneta.spi.LoaderService#removeLoaderListener(org.javamoney
391         * .moneta.spi.LoaderService.LoaderListener, java.lang.String[])
392         */
393        @Override
394        public void removeLoaderListener(LoaderListener l, String... dataIds) {
395                if (dataIds.length == 0) {
396                        List<LoaderListener> listeners = getListeners("");
397                        synchronized (listeners) {
398                                listeners.remove(l);
399                        }
400                } else {
401                        for (String dataId : dataIds) {
402                                List<LoaderListener> listeners = getListeners(dataId);
403                                synchronized (listeners) {
404                                        listeners.remove(l);
405                                }
406                        }
407                }
408        }
409
410        /*
411         * (non-Javadoc)
412         * 
413         * @see
414         * org.javamoney.moneta.spi.LoaderService#getUpdatePolicy(java.lang.String)
415         */
416        @Override
417        public UpdatePolicy getUpdatePolicy(String resourceId) {
418                LoadableResource res = this.resources.get(resourceId);
419                if (res != null) {
420                        return res.getUpdatePolicy();
421                }
422                throw new IllegalArgumentException("No such resource: " + resourceId);
423        }
424
425        /**
426         * Create the schedule for the given {@link LoadableResource}.
427         * 
428         * @param loadableResource
429         */
430        private void addScheduledLoad(final LoadableResource loadableResource) {
431                TimerTask task = new TimerTask() {
432                        @Override
433                        public void run() {
434                                try {
435                                        loadableResource.load();
436                                } catch (Exception e) {
437                                        LOG.log(Level.SEVERE, "Failed to update remote resource: "
438                                                        + loadableResource.getResourceId(), e);
439                                }
440                        }
441                };
442                Map<String, String> props = loadableResource.getUpdateConfig();
443                if (props != null) {
444                        String value = props.get("period");
445                        long periodMS = parseDuration(value);
446                        value = props.get("delay");
447                        long delayMS = periodMS = parseDuration(value);
448                        if (periodMS > 0) {
449                                timer.scheduleAtFixedRate(task, delayMS, periodMS);
450                        }
451                        value = props.get("at");
452                        if (value != null) {
453                                List<GregorianCalendar> dates = parseDates(value);
454                                for (GregorianCalendar date : dates) {
455                                        timer.schedule(task, date.getTime(), 3600000 * 24 /* daily */);
456                                }
457                        }
458                }
459        }
460
461        /**
462         * Parse the dates of type HH:mm:ss:nnn, whereas minutes and smaller are
463         * optional.
464         * 
465         * @param value
466         *            the input text
467         * @return the parsed
468         */
469        private List<GregorianCalendar> parseDates(String value) {
470                String[] parts = value.split(",");
471                List<GregorianCalendar> result = new ArrayList<>();
472                for (String part : parts) {
473                        if (part.isEmpty()) {
474                                continue;
475                        }
476                        String[] subparts = part.split(":");
477                        GregorianCalendar cal = new GregorianCalendar();
478                        for (int i = 0; i < subparts.length; i++) {
479                                switch (i) {
480                                case 0:
481                                        cal.set(GregorianCalendar.HOUR_OF_DAY,
482                                                        Integer.parseInt(subparts[i]));
483                                        break;
484                                case 1:
485                                        cal.set(GregorianCalendar.MINUTE,
486                                                        Integer.parseInt(subparts[i]));
487                                        break;
488                                case 2:
489                                        cal.set(GregorianCalendar.SECOND,
490                                                        Integer.parseInt(subparts[i]));
491                                        break;
492                                case 3:
493                                        cal.set(GregorianCalendar.MILLISECOND,
494                                                        Integer.parseInt(subparts[i]));
495                                        break;
496                                }
497                        }
498                        result.add(cal);
499                }
500                return result;
501        }
502
503        /**
504         * Parse a duration of the form HH:mm:ss:nnn, whereas only hours are non
505         * optional.
506         * 
507         * @param value
508         *            the input value
509         * @return the duration in ms.
510         */
511        protected long parseDuration(String value) {
512                long periodMS = 0L;
513                if (value != null) {
514                        String[] parts = value.split(":");
515                        for (int i = 0; i < parts.length; i++) {
516                                switch (i) {
517                                case 0: // hours
518                                        periodMS += (Integer.parseInt(parts[i])) * 3600000L;
519                                        break;
520                                case 1: // minutes
521                                        periodMS += (Integer.parseInt(parts[i])) * 60000L;
522                                        break;
523                                case 2: // seconds
524                                        periodMS += (Integer.parseInt(parts[i])) * 1000L;
525                                        break;
526                                case 3: // ms
527                                        periodMS += (Integer.parseInt(parts[i]));
528                                        break;
529                                default:
530                                        break;
531                                }
532                        }
533                }
534                return periodMS;
535        }
536
537        @Override
538        public String toString() {
539                return "DefaultLoaderService [resources=" + resources
540                                + ", resourceCache=" + resourceCache + "]";
541        }
542        
543        
544
545}