FileDocCategorySizeDatePackage
ZoneInfoDB.javaAPI DocAndroid 1.5 API12876Wed May 06 22:41:04 BST 2009org.apache.harmony.luni.internal.util

ZoneInfoDB.java

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.harmony.luni.internal.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.logging.Logger;

/**
 * A class used to initialize the time zone database.  This implementation uses the
 * 'zoneinfo' database as the source of time zone information.  However, to conserve
 * disk space the data for all time zones are concatenated into a single file, and a
 * second file is used to indicate the starting position of each time zone record.  A
 * third file indicates the version of the zoneinfo databse used to generate the data.
 *
 * {@hide}
 */
public final class ZoneInfoDB {
    private static final int TZNAME_LENGTH = 40;
    private static final int TZINT_LENGTH = 4;

    /**
     * The directory contining the time zone database files.
     */
    private static final String ZONE_DIRECTORY_NAME =
        System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/";

    /**
     * The name of the file containing the concatenated time zone records.
     */
    private static final String ZONE_FILE_NAME =
        ZONE_DIRECTORY_NAME + "zoneinfo.dat";

    /**
     * The name of the file containing the index to each time zone record within
     * the zoneinfo.dat file.
     */
    private static final String INDEX_FILE_NAME =
        ZONE_DIRECTORY_NAME + "zoneinfo.idx";

    /**
     *  Zoneinfo version used prior to creation of the zoneinfo.version file,
     *  equal to "2007h".
     */
    private static final String DEFAULT_VERSION = "2007h";

    /**
     * The name of the file indicating the database version in use.  If the file is not
     * present or is unreadable, we assume a version of DEFAULT_VERSION.
     */
    private static final String VERSION_FILE_NAME =
        ZONE_DIRECTORY_NAME + "zoneinfo.version";

    private static Object lock = new Object();
    private static TimeZone defaultZone = null;

    private static String version;
    private static String[] names;
    private static int[] starts;
    private static int[] lengths;
    private static int[] offsets;

    /**
     * This class is uninstantiable.
     */
    private ZoneInfoDB() {
        // This space intentionally left blank.
    }

    private static void readVersion() throws IOException {
        RandomAccessFile versionFile = new RandomAccessFile(VERSION_FILE_NAME, "r");
        int len = (int) versionFile.length();
        byte[] vbuf = new byte[len];
        versionFile.readFully(vbuf);
        version = new String(vbuf, 0, len, "ISO-8859-1").trim();
        versionFile.close();
    }

    private static void readDatabase() throws IOException {
        if (starts != null) {
            return;
        }

        RandomAccessFile indexFile = new RandomAccessFile(INDEX_FILE_NAME, "r");
        byte[] nbuf = new byte[TZNAME_LENGTH];

        int numEntries =
            (int) (indexFile.length() / (TZNAME_LENGTH + 3*TZINT_LENGTH));

        char[] namebuf = new char[numEntries * TZNAME_LENGTH];
        int[] nameend = new int[numEntries];
        int nameoff = 0;

        starts = new int[numEntries];
        lengths = new int[numEntries];
        offsets = new int[numEntries];

        for (int i = 0; i < numEntries; i++) {
            indexFile.readFully(nbuf);
            starts[i] = indexFile.readInt();
            lengths[i] = indexFile.readInt();
            offsets[i] = indexFile.readInt();

            // Don't include null chars in the String
            int len = nbuf.length;
            for (int j = 0; j < len; j++) {
                if (nbuf[j] == 0) {
                    break;
                }
                namebuf[nameoff++] = (char) (nbuf[j] & 0xFF);
            }

            nameend[i] = nameoff;
        }

        String name = new String(namebuf, 0, nameoff);

        // Assumes the namebuf is all ASCII (so byte offsets == char offsets).
        names = new String[numEntries];
        for (int i = 0; i < numEntries; i++) {
            names[i] = name.substring(i == 0 ? 0 : nameend[i - 1],
                                      nameend[i]);
        }

        indexFile.close();
    }

    static {
        // Don't attempt to log here because the logger requires this class to be initialized
        try {
            readVersion();
        } catch (IOException e) {
            // The version can't be read, we can continue without it
            version = DEFAULT_VERSION;
        }

        try {
            readDatabase();
        } catch (IOException e) {
            // The database can't be read, try to continue without it
            names = new String[0];
            starts = new int[0];
            lengths = new int[0];
            offsets = new int[0];
        }
    }

    public static String getVersion() {
        return version;
    }

    public static String[] getAvailableIDs() {
        return _getAvailableIDs(0, false);
    }

    public static String[] getAvailableIDs(int rawOffset) {
        return _getAvailableIDs(rawOffset, true);
    }

    private static String[] _getAvailableIDs(int rawOffset,
            boolean checkOffset) {
        List<String> matches = new ArrayList<String>();

        int[] _offsets = ZoneInfoDB.offsets;
        String[] _names = ZoneInfoDB.names;
        int len = _offsets.length;
        for (int i = 0; i < len; i++) {
            if (!checkOffset || _offsets[i] == rawOffset) {
                matches.add(_names[i]);
            }
        }

        return matches.toArray(new String[matches.size()]);
    }

