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.ByteArrayInputStream; 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.InputStream; 022import java.net.URL; 023import java.net.URLConnection; 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.LoaderService.UpdatePolicy; 035 036/** 037 * This class represent a resource that automatically is reloaded, if needed. 038 * 039 * @author Anatole Tresch 040 */ 041public class LoadableResource { 042 /** The logger used. */ 043 private static final Logger LOG = Logger.getLogger(LoadableResource.class 044 .getName()); 045 /** Lock for this instance. */ 046 private final Object LOCK = new Object(); 047 /** resource id. */ 048 private String resourceId; 049 /** The {@link UpdatePolicy}. */ 050 private UpdatePolicy updatePolicy; 051 /** The remote URLs to be looked up (first wins). */ 052 private List<URL> remoteResources = new ArrayList<>(); 053 /** The fallback location (classpath). */ 054 private URL fallbackLocation; 055 /** The cached resource URL. */ 056 private URL cachedResource; 057 /** How many times this resource was successfully loaded. */ 058 private AtomicInteger loadCount = new AtomicInteger(); 059 /** How many times this resource was accessed. */ 060 private AtomicInteger accessCount = new AtomicInteger(); 061 /** The current data array. */ 062 private volatile byte[] data; 063 /** THe timestamp of the last successful load. */ 064 private long lastLoaded; 065 /** The registration config. */ 066 private Map<String, String> updateConfig; 067 068 /** 069 * Create a new instance. 070 * 071 * @param resourceId 072 * The dataId. 073 * @param updatePolicy 074 * The {@link UpdatePolicy}, not null. 075 * @param fallbackLocation 076 * teh fallback ULR, not null. 077 * @param locations 078 * the remote locations, not null (but may be empty!) 079 */ 080 public LoadableResource(String resourceId, UpdatePolicy updatePolicy, 081 URL fallbackLocation, URL... locations) { 082 Objects.requireNonNull(resourceId, "resourceId required"); 083 Objects.requireNonNull(fallbackLocation, "classpathDefault required"); 084 Objects.requireNonNull(updatePolicy, "UpdatePolicy required"); 085 this.resourceId = resourceId; 086 this.fallbackLocation = fallbackLocation; 087 this.remoteResources.addAll(Arrays.asList(locations)); 088 this.updatePolicy = updatePolicy; 089 } 090 091 /** 092 * Loads the resource, first from the remote resources, if that fails from 093 * the fallback location. 094 * 095 * @return true, if load succeeded. 096 */ 097 public boolean load() { 098 if (!loadRemote()) { 099 return loadFallback(); 100 } 101 return true; 102 } 103 104 /** 105 * Get the resourceId. 106 * 107 * @return the resourceId 108 */ 109 public final String getResourceId() { 110 return resourceId; 111 } 112 113 /** 114 * Get the {@link UpdatePolicy}. 115 * 116 * @return the updatePolicy 117 */ 118 public UpdatePolicy getUpdatePolicy() { 119 return updatePolicy; 120 } 121 122 /** 123 * Get the remote locations. 124 * 125 * @return the remote locations, maybe empty. 126 */ 127 public final List<URL> getRemoteResources() { 128 return Collections.unmodifiableList(remoteResources); 129 } 130 131 /** 132 * Return the fallback location. 133 * 134 * @return the fallback location 135 */ 136 public final URL getFallbackResource() { 137 return fallbackLocation; 138 } 139 140 /** 141 * Get the URL of the locally cached resource. 142 * 143 * @return the cachedResource 144 */ 145 public final URL getCachedResource() { 146 return cachedResource; 147 } 148 149 /** 150 * Get the number of active loads of this resource (InputStream). 151 * 152 * @return the number of successful loads. 153 */ 154 public final int getLoadCount() { 155 return loadCount.get(); 156 } 157 158 /** 159 * Get the number of successful accesses. 160 * 161 * @return the number of successful accesses. 162 */ 163 public final int getAccessCount() { 164 return accessCount.get(); 165 } 166 167 /** 168 * Get the resource data. This will trigger a full load, if the resource is 169 * not loaded, e.g. for LAZY resources. 170 * 171 * @return the data to load. 172 */ 173 public final byte[] getData() { 174 accessCount.incrementAndGet(); 175 if (this.data == null) { 176 synchronized (LOCK) { 177 if (this.data == null) { 178 if (!loadRemote()) { 179 loadFallback(); 180 } 181 } 182 if (this.data == null) { 183 throw new IllegalStateException( 184 "Failed to load remote as well as fallback resources for " 185 + this); 186 } 187 } 188 } 189 return data.clone(); 190 } 191 192 /** 193 * Get the resource data as input stream. 194 * 195 * @return the input stream. 196 */ 197 public InputStream getDataStream() { 198 return new WrappedInputStream(new ByteArrayInputStream(getData())); 199 } 200 201 /** 202 * Get the timestamp of the last succesful load. 203 * 204 * @return the lastLoaded 205 */ 206 public final long getLastLoaded() { 207 return lastLoaded; 208 } 209 210 /** 211 * Try to load the resource from the remote locations. 212 * 213 * @return true, on success. 214 */ 215 public boolean loadRemote() { 216 for (URL itemToLoad : remoteResources) { 217 try { 218 load(itemToLoad, false); 219 return true; 220 } catch (Exception e) { 221 LOG.log(Level.INFO, "Failed to load resource: " + itemToLoad, e); 222 } 223 } 224 return false; 225 } 226 227 /** 228 * Try to load the resource from the faööback resources. This will override 229 * any remote data already loaded. 230 * 231 * @return true, on success. 232 */ 233 public boolean loadFallback() { 234 try { 235 load(fallbackLocation, true); 236 return true; 237 } catch (Exception e) { 238 LOG.log(Level.SEVERE, "Failed to load fallback resource: " 239 + fallbackLocation, e); 240 } 241 return false; 242 } 243 244 /** 245 * Load the data. 246 * 247 * @param itemToLoad 248 * the target {@link URL} 249 * @param fallbackLoad 250 * true, for a fallback URL. 251 * @throws IOException 252 * if load fails. 253 */ 254 private void load(URL itemToLoad, boolean fallbackLoad) throws IOException { 255 InputStream is = null; 256 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 257 try { 258 URLConnection conn = itemToLoad.openConnection(); 259 byte[] data = new byte[4096]; 260 is = conn.getInputStream(); 261 int read = is.read(data); 262 while (read > 0) { 263 bos.write(data, 0, read); 264 read = is.read(data); 265 } 266 this.data = bos.toByteArray(); 267 } finally { 268 if (is != null) { 269 try { 270 is.close(); 271 } catch (Exception e) { 272 LOG.log(Level.SEVERE, "Error closing resource input for " 273 + resourceId, e); 274 } 275 } 276 if (bos != null) { 277 bos.close(); 278 } 279 } 280 if (!fallbackLoad) { 281 lastLoaded = System.currentTimeMillis(); 282 loadCount.incrementAndGet(); 283 } 284 } 285 286 /** 287 * Unloads the data. 288 */ 289 public void unload() { 290 synchronized (LOCK) { 291 int count = accessCount.decrementAndGet(); 292 if (count == 0) { 293 this.data = null; 294 } 295 } 296 } 297 298 /** 299 * InputStream , that helps managing the load count. 300 * 301 * @author Anatole 302 * 303 */ 304 private final class WrappedInputStream extends InputStream { 305 306 private InputStream wrapped; 307 308 public WrappedInputStream(InputStream wrapped) { 309 this.wrapped = wrapped; 310 } 311 312 @Override 313 public int read() throws IOException { 314 return wrapped.read(); 315 } 316 317 @Override 318 public void close() throws IOException { 319 try { 320 wrapped.close(); 321 super.close(); 322 } finally { 323 unload(); 324 } 325 } 326 327 } 328 329 /** 330 * Explcitly override the resource wih the fallback context and resets the 331 * load counter. 332 * 333 * @return true on success. 334 * @throws IOException 335 */ 336 public boolean reset() throws IOException { 337 if (loadFallback()) { 338 loadCount.set(0); 339 return true; 340 } 341 return false; 342 } 343 344 /** 345 * Access the registration config. 346 * 347 * @return the config, not null. 348 */ 349 public Map<String, String> getUpdateConfig() { 350 return this.updateConfig; 351 } 352 353 @Override 354 public String toString() { 355 return "LoadableResource [resourceId=" + resourceId + ", updatePolicy=" 356 + updatePolicy + ", fallbackLocation=" + fallbackLocation 357 + ", remoteResources=" + remoteResources + ", cachedResource=" 358 + cachedResource + ", loadCount=" + loadCount 359 + ", accessCount=" + accessCount + ", lastLoaded=" + lastLoaded 360 + ", updateConfig=" + updateConfig + "]"; 361 } 362 363 364 365}