001    /*
002     *  Copyright 2001-2005 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.TreeMap;
027    import java.util.TreeSet;
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 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 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    }