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 }