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}