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    }