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