1 /*
2 * Copyright 2001-2013 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.Collections;
25 import java.util.Map;
26 import java.util.Set;
27 import java.util.TreeSet;
28 import java.util.concurrent.ConcurrentHashMap;
29
30 import org.joda.time.DateTimeZone;
31
32 /**
33 * ZoneInfoProvider loads compiled data files as generated by
34 * {@link ZoneInfoCompiler}.
35 * <p>
36 * ZoneInfoProvider is thread-safe and publicly immutable.
37 *
38 * @author Brian S O'Neill
39 * @since 1.0
40 */
41 public class ZoneInfoProvider implements Provider {
42
43 /** The directory where the files are held. */
44 private final File iFileDir;
45 /** The resource path. */
46 private final String iResourcePath;
47 /** The class loader to use. */
48 private final ClassLoader iLoader;
49 /** Maps ids to strings or SoftReferences to DateTimeZones. */
50 private final Map<String, Object> iZoneInfoMap;
51 /** Maps ids to strings or SoftReferences to DateTimeZones. */
52 private final Set<String> iZoneInfoKeys;
53
54 /**
55 * ZoneInfoProvider searches the given directory for compiled data files.
56 *
57 * @throws IOException if directory or map file cannot be read
58 */
59 public ZoneInfoProvider(File fileDir) throws IOException {
60 if (fileDir == null) {
61 throw new IllegalArgumentException("No file directory provided");
62 }
63 if (!fileDir.exists()) {
64 throw new IOException("File directory doesn't exist: " + fileDir);
65 }
66 if (!fileDir.isDirectory()) {
67 throw new IOException("File doesn't refer to a directory: " + fileDir);
68 }
69
70 iFileDir = fileDir;
71 iResourcePath = null;
72 iLoader = null;
73
74 iZoneInfoMap = loadZoneInfoMap(openResource("ZoneInfoMap"));
75 iZoneInfoKeys = Collections.unmodifiableSortedSet(new TreeSet<String>(iZoneInfoMap.keySet()));
76 }
77
78 /**
79 * ZoneInfoProvider searches the given ClassLoader resource path for
80 * compiled data files. Resources are loaded from the ClassLoader that
81 * loaded this class.
82 *
83 * @throws IOException if directory or map file cannot be read
84 */
85 public ZoneInfoProvider(String resourcePath) throws IOException {
86 this(resourcePath, null, false);
87 }
88
89 /**
90 * ZoneInfoProvider searches the given ClassLoader resource path for
91 * compiled data files.
92 *
93 * @param loader ClassLoader to load compiled data files from. If null,
94 * use system ClassLoader.
95 * @throws IOException if directory or map file cannot be read
96 */
97 public ZoneInfoProvider(String resourcePath, ClassLoader loader)
98 throws IOException
99 {
100 this(resourcePath, loader, true);
101 }
102
103 /**
104 * @param favorSystemLoader when true, use the system class loader if
105 * loader null. When false, use the current class loader if loader is null.
106 */
107 private ZoneInfoProvider(String resourcePath,
108 ClassLoader loader, boolean favorSystemLoader)
109 throws IOException
110 {
111 if (resourcePath == null) {
112 throw new IllegalArgumentException("No resource path provided");
113 }
114 if (!resourcePath.endsWith("/")) {
115 resourcePath += '/';
116 }
117
118 iFileDir = null;
119 iResourcePath = resourcePath;
120
121 if (loader == null && !favorSystemLoader) {
122 loader = getClass().getClassLoader();
123 }
124
125 iLoader = loader;
126
127 iZoneInfoMap = loadZoneInfoMap(openResource("ZoneInfoMap"));
128 iZoneInfoKeys = Collections.unmodifiableSortedSet(new TreeSet<String>(iZoneInfoMap.keySet()));
129 }
130
131 //-----------------------------------------------------------------------
132 /**
133 * If an error is thrown while loading zone data, the exception is logged
134 * to system error and null is returned for this and all future requests.
135 *
136 * @param id the id to load
137 * @return the loaded zone
138 */
139 public DateTimeZone getZone(String id) {
140 if (id == null) {
141 return null;
142 }
143
144 Object obj = iZoneInfoMap.get(id);
145 if (obj == null) {
146 return null;
147 }
148
149 if (obj instanceof SoftReference<?>) {
150 @SuppressWarnings("unchecked")
151 SoftReference<DateTimeZone> ref = (SoftReference<DateTimeZone>) obj;
152 DateTimeZone tz = ref.get();
153 if (tz != null) {
154 return tz;
155 }
156 // Reference cleared; load data again.
157 return loadZoneData(id);
158 } else if (id.equals(obj)) {
159 // Load zone data for the first time.
160 return loadZoneData(id);
161 }
162
163 // If this point is reached, mapping must link to another.
164 return getZone((String)obj);
165 }
166
167 /**
168 * Gets a list of all the available zone ids.
169 *
170 * @return the zone ids
171 */
172 public Set<String> getAvailableIDs() {
173 return iZoneInfoKeys;
174 }
175
176 /**
177 * Called if an exception is thrown from getZone while loading zone data.
178 *
179 * @param ex the exception
180 */
181 protected void uncaughtException(Exception ex) {
182 ex.printStackTrace();
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 @SuppressWarnings("resource")
193 private InputStream openResource(String name) throws IOException {
194 InputStream in;
195 if (iFileDir != null) {
196 in = new FileInputStream(new File(iFileDir, name));
197 } else {
198 String path = iResourcePath.concat(name);
199 if (iLoader != null) {
200 in = iLoader.getResourceAsStream(path);
201 } else {
202 in = ClassLoader.getSystemResourceAsStream(path);
203 }
204 if (in == null) {
205 StringBuilder buf = new StringBuilder(40)
206 .append("Resource not found: \"")
207 .append(path)
208 .append("\" ClassLoader: ")
209 .append(iLoader != null ? iLoader.toString() : "system");
210 throw new IOException(buf.toString());
211 }
212 }
213 return in;
214 }
215
216 /**
217 * Loads the time zone data for one id.
218 *
219 * @param id the id to load
220 * @return the zone
221 */
222 private DateTimeZone loadZoneData(String id) {
223 InputStream in = null;
224 try {
225 in = openResource(id);
226 DateTimeZone tz = DateTimeZoneBuilder.readFrom(in, id);
227 iZoneInfoMap.put(id, new SoftReference<DateTimeZone>(tz));
228 return tz;
229 } catch (IOException ex) {
230 uncaughtException(ex);
231 iZoneInfoMap.remove(id);
232 return null;
233 } finally {
234 try {
235 if (in != null) {
236 in.close();
237 }
238 } catch (IOException ex) {
239 }
240 }
241 }
242
243 //-----------------------------------------------------------------------
244 /**
245 * Loads the zone info map.
246 *
247 * @param in the input stream
248 * @return the map
249 */
250 private static Map<String, Object> loadZoneInfoMap(InputStream in) throws IOException {
251 Map<String, Object> map = new ConcurrentHashMap<String, Object>();
252 DataInputStream din = new DataInputStream(in);
253 try {
254 readZoneInfoMap(din, map);
255 } finally {
256 try {
257 din.close();
258 } catch (IOException ex) {
259 }
260 }
261 map.put("UTC", new SoftReference<DateTimeZone>(DateTimeZone.UTC));
262 return map;
263 }
264
265 /**
266 * Reads the zone info map from file.
267 *
268 * @param din the input stream
269 * @param zimap gets filled with string id to string id mappings
270 */
271 private static void readZoneInfoMap(DataInputStream din, Map<String, Object> zimap) throws IOException {
272 // Read the string pool.
273 int size = din.readUnsignedShort();
274 String[] pool = new String[size];
275 for (int i=0; i<size; i++) {
276 pool[i] = din.readUTF().intern();
277 }
278
279 // Read the mappings.
280 size = din.readUnsignedShort();
281 for (int i=0; i<size; i++) {
282 try {
283 zimap.put(pool[din.readUnsignedShort()], pool[din.readUnsignedShort()]);
284 } catch (ArrayIndexOutOfBoundsException ex) {
285 throw new IOException("Corrupt zone info map");
286 }
287 }
288 }
289
290 }
291