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(
059: "No file directory provided");
060: }
061: if (!fileDir.exists()) {
062: throw new IOException("File directory doesn't exist: "
063: + fileDir);
064: }
065: if (!fileDir.isDirectory()) {
066: throw new IOException("File doesn't refer to a directory: "
067: + fileDir);
068: }
069:
070: iFileDir = fileDir;
071: iResourcePath = null;
072: iLoader = null;
073:
074: iZoneInfoMap = loadZoneInfoMap(openResource("ZoneInfoMap"));
075: }
076:
077: /**
078: * ZoneInfoProvider searches the given ClassLoader resource path for
079: * compiled data files. Resources are loaded from the ClassLoader that
080: * loaded this class.
081: *
082: * @throws IOException if directory or map file cannot be read
083: */
084: public ZoneInfoProvider(String resourcePath) throws IOException {
085: this (resourcePath, null, false);
086: }
087:
088: /**
089: * ZoneInfoProvider searches the given ClassLoader resource path for
090: * compiled data files.
091: *
092: * @param loader ClassLoader to load compiled data files from. If null,
093: * use system ClassLoader.
094: * @throws IOException if directory or map file cannot be read
095: */
096: public ZoneInfoProvider(String resourcePath, ClassLoader loader)
097: throws IOException {
098: this (resourcePath, loader, true);
099: }
100:
101: /**
102: * @param favorSystemLoader when true, use the system class loader if
103: * loader null. When false, use the current class loader if loader is null.
104: */
105: private ZoneInfoProvider(String resourcePath, ClassLoader loader,
106: boolean favorSystemLoader) throws IOException {
107: if (resourcePath == null) {
108: throw new IllegalArgumentException(
109: "No resource path provided");
110: }
111: if (!resourcePath.endsWith("/")) {
112: resourcePath += '/';
113: }
114:
115: iFileDir = null;
116: iResourcePath = resourcePath;
117:
118: if (loader == null && !favorSystemLoader) {
119: loader = getClass().getClassLoader();
120: }
121:
122: iLoader = loader;
123:
124: iZoneInfoMap = loadZoneInfoMap(openResource("ZoneInfoMap"));
125: }
126:
127: //-----------------------------------------------------------------------
128: /**
129: * If an error is thrown while loading zone data, uncaughtException is
130: * called to log the error and null is returned for this and all future
131: * requests.
132: *
133: * @param id the id to load
134: * @return the loaded zone
135: */
136: public synchronized DateTimeZone getZone(String id) {
137: if (id == null) {
138: return null;
139: }
140:
141: Object obj = iZoneInfoMap.get(id);
142: if (obj == null) {
143: return null;
144: }
145:
146: if (id.equals(obj)) {
147: // Load zone data for the first time.
148: return loadZoneData(id);
149: }
150:
151: if (obj instanceof SoftReference) {
152: DateTimeZone tz = (DateTimeZone) ((SoftReference) obj)
153: .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 synchronized Set 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(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: StringBuffer buf = new StringBuffer(40).append(
207: "Resource not found: \"").append(path).append(
208: "\" ClassLoader: ")
209: .append(
210: iLoader != null ? iLoader.toString()
211: : "system");
212: throw new IOException(buf.toString());
213: }
214: }
215: return in;
216: }
217:
218: /**
219: * Loads the time zone data for one id.
220: *
221: * @param id the id to load
222: * @return the zone
223: */
224: private DateTimeZone loadZoneData(String id) {
225: InputStream in = null;
226: try {
227: in = openResource(id);
228: DateTimeZone tz = DateTimeZoneBuilder.readFrom(in, id);
229: iZoneInfoMap.put(id, new SoftReference(tz));
230: return tz;
231: } catch (IOException e) {
232: uncaughtException(e);
233: iZoneInfoMap.remove(id);
234: return null;
235: } finally {
236: try {
237: if (in != null) {
238: in.close();
239: }
240: } catch (IOException e) {
241: }
242: }
243: }
244:
245: //-----------------------------------------------------------------------
246: /**
247: * Loads the zone info map.
248: *
249: * @param in the input stream
250: * @return the map
251: */
252: private static Map loadZoneInfoMap(InputStream in)
253: throws IOException {
254: Map map = new TreeMap(String.CASE_INSENSITIVE_ORDER);
255: DataInputStream din = new DataInputStream(in);
256: try {
257: readZoneInfoMap(din, map);
258: } finally {
259: try {
260: din.close();
261: } catch (IOException e) {
262: }
263: }
264: map.put("UTC", new SoftReference(DateTimeZone.UTC));
265: return map;
266: }
267:
268: /**
269: * Reads the zone info map from file.
270: *
271: * @param din the input stream
272: * @param zimap gets filled with string id to string id mappings
273: */
274: private static void readZoneInfoMap(DataInputStream din, Map zimap)
275: throws IOException {
276: // Read the string pool.
277: int size = din.readUnsignedShort();
278: String[] pool = new String[size];
279: for (int i = 0; i < size; i++) {
280: pool[i] = din.readUTF().intern();
281: }
282:
283: // Read the mappings.
284: size = din.readUnsignedShort();
285: for (int i = 0; i < size; i++) {
286: try {
287: zimap.put(pool[din.readUnsignedShort()], pool[din
288: .readUnsignedShort()]);
289: } catch (ArrayIndexOutOfBoundsException e) {
290: throw new IOException("Corrupt zone info map");
291: }
292: }
293: }
294:
295: }
|