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 */
016package org.joda.time.tz;
017
018import org.joda.time.DateTimeZone;
019
020/**
021 * Improves the performance of requesting time zone offsets and name keys by
022 * caching the results. Time zones that have simple rules or are fixed should
023 * not be cached, as it is unlikely to improve performance.
024 * <p>
025 * CachedDateTimeZone is thread-safe and immutable.
026 * 
027 * @author Brian S O'Neill
028 * @since 1.0
029 */
030public class CachedDateTimeZone extends DateTimeZone {
031
032    private static final long serialVersionUID = 5472298452022250685L;
033
034    private static final int cInfoCacheMask;
035
036    static {
037        Integer i;
038        try {
039            i = Integer.getInteger("org.joda.time.tz.CachedDateTimeZone.size");
040        } catch (SecurityException e) {
041            i = null;
042        }
043
044        int cacheSize;
045        if (i == null) {
046            // With a cache size of 512, dates that lie within any 69.7 year
047            // period have no cache collisions.
048            cacheSize = 512; // (1 << 9)
049        } else {
050            cacheSize = i.intValue();
051            // Ensure cache size is even power of 2.
052            cacheSize--;
053            int shift = 0;
054            while (cacheSize > 0) {
055                shift++;
056                cacheSize >>= 1;
057            }
058            cacheSize = 1 << shift;
059        }
060
061        cInfoCacheMask = cacheSize - 1;
062    }
063
064    /**
065     * Returns a new CachedDateTimeZone unless given zone is already cached.
066     */
067    public static CachedDateTimeZone forZone(DateTimeZone zone) {
068        if (zone instanceof CachedDateTimeZone) {
069            return (CachedDateTimeZone)zone;
070        }
071        return new CachedDateTimeZone(zone);
072    }
073
074    /*
075     * Caching is performed by breaking timeline down into periods of 2^32
076     * milliseconds, or about 49.7 days. A year has about 7.3 periods, usually
077     * with only 2 time zone offset periods. Most of the 49.7 day periods will
078     * have no transition, about one quarter have one transition, and very rare
079     * cases have multiple transitions.
080     */
081
082    private final DateTimeZone iZone;
083
084    private transient Info[] iInfoCache;
085
086    private CachedDateTimeZone(DateTimeZone zone) {
087        super(zone.getID());
088        iZone = zone;
089        iInfoCache = new Info[cInfoCacheMask + 1];
090    }
091
092    private void readObject(java.io.ObjectInputStream in)
093        throws java.io.IOException, ClassNotFoundException
094    {
095        in.defaultReadObject();
096        iInfoCache = new Info[cInfoCacheMask + 1];
097    }
098
099    /**
100     * Returns the DateTimeZone being wrapped.
101     */
102    public DateTimeZone getUncachedZone() {
103        return iZone;
104    }
105
106    public String getNameKey(long instant) {
107        return getInfo(instant).getNameKey(instant);
108    }
109
110    public int getOffset(long instant) {
111        return getInfo(instant).getOffset(instant);
112    }
113
114    public int getStandardOffset(long instant) {
115        return getInfo(instant).getStandardOffset(instant);
116    }
117
118    public boolean isFixed() {
119        return iZone.isFixed();
120    }
121
122    public long nextTransition(long instant) {
123        return iZone.nextTransition(instant);
124    }
125
126    public long previousTransition(long instant) {
127        return iZone.previousTransition(instant);
128    }
129
130    public int hashCode() {
131        return iZone.hashCode();
132    }
133
134    public boolean equals(Object obj) {
135        if (this == obj) {
136            return true;
137        }
138        if (obj instanceof CachedDateTimeZone) {
139            return iZone.equals(((CachedDateTimeZone)obj).iZone);
140        }
141        return false;
142    }
143
144    // Although accessed by multiple threads, this method doesn't need to be
145    // synchronized.
146
147    private Info getInfo(long millis) {
148        int period = (int)(millis >> 32);
149        Info[] cache = iInfoCache;
150        int index = period & cInfoCacheMask;
151        Info info = cache[index];
152        if (info == null || (int)((info.iPeriodStart >> 32)) != period) {
153            info = createInfo(millis);
154            cache[index] = info;
155        }
156        return info;
157    }
158
159    private Info createInfo(long millis) {
160        long periodStart = millis & (0xffffffffL << 32);
161        Info info = new Info(iZone, periodStart);
162        
163        long end = periodStart | 0xffffffffL;
164        Info chain = info;
165        while (true) {
166            long next = iZone.nextTransition(periodStart);
167            if (next == periodStart || next > end) {
168                break;
169            }
170            periodStart = next;
171            chain = (chain.iNextInfo = new Info(iZone, periodStart));
172        }
173
174        return info;
175    }
176
177    private final static class Info {
178        // For first Info in chain, iPeriodStart's lower 32 bits are clear.
179        public final long iPeriodStart;
180        public final DateTimeZone iZoneRef;
181
182        Info iNextInfo;
183
184        private String iNameKey;
185        private int iOffset = Integer.MIN_VALUE;
186        private int iStandardOffset = Integer.MIN_VALUE;
187
188        Info(DateTimeZone zone, long periodStart) {
189            iPeriodStart = periodStart;
190            iZoneRef = zone;
191        }
192
193        public String getNameKey(long millis) {
194            if (iNextInfo == null || millis < iNextInfo.iPeriodStart) {
195                if (iNameKey == null) {
196                    iNameKey = iZoneRef.getNameKey(iPeriodStart);
197                }
198                return iNameKey;
199            }
200            return iNextInfo.getNameKey(millis);
201        }
202
203        public int getOffset(long millis) {
204            if (iNextInfo == null || millis < iNextInfo.iPeriodStart) {
205                if (iOffset == Integer.MIN_VALUE) {
206                    iOffset = iZoneRef.getOffset(iPeriodStart);
207                }
208                return iOffset;
209            }
210            return iNextInfo.getOffset(millis);
211        }
212
213        public int getStandardOffset(long millis) {
214            if (iNextInfo == null || millis < iNextInfo.iPeriodStart) {
215                if (iStandardOffset == Integer.MIN_VALUE) {
216                    iStandardOffset = iZoneRef.getStandardOffset(iPeriodStart);
217                }
218                return iStandardOffset;
219            }
220            return iNextInfo.getStandardOffset(millis);
221        }
222    }
223}