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 java.io.ByteArrayInputStream; 021import java.io.ByteArrayOutputStream; 022import java.io.IOException; 023import java.io.InputStream; 024import java.lang.ref.SoftReference; 025import java.net.URI; 026import java.net.URL; 027import java.net.URLConnection; 028import java.util.*; 029import java.util.concurrent.atomic.AtomicInteger; 030import java.util.logging.Level; 031import java.util.logging.Logger; 032 033/** 034 * This class represent a resource that automatically is reloaded, if needed. 035 * 036 * @author Anatole Tresch 037 */ 038public class LoadableResource { 039 040 /** 041 * The logger used. 042 */ 043 private static final Logger LOG = Logger.getLogger(LoadableResource.class.getName()); 044 /** 045 * Lock for this instance. 046 */ 047 private final Object lock = new Object(); 048 /** 049 * resource id. 050 */ 051 private final String resourceId; 052 /** 053 * The remote URLs to be looked up (first wins). 054 */ 055 private final List<URI> remoteResources = new ArrayList<>(); 056 /** 057 * The fallback location (classpath). 058 */ 059 private final URI fallbackLocation; 060 /** 061 * The cache used. 062 */ 063 private final ResourceCache cache; 064 /** 065 * How many times this resource was successfully loaded. 066 */ 067 private final AtomicInteger loadCount = new AtomicInteger(); 068 /** 069 * How many times this resource was accessed. 070 */ 071 private final AtomicInteger accessCount = new AtomicInteger(); 072 /** 073 * The current data array. 074 */ 075 private volatile SoftReference<byte[]> data; 076 /** 077 * THe timestamp of the last successful load. 078 */ 079 private long lastLoaded; 080 /** 081 * The time to live (TTL) of cache entries in milliseconds, by default 24 h. 082 */ 083 private long cacheTTLMillis = 3600000L * 24; // 24 h 084 085 /** 086 * The required update policy for this resource. 087 */ 088 private final LoaderService.UpdatePolicy updatePolicy; 089 /** 090 * The resource configuration. 091 */ 092 private final Map<String, String> properties; 093 094 095 /** 096 * Create a new instance. 097 * 098 * @param resourceId The dataId. 099 * @param cache The cache to be used for storing remote data locally. 100 * @param properties The configuration properties. 101 * @param fallbackLocation teh fallback ULR, not null. 102 * @param locations the remote locations, not null (but may be empty!) 103 */ 104 public LoadableResource(String resourceId, ResourceCache cache, LoaderService.UpdatePolicy updatePolicy, 105 Map<String, String> properties, URI fallbackLocation, URI... locations) { 106 Objects.requireNonNull(resourceId, "resourceId required"); 107 Objects.requireNonNull(properties, "properties required"); 108 Objects.requireNonNull(updatePolicy, "updatePolicy required"); 109 String val = properties.get("cacheTTLMillis"); 110 if (val != null) { 111 this.cacheTTLMillis = Long.parseLong(val); 112 } 113 this.cache = cache; 114 this.resourceId = resourceId; 115 this.updatePolicy = updatePolicy; 116 this.properties = properties; 117 this.fallbackLocation = fallbackLocation; 118 this.remoteResources.addAll(Arrays.asList(locations)); 119 } 120 121 /** 122 * Get the UpdatePolicy of this resource. 123 * 124 * @return the UpdatePolicy of this resource, never null. 125 */ 126 public LoaderService.UpdatePolicy getUpdatePolicy() { 127 return updatePolicy; 128 } 129 130 /** 131 * Get the configuration properties of this resource. 132 * 133 * @return the configuration properties of this resource, never null. 134 */ 135 public Map<String, String> getProperties() { 136 return properties; 137 } 138 139 /** 140 * Loads the resource, first from the remote resources, if that fails from 141 * the fallback location. 142 * 143 * @return true, if load succeeded. 144 */ 145 public boolean load() { 146 if ((lastLoaded + cacheTTLMillis) <= System.currentTimeMillis()) { 147 clearCache(); 148 } 149 if (!readCache()) { 150 if (loadRemote()) { 151 return loadFallback(); 152 } 153 } 154 return true; 155 } 156 157 /** 158 * Get the resourceId. 159 * 160 * @return the resourceId 161 */ 162 public final String getResourceId() { 163 return resourceId; 164 } 165 166 /** 167 * Get the remote locations. 168 * 169 * @return the remote locations, maybe empty. 170 */ 171 public final List<URI> getRemoteResources() { 172 return Collections.unmodifiableList(remoteResources); 173 } 174 175 /** 176 * Return the fallback location. 177 * 178 * @return the fallback location, or null. 179 */ 180 public final URI getFallbackResource() { 181 return fallbackLocation; 182 } 183 184 /** 185 * Get the number of active loads of this resource (InputStream). 186 * 187 * @return the number of successful loads. 188 */ 189 public final int getLoadCount() { 190 return loadCount.get(); 191 } 192 193 /** 194 * Get the number of successful accesses. 195 * 196 * @return the number of successful accesses. 197 */ 198 public final int getAccessCount() { 199 return accessCount.get(); 200 } 201 202 /** 203 * Get the resource data as input stream. 204 * 205 * @return the input stream. 206 */ 207 public InputStream getDataStream() { 208 return new WrappedInputStream(new ByteArrayInputStream(getData())); 209 } 210 211 /** 212 * Get the timestamp of the last succesful load. 213 * 214 * @return the lastLoaded 215 */ 216 public final long getLastLoaded() { 217 return lastLoaded; 218 } 219 220 /** 221 * Try to load the resource from the remote locations. 222 * 223 * @return true, on success. 224 */ 225 public boolean loadRemote() { 226 for (URI itemToLoad : remoteResources) { 227 try { 228 return !load(itemToLoad, false); 229 } catch (Exception e) { 230 LOG.log(Level.INFO, "Failed to load resource: " + itemToLoad, e); 231 } 232 } 233 return true; 234 } 235 236 /** 237 * Try to load the resource from the fallback resources. This will override 238 * any remote data already loaded, and also will clear the cached data. 239 * 240 * @return true, on success. 241 */ 242 public boolean loadFallback() { 243 try { 244 if (fallbackLocation == null) { 245 Logger.getLogger(getClass().getName()).warning("No fallback resource for " + this + 246 ", loadFallback not supported."); 247 return false; 248 } 249 load(fallbackLocation, true); 250 clearCache(); 251 return true; 252 } catch (Exception e) { 253 LOG.log(Level.SEVERE, "Failed to load fallback resource: " + fallbackLocation, e); 254 } 255 return false; 256 } 257 258 /** 259 * This method is called when the cached data should be removed, e.g. after an explicit fallback reload, or 260 * a clear operation. 261 */ 262 protected void clearCache() { 263 if (this.cache != null) { 264 this.cache.clear(resourceId); 265 } 266 } 267 268 /** 269 * This method is called when the data should be loaded from the cache. This method abstracts the effective 270 * caching mechanism implemented. By default it tries to read a file from the current user's home directory. 271 * If the data could be read, #setData(byte[]) should be called to apply the data read. 272 * 273 * @return true, if data could be read and applied from the cache sucdcessfully. 274 */ 275 protected boolean readCache() { 276 if (this.cache != null) { 277 if (this.cache.isCached(resourceId)) { 278 byte[] data = this.cache.read(resourceId); 279 if (data != null) { 280 setData(data); 281 return true; 282 } 283 } 284 } 285 return false; 286 } 287 288 /** 289 * This method is called after data could be successfully loaded from a non fallback resource. This method by 290 * default writes an file containing the data into the user's local home directory, so subsequent or later calls, 291 * even after a VM restart, should be able to recover this information. 292 */ 293 protected void writeCache() throws IOException { 294 if (this.cache != null) { 295 byte[] data = this.data == null ? null : this.data.get(); 296 if (data == null) { 297 return; 298 } 299 this.cache.write(resourceId, data); 300 } 301 } 302 303 /** 304 * Tries to load the data from the given location. The location hereby can be a remote location or a local 305 * location. Also it can be an URL pointing to a current dataset, or an url directing to fallback resources, 306 * e.g. within the cuzrrent classpath. 307 * 308 * @param itemToLoad the target {@link URL} 309 * @param fallbackLoad true, for a fallback URL. 310 */ 311 protected boolean load(URI itemToLoad, boolean fallbackLoad) { 312 InputStream is = null; 313 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 314 try { 315 URLConnection conn = itemToLoad.toURL().openConnection(); 316 byte[] data = new byte[4096]; 317 is = conn.getInputStream(); 318 int read = is.read(data); 319 while (read > 0) { 320 bos.write(data, 0, read); 321 read = is.read(data); 322 } 323 setData(bos.toByteArray()); 324 if (!fallbackLoad) { 325 writeCache(); 326 } 327 if (!fallbackLoad) { 328 lastLoaded = System.currentTimeMillis(); 329 loadCount.incrementAndGet(); 330 } 331 return true; 332 } catch (Exception e) { 333 LOG.log(Level.INFO, "Failed to load resource input for " + resourceId + " from " + itemToLoad, e); 334 } finally { 335 if (Objects.nonNull(is)) { 336 try { 337 is.close(); 338 } catch (Exception e) { 339 LOG.log(Level.INFO, "Error closing resource input for " + resourceId, e); 340 } 341 } 342 try { 343 bos.close(); 344 } catch (IOException e) { 345 LOG.log(Level.INFO, "Error closing resource input for " + resourceId, e); 346 } 347 } 348 return false; 349 } 350 351 /** 352 * Get the resource data. This will trigger a full load, if the resource is 353 * not loaded, e.g. for LAZY resources. 354 * 355 * @return the data to load. 356 */ 357 public final byte[] getData() { 358 return getData(true); 359 } 360 361 protected byte[] getData(boolean loadIfNeeded) { 362 byte[] result = this.data == null ? null : this.data.get(); 363 if (result == null && loadIfNeeded) { 364 accessCount.incrementAndGet(); 365 byte[] currentData = this.data == null ? null : this.data.get(); 366 if (Objects.isNull(currentData)) { 367 synchronized (lock) { 368 currentData = this.data == null ? null : this.data.get(); 369 if (Objects.isNull(currentData)) { 370 if (loadRemote()) { 371 loadFallback(); 372 } 373 } 374 } 375 } 376 currentData = this.data == null ? null : this.data.get(); 377 if (Objects.isNull(currentData)) { 378 throw new IllegalStateException("Failed to load remote as well as fallback resources for " + this); 379 } 380 return currentData.clone(); 381 } 382 return result; 383 } 384 385 protected final void setData(byte[] bytes) { 386 this.data = new SoftReference<>(bytes); 387 } 388 389 390 public void unload() { 391 synchronized (lock) { 392 int count = accessCount.decrementAndGet(); 393 if (count == 0) { 394 this.data = null; 395 } 396 } 397 } 398 399 /** 400 * Explicitly override the resource wih the fallback context and resets the 401 * load counter. 402 * 403 * @return true on success. 404 * @throws IOException 405 */ 406 public boolean resetToFallback() { 407 if (loadFallback()) { 408 loadCount.set(0); 409 return true; 410 } 411 return false; 412 } 413 414 @Override 415 public String toString() { 416 return "LoadableResource [resourceId=" + resourceId + ", fallbackLocation=" + 417 fallbackLocation + ", remoteResources=" + remoteResources + 418 ", loadCount=" + loadCount + ", accessCount=" + accessCount + ", lastLoaded=" + lastLoaded + ']'; 419 } 420 421 /** 422 * InputStream , that helps managing the load count. 423 * 424 * @author Anatole 425 */ 426 private final class WrappedInputStream extends InputStream { 427 428 private final InputStream wrapped; 429 430 WrappedInputStream(InputStream wrapped) { 431 this.wrapped = wrapped; 432 } 433 434 @Override 435 public int read() throws IOException { 436 return wrapped.read(); 437 } 438 439 @Override 440 public void close() throws IOException { 441 try { 442 wrapped.close(); 443 super.close(); 444 } finally { 445 unload(); 446 } 447 } 448 449 } 450 451}