1/*
2 * Copyright (C) 2007 The Android Open Source Project
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
17package org.apache.harmony.luni.internal.util;
18
19import java.io.File;
20import java.io.FileInputStream;
21import java.io.IOException;
22import java.io.RandomAccessFile;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Date;
26import java.util.List;
27import java.util.TimeZone;
28import java.util.logging.Logger;
29
30/**
31 * A class used to initialize the time zone database.  This implementation uses the
32 * 'zoneinfo' database as the source of time zone information.  However, to conserve
33 * disk space the data for all time zones are concatenated into a single file, and a
34 * second file is used to indicate the starting position of each time zone record.  A
35 * third file indicates the version of the zoneinfo databse used to generate the data.
36 *
37 * {@hide}
38 */
39public final class ZoneInfoDB {
40    private static final int TZNAME_LENGTH = 40;
41    private static final int TZINT_LENGTH = 4;
42
43    /**
44     * The directory contining the time zone database files.
45     */
46    private static final String ZONE_DIRECTORY_NAME =
47        System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/";
48
49    /**
50     * The name of the file containing the concatenated time zone records.
51     */
52    private static final String ZONE_FILE_NAME =
53        ZONE_DIRECTORY_NAME + "zoneinfo.dat";
54
55    /**
56     * The name of the file containing the index to each time zone record within
57     * the zoneinfo.dat file.
58     */
59    private static final String INDEX_FILE_NAME =
60        ZONE_DIRECTORY_NAME + "zoneinfo.idx";
61
62    /**
63     *  Zoneinfo version used prior to creation of the zoneinfo.version file,
64     *  equal to "2007h".
65     */
66    private static final String DEFAULT_VERSION = "2007h";
67
68    /**
69     * The name of the file indicating the database version in use.  If the file is not
70     * present or is unreadable, we assume a version of DEFAULT_VERSION.
71     */
72    private static final String VERSION_FILE_NAME =
73        ZONE_DIRECTORY_NAME + "zoneinfo.version";
74
75    private static Object lock = new Object();
76    private static TimeZone defaultZone = null;
77
78    private static String version;
79    private static String[] names;
80    private static int[] starts;
81    private static int[] lengths;
82    private static int[] offsets;
83
84    /**
85     * This class is uninstantiable.
86     */
87    private ZoneInfoDB() {
88        // This space intentionally left blank.
89    }
90
91    private static void readVersion() throws IOException {
92        RandomAccessFile versionFile = new RandomAccessFile(VERSION_FILE_NAME, "r");
93        int len = (int) versionFile.length();
94        byte[] vbuf = new byte[len];
95        versionFile.readFully(vbuf);
96        version = new String(vbuf, 0, len, "ISO-8859-1").trim();
97        versionFile.close();
98    }
99
100    private static void readDatabase() throws IOException {
101        if (starts != null) {
102            return;
103        }
104
105        RandomAccessFile indexFile = new RandomAccessFile(INDEX_FILE_NAME, "r");
106        byte[] nbuf = new byte[TZNAME_LENGTH];
107
108        int numEntries =
109            (int) (indexFile.length() / (TZNAME_LENGTH + 3*TZINT_LENGTH));
110
111        char[] namebuf = new char[numEntries * TZNAME_LENGTH];
112        int[] nameend = new int[numEntries];
113        int nameoff = 0;
114
115        starts = new int[numEntries];
116        lengths = new int[numEntries];
117        offsets = new int[numEntries];
118
119        for (int i = 0; i < numEntries; i++) {
120            indexFile.readFully(nbuf);
121            starts[i] = indexFile.readInt();
122            lengths[i] = indexFile.readInt();
123            offsets[i] = indexFile.readInt();
124
125            // Don't include null chars in the String
126            int len = nbuf.length;
127            for (int j = 0; j < len; j++) {
128                if (nbuf[j] == 0) {
129                    break;
130                }
131                namebuf[nameoff++] = (char) (nbuf[j] & 0xFF);
132            }
133
134            nameend[i] = nameoff;
135        }
136
137        String name = new String(namebuf, 0, nameoff);
138
139        // Assumes the namebuf is all ASCII (so byte offsets == char offsets).
140        names = new String[numEntries];
141        for (int i = 0; i < numEntries; i++) {
142            names[i] = name.substring(i == 0 ? 0 : nameend[i - 1],
143                                      nameend[i]);
144        }
145
146        indexFile.close();
147    }
148
149    static {
150        // Don't attempt to log here because the logger requires this class to be initialized
151        try {
152            readVersion();
153        } catch (IOException e) {
154            // The version can't be read, we can continue without it
155            version = DEFAULT_VERSION;
156        }
157
158        try {
159            readDatabase();
160        } catch (IOException e) {
161            // The database can't be read, try to continue without it
162            names = new String[0];
163            starts = new int[0];
164            lengths = new int[0];
165            offsets = new int[0];
166        }
167    }
168
169    public static String getVersion() {
170        return version;
171    }
172
173    public static String[] getAvailableIDs() {
174        return _getAvailableIDs(0, false);
175    }
176
177    public static String[] getAvailableIDs(int rawOffset) {
178        return _getAvailableIDs(rawOffset, true);
179    }
180
181    private static String[] _getAvailableIDs(int rawOffset,
182            boolean checkOffset) {
183        List<String> matches = new ArrayList<String>();
184
185        int[] _offsets = ZoneInfoDB.offsets;
186        String[] _names = ZoneInfoDB.names;
187        int len = _offsets.length;
188        for (int i = 0; i < len; i++) {
189            if (!checkOffset || _offsets[i] == rawOffset) {
190                matches.add(_names[i]);
191            }
192        }
193
194        return matches.toArray(new String[matches.size()]);
195    }
196
197    /*package*/ static TimeZone _getTimeZone(String name)
198            throws IOException {
199        FileInputStream fis = null;
200        int length = 0;
201
202        File f = new File(ZONE_DIRECTORY_NAME + name);
203        if (!f.exists()) {
204            fis = new FileInputStream(ZONE_FILE_NAME);
205            int i = Arrays.binarySearch(ZoneInfoDB.names, name);
206
207            if (i < 0) {
208                return null;
209            }
210
211            int start = ZoneInfoDB.starts[i];
212            length = ZoneInfoDB.lengths[i];
213
214            fis.skip(start);
215        }
216
217        if (fis == null) {
218            fis = new FileInputStream(f);
219            length = (int)f.length(); // data won't exceed 2G!
220        }
221
222        byte[] data = new byte[length];
223        int nread = 0;
224        while (nread < length) {
225            int size = fis.read(data, nread, length - nread);
226            if (size > 0) {
227                nread += size;
228            }
229        }
230
231        try {
232            fis.close();
233        } catch (IOException e3) {
234            // probably better to continue than to fail here
235            java.util.logging.Logger.global.warning("IOException " + e3 +
236                " retrieving time zone data");
237            e3.printStackTrace();
238        }
239
240        if (data.length < 36) {
241            return null;
242        }
243        if (data[0] != 'T' || data[1] != 'Z' ||
244            data[2] != 'i' || data[3] != 'f') {
245            return null;
246        }
247
248        int ntransition = read4(data, 32);
249        int ngmtoff = read4(data, 36);
250        int base = 44;
251
252        int[] transitions = new int[ntransition];
253        for (int i = 0; i < ntransition; i++)
254            transitions[i] = read4(data, base + 4 * i);
255        base += 4 * ntransition;
256
257        byte[] type = new byte[ntransition];
258        for (int i = 0; i < ntransition; i++)
259            type[i] = data[base + i];
260        base += ntransition;
261
262        int[] gmtoff = new int[ngmtoff];
263        byte[] isdst = new byte[ngmtoff];
264        byte[] abbrev = new byte[ngmtoff];
265        for (int i = 0; i < ngmtoff; i++) {
266            gmtoff[i] = read4(data, base + 6 * i);
267            isdst[i] = data[base + 6 * i + 4];
268            abbrev[i] = data[base + 6 * i + 5];
269        }
270
271        base += 6 * ngmtoff;
272
273        return new ZoneInfo(name, transitions, type, gmtoff, isdst, abbrev, data, base);
274    }
275
276    private static int read4(byte[] data, int off) {
277        return ((data[off    ] & 0xFF) << 24) |
278               ((data[off + 1] & 0xFF) << 16) |
279               ((data[off + 2] & 0xFF) <<  8) |
280               ((data[off + 3] & 0xFF) <<  0);
281    }
282
283    public static TimeZone getTimeZone(String id) {
284        if (id != null) {
285            if (id.equals("GMT") || id.equals("UTC")) {
286                TimeZone tz = new MinimalTimeZone(0);
287                tz.setID(id);
288                return tz;
289            }
290
291            if (id.startsWith("GMT")) {
292                return new MinimalTimeZone(parseNumericZone(id) * 1000);
293            }
294        }
295
296        TimeZone tz = ZoneInfo.getTimeZone(id);
297
298        if (tz != null)
299            return tz;
300
301        /*
302         * It isn't GMT+anything, and it also isn't something we have
303         * in the database.  Give up and return GMT.
304         */
305        tz = new MinimalTimeZone(0);
306        tz.setID("GMT");
307        return tz;
308    }
309
310    public static TimeZone getDefault() {
311        TimeZone zone;
312
313        synchronized (lock) {
314            if (defaultZone != null) {
315                return defaultZone;
316            }
317
318            String zoneName = null;
319            TimezoneGetter tzGetter = TimezoneGetter.getInstance();
320            if (tzGetter != null) {
321                zoneName = tzGetter.getId();
322            }
323            if (zoneName != null && zoneName.length() > 0) {
324                zone = TimeZone.getTimeZone(zoneName.trim());
325            } else {
326                // use localtime here so that the simulator works
327                zone = TimeZone.getTimeZone("localtime");
328            }
329
330            defaultZone = zone;
331        }
332        return zone;
333    }
334
335    // TODO - why does this ignore the 'zone' parameter?
336    public static void setDefault(@SuppressWarnings("unused") TimeZone zone) {
337        /*
338         * if (zone == null), the next call to getDefault will set it to the
339         * the system's default time zone.
340         */
341        synchronized (lock) {
342            defaultZone = null;
343        }
344    }
345
346    private static int parseNumericZone(String name) {
347        if (name == null)
348            return 0;
349
350        if (!name.startsWith("GMT"))
351            return 0;
352
353        if (name.length() == 3)
354            return 0;
355
356        int sign;
357        if (name.charAt(3) == '+')
358            sign = 1;
359        else if (name.charAt(3) == '-')
360            sign = -1;
361        else
362            return 0;
363
364        int where;
365        int hour = 0;
366        boolean colon = false;
367        for (where = 4; where < name.length(); where++) {
368            char c = name.charAt(where);
369
370            if (c == ':') {
371                where++;
372                colon = true;
373                break;
374            }
375
376            if (c >= '0' && c <= '9')
377                hour = hour * 10 + c - '0';
378            else
379                return 0;
380        }
381
382        int min = 0;
383        for (; where < name.length(); where++) {
384            char c = name.charAt(where);
385
386            if (c >= '0' && c <= '9')
387                min = min * 10 + c - '0';
388            else
389                return 0;
390        }
391
392        if (colon)
393            return sign * (hour * 60 + min) * 60;
394        else if (hour >= 100)
395            return sign * ((hour / 100) * 60 + (hour % 100)) * 60;
396        else
397            return sign * (hour * 60) * 60;
398    }
399
400    /*package*/ static class MinimalTimeZone extends TimeZone {
401        private int rawOffset;
402
403        public MinimalTimeZone(int offset) {
404            rawOffset = offset;
405            setID(getDisplayName());
406        }
407
408        @SuppressWarnings("unused")
409        @Override
410        public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) {
411            return getRawOffset();
412        }
413
414        @Override
415        public int getRawOffset() {
416            return rawOffset;
417        }
418
419        @Override
420        public void setRawOffset(int off) {
421            rawOffset = off;
422        }
423
424        @SuppressWarnings("unused")
425        @Override
426        public boolean inDaylightTime(Date when) {
427            return false;
428        }
429
430        @Override
431        public boolean useDaylightTime() {
432            return false;
433        }
434    }
435}
436