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}