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 java.io.ByteArrayInputStream; 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.lang.ref.SoftReference; 023import java.net.URI; 024import java.net.URL; 025import java.net.URLConnection; 026import java.util.ArrayList; 027import java.util.Arrays; 028import java.util.Collections; 029import java.util.List; 030import java.util.Map; 031import java.util.Objects; 032import java.util.concurrent.atomic.AtomicInteger; 033import java.util.logging.Level; 034import java.util.logging.Logger; 035 036import org.javamoney.moneta.spi.LoadDataInformation; 037import org.javamoney.moneta.spi.LoaderService; 038 039/** 040 * This class represent a resource that automatically is reloaded, if needed. 041 * To create this instance use: {@link LoadableResourceBuilder} 042 * @author Anatole Tresch 043 */ 044public class LoadableResource { 045 046 /** 047 * The logger used. 048 */ 049 private static final Logger LOG = Logger.getLogger(LoadableResource.class.getName()); 050 /** 051 * Lock for this instance. 052 */ 053 private final Object lock = new Object(); 054 /** 055 * resource id. 056 */ 057 private final String resourceId; 058 /** 059 * The remote URLs to be looked up (first wins). 060 */ 061 private final List<URI> remoteResources = new ArrayList<>(); 062 /** 063 * The fallback location (classpath). 064 */ 065 private final URI fallbackLocation; 066 /** 067 * The cache used. 068 */ 069 private final ResourceCache cache; 070 /** 071 * How many times this resource was successfully loaded. 072 */ 073 private final AtomicInteger loadCount = new AtomicInteger(); 074 /** 075 * How many times this resource was accessed. 076 */ 077 private final AtomicInteger accessCount = new AtomicInteger(); 078 /** 079 * The current data array. 080 */ 081 private volatile SoftReference<byte[]> data; 082 /** 083 * THe timestamp of the last successful load. 084 */ 085 private long lastLoaded; 086 /** 087 * The time to live (TTL) of cache entries in milliseconds, by default 24 h. 088 */ 089 private long cacheTTLMillis = 3600000L * 24; // 24 h 090 091 /** 092 * The required update policy for this resource. 093 */ 094 private final LoaderService.UpdatePolicy updatePolicy; 095 /** 096 * The resource configuration. 097 */ 098 private final Map<String, String> properties; 099 100 101 LoadableResource(ResourceCache cache, LoadDataInformation loadDataInformation) { 102 103 104 Objects.requireNonNull(loadDataInformation.getResourceId(), "resourceId required"); 105 Objects.requireNonNull(loadDataInformation.getProperties(), "properties required"); 106 Objects.requireNonNull(loadDataInformation.getUpdatePolicy(), "updatePolicy required"); 107 String val = loadDataInformation.getProperties().get("cacheTTLMillis"); 108 if (val != null) { 109 this.cacheTTLMillis = Long.parseLong(val); 110 } 111 this.cache = cache; 112 this.resourceId = loadDataInformation.getResourceId(); 113 this.updatePolicy = loadDataInformation.getUpdatePolicy(); 114 this.properties = loadDataInformation.getProperties(); 115 this.fallbackLocation = loadDataInformation.getBackupResource(); 116 this.remoteResources.addAll(Arrays.asList(loadDataInformation.getResourceLocations())); 117 } 118 119 /** 120 * Get the UpdatePolicy of this resource. 121 * 122 * @return the UpdatePolicy of this resource, never null. 123 */ 124 public LoaderService.UpdatePolicy getUpdatePolicy() { 125 return updatePolicy; 126 } 127 128 /** 129 * Get the configuration properties of this resource. 130 * 131 * @return the configuration properties of this resource, never null. 132 */ 133 public Map<String, String> getProperties() { 134 return properties; 135 } 136 137 /** 138 * Loads the resource, first from the remote resources, if that fails from 139 * the fallback location. 140 * 141 * @return true, if load succeeded. 142 */ 143 public boolean load() { 144 if ((lastLoaded + cacheTTLMillis) <= System.currentTimeMillis()) { 145 clearCache(); 146 } 147 if (!readCache()) { 148 if (shouldReadDataFromCallBack()) { 149 return loadFallback(); 150 } 151 } 152 return true; 153 } 154 155 private boolean shouldReadDataFromCallBack() { 156 return LoaderService.UpdatePolicy.NEVER.equals(updatePolicy) || !loadRemote(); 157 } 158 159 /** 160 * Get the resourceId. 161 * 162 * @return the resourceId 163 */ 164 public final String getResourceId() { 165 return resourceId; 166 } 167 168 /** 169 * Get the remote locations. 170 * 171 * @return the remote locations, maybe empty. 172 */ 173 public final List<URI> getRemoteResources() { 174 return Collections.unmodifiableList(remoteResources); 175 } 176 177 /** 178 * Return the fallback location. 179 * 180 * @return the fallback location, or null. 181 */ 182 public final URI getFallbackResource() { 183 return fallbackLocation; 184 } 185 186 /** 187 * Get the number of active loads of this resource (InputStream). 188 * 189 * @return the number of successful loads. 190 */ 191 public final int getLoadCount() { 192 return loadCount.get(); 193 } 194 195 /** 196 * Get the number of successful accesses. 197 * 198 * @return the number of successful accesses. 199 */ 200 public final int getAccessCount() { 201 return accessCount.get(); 202 } 203 204 /** 205 * Get the resource data as input stream. 206 * 207 * @return the input stream. 208 */ 209 public InputStream getDataStream() { 210 return new WrappedInputStream(new ByteArrayInputStream(getData())); 211 } 212 213 /** 214 * Get the timestamp of the last succesful load. 215 * 216 * @return the lastLoaded 217 */ 218 public final long getLastLoaded() { 219 return lastLoaded; 220 } 221 222 /** 223 * Try to load the resource from the remote locations. 224 * 225 * @return true, on success. 226 */ 227 public boolean loadRemote() { 228 for (URI itemToLoad : remoteResources) { 229 try { 230 return !load(itemToLoad, false); 231 } catch (Exception e) { 232 LOG.log(Level.INFO, "Failed to load resource: " + itemToLoad, e); 233 } 234 } 235 return true; 236 } 237 238 /** 239 * Try to load the resource from the fallback resources. This will override 240 * any remote data already loaded, and also will clear the cached data. 241 * 242 * @return true, on success. 243 */ 244 public boolean loadFallback() { 245 try { 246 if (fallbackLocation == null) { 247 Logger.getLogger(getClass().getName()).warning("No fallback resource for " + this + 248 ", loadFallback not supported."); 249 return false; 250 } 251 load(fallbackLocation, true); 252 clearCache(); 253 return true; 254 } catch (Exception e) { 255 LOG.log(Level.SEVERE, "Failed to load fallback resource: " + fallbackLocation, e); 256 } 257 return false; 258 } 259 260 /** 261 * This method is called when the cached data should be removed, e.g. after an explicit fallback reload, or 262 * a clear operation. 263 */ 264 protected void clearCache() { 265 if (this.cache != null) { 266 this.cache.clear(resourceId); 267 } 268 } 269 270 /** 271 * This method is called when the data should be loaded from the cache. This method abstracts the effective 272 * caching mechanism implemented. By default it tries to read a file from the current user's home directory. 273 * If the data could be read, #setData(byte[]) should be called to apply the data read. 274 * 275 * @return true, if data could be read and applied from the cache sucdcessfully. 276 */ 277 protected boolean readCache() { 278 if (this.cache != null) { 279 if (this.cache.isCached(resourceId)) { 280 byte[] data = this.cache.read(resourceId); 281 if (data != null) { 282 setData(data); 283 return true; 284 } 285 } 286 } 287 return false; 288 } 289 290 /** 291 * This method is called after data could be successfully loaded from a non fallback resource. This method by 292 * default writes an file containing the data into the user's local home directory, so subsequent or later calls, 293 * even after a VM restart, should be able to recover this information. 294 */ 295 protected void writeCache() throws IOException { 296 if (this.cache != null) { 297 byte[] data = this.data == null ? null : this.data.get(); 298 if (data == null) { 299 return; 300 } 301 this.cache.write(resourceId, data); 302 } 303 } 304 305 /** 306 * Tries to load the data from the given location. The location hereby can be a remote location or a local 307 * location. Also it can be an URL pointing to a current dataset, or an url directing to fallback resources, 308 * e.g. within the cuzrrent classpath. 309 * 310 * @param itemToLoad the target {@link URL} 311 * @param fallbackLoad true, for a fallback URL. 312 */ 313 protected boolean load(URI itemToLoad, boolean fallbackLoad) { 314 InputStream is = null; 315 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 316 try { 317 URLConnection conn = itemToLoad.toURL().openConnection(); 318 byte[] data = new byte[4096]; 319 is = conn.getInputStream(); 320 int read = is.read(data); 321 while (read > 0) { 322 stream.write(data, 0, read); 323 read = is.read(data); 324 } 325 setData(stream.toByteArray()); 326 if (!fallbackLoad) { 327 writeCache(); 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 stream.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 (shouldReadDataFromCallBack()) { 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}