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