1 /*
2 * Copyright 2001-2009 Stephen Colebourne
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.joda.time.tz;
17
18 import java.io.DataInputStream;
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.lang.ref.SoftReference;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.TreeSet;
27 import java.util.concurrent.ConcurrentHashMap;
28
29 import org.joda.time.DateTimeZone;
30
31 /**
32 * ZoneInfoProvider loads compiled data files as generated by
33 * {@link ZoneInfoCompiler}.
34 * <p>
35 * ZoneInfoProvider is thread-safe and publicly immutable.
36 *
37 * @author Brian S O'Neill
38 * @since 1.0
39 */
40 public class ZoneInfoProvider implements Provider {
41
42 /** The directory where the files are held. */
43 private final File iFileDir;
44 /** The resource path. */
45 private final String iResourcePath;
46 /** The class loader to use. */
47 private final ClassLoader iLoader;
48 /** Maps ids to strings or SoftReferences to DateTimeZones. */
49 private final Map<String, Object> iZoneInfoMap;
50
51 /**
52 * ZoneInfoProvider searches the given directory for compiled data files.
53 *
54 * @throws IOException if directory or map file cannot be read
55 */
56 public ZoneInfoProvider(File fileDir) throws IOException {
57 if (fileDir == null) {
58 throw new IllegalArgumentException("No file directory provided");
59 }
60 if (!fileDir.exists()) {
61 throw new IOException("File directory doesn't exist: " + fileDir);
62 }
63 if (!fileDir.isDirectory()) {
64 throw new IOException("File doesn't refer to a directory: " + fileDir);
65 }
66
67 iFileDir = fileDir;
68 iResourcePath = null;
69 iLoader = null;
70
71 iZoneInfoMap = loadZoneInfoMap(openResource("ZoneInfoMap"));
72 }
73
74 /**
75 * ZoneInfoProvider searches the given ClassLoader resource path for
76 * compiled data files. Resources are loaded from the ClassLoader that
77 * loaded this class.
78 *
79 * @throws IOException if directory or map file cannot be read
80 */
81 public ZoneInfoProvider(String resourcePath) throws IOException {
82 this(resourcePath, null, false);
83 }
84
85 /**
86 * ZoneInfoProvider searches the given ClassLoader resource path for
87 * compiled data files.
88 *
89 * @param loader ClassLoader to load compiled data files from. If null,
90 * use system ClassLoader.
91 * @throws IOException if directory or map file cannot be read
92 */
93 public ZoneInfoProvider(String resourcePath, ClassLoader loader)
94 throws IOException
95 {
96 this(resourcePath, loader, true);
97 }
98
99 /**
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 }