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, nullfalse);
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