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}