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 */
016package org.joda.time.tz;
017
018import java.io.DataInputStream;
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.lang.ref.SoftReference;
024import java.util.Map;
025import java.util.Set;
026import java.util.TreeSet;
027import java.util.concurrent.ConcurrentHashMap;
028
029import 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 */
040public 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}