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