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