001 /* 002 * Copyright 2001-2009 Stephen Colebourne 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 */ 016 package org.joda.time.tz; 017 018 import java.io.DataInputStream; 019 import java.io.File; 020 import java.io.FileInputStream; 021 import java.io.IOException; 022 import java.io.InputStream; 023 import java.lang.ref.SoftReference; 024 import java.util.Map; 025 import java.util.Set; 026 import java.util.TreeSet; 027 import java.util.concurrent.ConcurrentHashMap; 028 029 import org.joda.time.DateTimeZone; 030 031 /** 032 * ZoneInfoProvider loads compiled data files as generated by 033 * {@link ZoneInfoCompiler}. 034 * <p> 035 * ZoneInfoProvider is thread-safe and publicly immutable. 036 * 037 * @author Brian S O'Neill 038 * @since 1.0 039 */ 040 public class ZoneInfoProvider implements Provider { 041 042 /** The directory where the files are held. */ 043 private final File iFileDir; 044 /** The resource path. */ 045 private final String iResourcePath; 046 /** The class loader to use. */ 047 private final ClassLoader iLoader; 048 /** Maps ids to strings or SoftReferences to DateTimeZones. */ 049 private final Map<String, Object> iZoneInfoMap; 050 051 /** 052 * ZoneInfoProvider searches the given directory for compiled data files. 053 * 054 * @throws IOException if directory or map file cannot be read 055 */ 056 public ZoneInfoProvider(File fileDir) throws IOException { 057 if (fileDir == null) { 058 throw new IllegalArgumentException("No file directory provided"); 059 } 060 if (!fileDir.exists()) { 061 throw new IOException("File directory doesn't exist: " + fileDir); 062 } 063 if (!fileDir.isDirectory()) { 064 throw new IOException("File doesn't refer to a directory: " + fileDir); 065 } 066 067 iFileDir = fileDir; 068 iResourcePath = null; 069 iLoader = null; 070 071 iZoneInfoMap = loadZoneInfoMap(openResource("ZoneInfoMap")); 072 } 073 074 /** 075 * ZoneInfoProvider searches the given ClassLoader resource path for 076 * compiled data files. Resources are loaded from the ClassLoader that 077 * loaded this class. 078 * 079 * @throws IOException if directory or map file cannot be read 080 */ 081 public ZoneInfoProvider(String resourcePath) throws IOException { 082 this(resourcePath, null, false); 083 } 084 085 /** 086 * ZoneInfoProvider searches the given ClassLoader resource path for 087 * compiled data files. 088 * 089 * @param loader ClassLoader to load compiled data files from. If null, 090 * use system ClassLoader. 091 * @throws IOException if directory or map file cannot be read 092 */ 093 public ZoneInfoProvider(String resourcePath, ClassLoader loader) 094 throws IOException 095 { 096 this(resourcePath, loader, true); 097 } 098 099 /** 100 * @param favorSystemLoader when true, use the system class loader if 101 * loader null. When false, use the current class loader if loader is null. 102 */ 103 private ZoneInfoProvider(String resourcePath, 104 ClassLoader loader, boolean favorSystemLoader) 105 throws IOException 106 { 107 if (resourcePath == null) { 108 throw new IllegalArgumentException("No resource path provided"); 109 } 110 if (!resourcePath.endsWith("/")) { 111 resourcePath += '/'; 112 } 113 114 iFileDir = null; 115 iResourcePath = resourcePath; 116 117 if (loader == null && !favorSystemLoader) { 118 loader = getClass().getClassLoader(); 119 } 120 121 iLoader = loader; 122 123 iZoneInfoMap = loadZoneInfoMap(openResource("ZoneInfoMap")); 124 } 125 126 //----------------------------------------------------------------------- 127 /** 128 * If an error is thrown while loading zone data, uncaughtException is 129 * called to log the error and null is returned for this and all future 130 * requests. 131 * 132 * @param id the id to load 133 * @return the loaded zone 134 */ 135 public DateTimeZone getZone(String id) { 136 if (id == null) { 137 return null; 138 } 139 140 Object obj = iZoneInfoMap.get(id); 141 if (obj == null) { 142 return null; 143 } 144 145 if (id.equals(obj)) { 146 // Load zone data for the first time. 147 return loadZoneData(id); 148 } 149 150 if (obj instanceof SoftReference<?>) { 151 @SuppressWarnings("unchecked") 152 SoftReference<DateTimeZone> ref = (SoftReference<DateTimeZone>) obj; 153 DateTimeZone tz = ref.get(); 154 if (tz != null) { 155 return tz; 156 } 157 // Reference cleared; load data again. 158 return loadZoneData(id); 159 } 160 161 // If this point is reached, mapping must link to another. 162 return getZone((String)obj); 163 } 164 165 /** 166 * Gets a list of all the available zone ids. 167 * 168 * @return the zone ids 169 */ 170 public Set<String> getAvailableIDs() { 171 // Return a copy of the keys rather than an umodifiable collection. 172 // This prevents ConcurrentModificationExceptions from being thrown by 173 // some JVMs if zones are opened while this set is iterated over. 174 return new TreeSet<String>(iZoneInfoMap.keySet()); 175 } 176 177 /** 178 * Called if an exception is thrown from getZone while loading zone data. 179 * 180 * @param ex the exception 181 */ 182 protected void uncaughtException(Exception ex) { 183 Thread t = Thread.currentThread(); 184 t.getThreadGroup().uncaughtException(t, ex); 185 } 186 187 /** 188 * Opens a resource from file or classpath. 189 * 190 * @param name the name to open 191 * @return the input stream 192 * @throws IOException if an error occurs 193 */ 194 private InputStream openResource(String name) throws IOException { 195 InputStream in; 196 if (iFileDir != null) { 197 in = new FileInputStream(new File(iFileDir, name)); 198 } else { 199 String path = iResourcePath.concat(name); 200 if (iLoader != null) { 201 in = iLoader.getResourceAsStream(path); 202 } else { 203 in = ClassLoader.getSystemResourceAsStream(path); 204 } 205 if (in == null) { 206 StringBuilder buf = new StringBuilder(40) 207 .append("Resource not found: \"") 208 .append(path) 209 .append("\" ClassLoader: ") 210 .append(iLoader != null ? iLoader.toString() : "system"); 211 throw new IOException(buf.toString()); 212 } 213 } 214 return in; 215 } 216 217 /** 218 * Loads the time zone data for one id. 219 * 220 * @param id the id to load 221 * @return the zone 222 */ 223 private DateTimeZone loadZoneData(String id) { 224 InputStream in = null; 225 try { 226 in = openResource(id); 227 DateTimeZone tz = DateTimeZoneBuilder.readFrom(in, id); 228 iZoneInfoMap.put(id, new SoftReference<DateTimeZone>(tz)); 229 return tz; 230 } catch (IOException ex) { 231 uncaughtException(ex); 232 iZoneInfoMap.remove(id); 233 return null; 234 } finally { 235 try { 236 if (in != null) { 237 in.close(); 238 } 239 } catch (IOException ex) { 240 } 241 } 242 } 243 244 //----------------------------------------------------------------------- 245 /** 246 * Loads the zone info map. 247 * 248 * @param in the input stream 249 * @return the map 250 */ 251 private static Map<String, Object> loadZoneInfoMap(InputStream in) throws IOException { 252 Map<String, Object> map = new ConcurrentHashMap<String, Object>(); 253 DataInputStream din = new DataInputStream(in); 254 try { 255 readZoneInfoMap(din, map); 256 } finally { 257 try { 258 din.close(); 259 } catch (IOException ex) { 260 } 261 } 262 map.put("UTC", new SoftReference<DateTimeZone>(DateTimeZone.UTC)); 263 return map; 264 } 265 266 /** 267 * Reads the zone info map from file. 268 * 269 * @param din the input stream 270 * @param zimap gets filled with string id to string id mappings 271 */ 272 private static void readZoneInfoMap(DataInputStream din, Map<String, Object> zimap) throws IOException { 273 // Read the string pool. 274 int size = din.readUnsignedShort(); 275 String[] pool = new String[size]; 276 for (int i=0; i<size; i++) { 277 pool[i] = din.readUTF().intern(); 278 } 279 280 // Read the mappings. 281 size = din.readUnsignedShort(); 282 for (int i=0; i<size; i++) { 283 try { 284 zimap.put(pool[din.readUnsignedShort()], pool[din.readUnsignedShort()]); 285 } catch (ArrayIndexOutOfBoundsException ex) { 286 throw new IOException("Corrupt zone info map"); 287 } 288 } 289 } 290 291 }