View Javadoc

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 }