    /*package*/ static TimeZone _getTimeZone(String name)
            throws IOException {
        FileInputStream fis = null;
        int length = 0;

        File f = new File(ZONE_DIRECTORY_NAME + name);
        if (!f.exists()) {
            fis = new FileInputStream(ZONE_FILE_NAME);
            int i = Arrays.binarySearch(ZoneInfoDB.names, name);

            if (i < 0) {
                return null;
            }

            int start = ZoneInfoDB.starts[i];
            length = ZoneInfoDB.lengths[i];

            fis.skip(start);
        }

        if (fis == null) {
            fis = new FileInputStream(f);
            length = (int)f.length(); // data won't exceed 2G!
        }

        byte[] data = new byte[length];
        int nread = 0;
        while (nread < length) {
            int size = fis.read(data, nread, length - nread);
            if (size > 0) {
                nread += size;
            }
        }

        try {
            fis.close();
        } catch (IOException e3) {
            // probably better to continue than to fail here
            java.util.logging.Logger.global.warning("IOException " + e3 +
                " retrieving time zone data");
            e3.printStackTrace();
        }

        if (data.length < 36) {
            return null;
        }
        if (data[0] != 'T' || data[1] != 'Z' ||
            data[2] != 'i' || data[3] != 'f') {
            return null;
        }

        int ntransition = read4(data, 32);
        int ngmtoff = read4(data, 36);
        int base = 44;

        int[] transitions = new int[ntransition];
        for (int i = 0; i < ntransition; i++)
            transitions[i] = read4(data, base + 4 * i);
        base += 4 * ntransition;

        byte[] type = new byte[ntransition];
        for (int i = 0; i < ntransition; i++)
            type[i] = data[base + i];
        base += ntransition;

        int[] gmtoff = new int[ngmtoff];
        byte[] isdst = new byte[ngmtoff];
        byte[] abbrev = new byte[ngmtoff];
        for (int i = 0; i < ngmtoff; i++) {
            gmtoff[i] = read4(data, base + 6 * i);
            isdst[i] = data[base + 6 * i + 4];
            abbrev[i] = data[base + 6 * i + 5];
        }

        base += 6 * ngmtoff;

        return new ZoneInfo(name, transitions, type, gmtoff, isdst, abbrev, data, base);
    }

    private static int read4(byte[] data, int off) {
        return ((data[off    ] & 0xFF) << 24) |
               ((data[off + 1] & 0xFF) << 16) |
               ((data[off + 2] & 0xFF) <<  8) |
               ((data[off + 3] & 0xFF) <<  0);
    }

    public static TimeZone getTimeZone(String id) {
        if (id != null) {
            if (id.equals("GMT") || id.equals("UTC")) {
                TimeZone tz = new MinimalTimeZone(0);
                tz.setID(id);
                return tz;
            }

            if (id.startsWith("GMT")) {
                return new MinimalTimeZone(parseNumericZone(id) * 1000);
            }
        }

        TimeZone tz = ZoneInfo.getTimeZone(id);

        if (tz != null)
            return tz;

        /*
         * It isn't GMT+anything, and it also isn't something we have
         * in the database.  Give up and return GMT.
         */
        tz = new MinimalTimeZone(0);
        tz.setID("GMT");
        return tz;
    }

    public static TimeZone getDefault() {
        TimeZone zone;

        synchronized (lock) {
            if (defaultZone != null) {
                return defaultZone;
            }

            String zoneName = null;
            TimezoneGetter tzGetter = TimezoneGetter.getInstance();
            if (tzGetter != null) {
                zoneName = tzGetter.getId();
            }
            if (zoneName != null && zoneName.length() > 0) {
                zone = TimeZone.getTimeZone(zoneName.trim());
            } else {
                // use localtime here so that the simulator works
                zone = TimeZone.getTimeZone("localtime");
            }

            defaultZone = zone;
        }
        return zone;
    }

    // TODO - why does this ignore the 'zone' parameter?
    public static void setDefault(@SuppressWarnings("unused") TimeZone zone) {
        /*
         * if (zone == null), the next call to getDefault will set it to the
         * the system's default time zone.
         */
        synchronized (lock) {
            defaultZone = null;
        }
    }

    private static int parseNumericZone(String name) {
        if (name == null)
            return 0;

        if (!name.startsWith("GMT"))
            return 0;

        if (name.length() == 3)
            return 0;

        int sign;
        if (name.charAt(3) == '+')
            sign = 1;
        else if (name.charAt(3) == '-')
            sign = -1;
        else
            return 0;

        int where;
        int hour = 0;
        boolean colon = false;
        for (where = 4; where < name.length(); where++) {
            char c = name.charAt(where);

            if (c == ':') {
                where++;
                colon = true;
                break;
            }

            if (c >= '0' && c <= '9')
                hour = hour * 10 + c - '0';
            else
                return 0;
        }

        int min = 0;
        for (; where < name.length(); where++) {
            char c = name.charAt(where);

            if (c >= '0' && c <= '9')
                min = min * 10 + c - '0';
            else
                return 0;
        }

        if (colon)
            return sign * (hour * 60 + min) * 60;
        else if (hour >= 100)
            return sign * ((hour / 100) * 60 + (hour % 100)) * 60;
        else
            return sign * (hour * 60) * 60;
    }

    /*package*/ static class MinimalTimeZone extends TimeZone {
        private int rawOffset;

        public MinimalTimeZone(int offset) {
            rawOffset = offset;
            setID(getDisplayName());
        }

        @SuppressWarnings("unused")
        @Override
        public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) {
            return getRawOffset();
        }

        @Override
        public int getRawOffset() {
            return rawOffset;
        }

        @Override
        public void setRawOffset(int off) {
            rawOffset = off;
        }

        @SuppressWarnings("unused")
        @Override
        public boolean inDaylightTime(Date when) {
            return false;
        }

        @Override
        public boolean useDaylightTime() {
            return false;
        }
    }
}