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