001 /* 002 * Copyright 2001-2005 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.TreeMap; 027 import java.util.TreeSet; 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 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 synchronized 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 DateTimeZone tz = (DateTimeZone)((SoftReference)obj).get(); 152 if (tz != null) { 153 return tz; 154 } 155 // Reference cleared; load data again. 156 return loadZoneData(id); 157 } 158 159 // If this point is reached, mapping must link to another. 160 return getZone((String)obj); 161 } 162 163 /** 164 * Gets a list of all the available zone ids. 165 * 166 * @return the zone ids 167 */ 168 public synchronized Set getAvailableIDs() { 169 // Return a copy of the keys rather than an umodifiable collection. 170 // This prevents ConcurrentModificationExceptions from being thrown by 171 // some JVMs if zones are opened while this set is iterated over. 172 return new TreeSet(iZoneInfoMap.keySet()); 173 } 174 175 /** 176 * Called if an exception is thrown from getZone while loading zone data. 177 * 178 * @param ex the exception 179 */ 180 protected void uncaughtException(Exception ex) { 181 Thread t = Thread.currentThread(); 182 t.getThreadGroup().uncaughtException(t, ex); 183 } 184 185 /** 186 * Opens a resource from file or classpath. 187 * 188 * @param name the name to open 189 * @return the input stream 190 * @throws IOException if an error occurs 191 */ 192 private InputStream openResource(String name) throws IOException { 193 InputStream in; 194 if (iFileDir != null) { 195 in = new FileInputStream(new File(iFileDir, name)); 196 } else { 197 String path = iResourcePath.concat(name); 198 if (iLoader != null) { 199 in = iLoader.getResourceAsStream(path); 200 } else { 201 in = ClassLoader.getSystemResourceAsStream(path); 202 } 203 if (in == null) { 204 StringBuffer buf = new StringBuffer(40) 205 .append("Resource not found: \"") 206 .append(path) 207 .append("\" ClassLoader: ") 208 .append(iLoader != null ? iLoader.toString() : "system"); 209 throw new IOException(buf.toString()); 210 } 211 } 212 return in; 213 } 214 215 /** 216 * Loads the time zone data for one id. 217 * 218 * @param id the id to load 219 * @return the zone 220 */ 221 private DateTimeZone loadZoneData(String id) { 222 InputStream in = null; 223 try { 224 in = openResource(id); 225 DateTimeZone tz = DateTimeZoneBuilder.readFrom(in, id); 226 iZoneInfoMap.put(id, new SoftReference(tz)); 227 return tz; 228 } catch (IOException e) { 229 uncaughtException(e); 230 iZoneInfoMap.remove(id); 231 return null; 232 } finally { 233 try { 234 if (in != null) { 235 in.close(); 236 } 237 } catch (IOException e) { 238 } 239 } 240 } 241 242 //----------------------------------------------------------------------- 243 /** 244 * Loads the zone info map. 245 * 246 * @param in the input stream 247 * @return the map 248 */ 249 private static Map loadZoneInfoMap(InputStream in) throws IOException { 250 Map map = new TreeMap(String.CASE_INSENSITIVE_ORDER); 251 DataInputStream din = new DataInputStream(in); 252 try { 253 readZoneInfoMap(din, map); 254 } finally { 255 try { 256 din.close(); 257 } catch (IOException e) { 258 } 259 } 260 map.put("UTC", new SoftReference(DateTimeZone.UTC)); 261 return map; 262 } 263 264 /** 265 * Reads the zone info map from file. 266 * 267 * @param din the input stream 268 * @param zimap gets filled with string id to string id mappings 269 */ 270 private static void readZoneInfoMap(DataInputStream din, Map zimap) throws IOException { 271 // Read the string pool. 272 int size = din.readUnsignedShort(); 273 String[] pool = new String[size]; 274 for (int i=0; i<size; i++) { 275 pool[i] = din.readUTF().intern(); 276 } 277 278 // Read the mappings. 279 size = din.readUnsignedShort(); 280 for (int i=0; i<size; i++) { 281 try { 282 zimap.put(pool[din.readUnsignedShort()], pool[din.readUnsignedShort()]); 283 } catch (ArrayIndexOutOfBoundsException e) { 284 throw new IOException("Corrupt zone info map"); 285 } 286 } 287 } 288 289 }