1 | /* |
2 | * Copyright 2001-2005 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.TreeMap; |
27 | import java.util.TreeSet; |
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 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 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 | } |