001 /*
002 * Copyright 2001-2009 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.TreeSet;
027 import java.util.concurrent.ConcurrentHashMap;
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<String, Object> 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 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 }