001/*
002 *  Copyright 2001-2012 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 final Info[] iInfoCache = new Info[cInfoCacheMask + 1];
085
086    private CachedDateTimeZone(DateTimeZone zone) {
087        super(zone.getID());
088        iZone = zone;
089    }
090
091    /**
092     * Returns the DateTimeZone being wrapped.
093     */
094    public DateTimeZone getUncachedZone() {
095        return iZone;
096    }
097
098    public String getNameKey(long instant) {
099        return getInfo(instant).getNameKey(instant);
100    }
101
102    public int getOffset(long instant) {
103        return getInfo(instant).getOffset(instant);
104    }
105
106    public int getStandardOffset(long instant) {
107        return getInfo(instant).getStandardOffset(instant);
108    }
109
110    public boolean isFixed() {
111        return iZone.isFixed();
112    }
113
114    public long nextTransition(long instant) {
115        return iZone.nextTransition(instant);
116    }
117
118    public long previousTransition(long instant) {
119        return iZone.previousTransition(instant);
120    }
121
122    public int hashCode() {
123        return iZone.hashCode();
124    }
125
126    public boolean equals(Object obj) {
127        if (this == obj) {
128            return true;
129        }
130        if (obj instanceof CachedDateTimeZone) {
131            return iZone.equals(((CachedDateTimeZone)obj).iZone);
132        }
133        return false;
134    }
135
136    // Although accessed by multiple threads, this method doesn't need to be
137    // synchronized.
138
139    private Info getInfo(long millis) {
140        int period = (int)(millis >> 32);
141        Info[] cache = iInfoCache;
142        int index = period & cInfoCacheMask;
143        Info info = cache[index];
144        if (info == null || (int)((info.iPeriodStart >> 32)) != period) {
145            info = createInfo(millis);
146            cache[index] = info;
147        }
148        return info;
149    }
150
151    private Info createInfo(long millis) {
152        long periodStart = millis & (0xffffffffL << 32);
153        Info info = new Info(iZone, periodStart);
154        
155        long end = periodStart | 0xffffffffL;
156        Info chain = info;
157        while (true) {
158            long next = iZone.nextTransition(periodStart);
159            if (next == periodStart || next > end) {
160                break;
161            }
162            periodStart = next;
163            chain = (chain.iNextInfo = new Info(iZone, periodStart));
164        }
165
166        return info;
167    }
168
169    private final static class Info {
170        // For first Info in chain, iPeriodStart's lower 32 bits are clear.
171        public final long iPeriodStart;
172        public final DateTimeZone iZoneRef;
173
174        Info iNextInfo;
175
176        private String iNameKey;
177        private int iOffset = Integer.MIN_VALUE;
178        private int iStandardOffset = Integer.MIN_VALUE;
179
180        Info(DateTimeZone zone, long periodStart) {
181            iPeriodStart = periodStart;
182            iZoneRef = zone;
183        }
184
185        public String getNameKey(long millis) {
186            if (iNextInfo == null || millis < iNextInfo.iPeriodStart) {
187                if (iNameKey == null) {
188                    iNameKey = iZoneRef.getNameKey(iPeriodStart);
189                }
190                return iNameKey;
191            }
192            return iNextInfo.getNameKey(millis);
193        }
194
195        public int getOffset(long millis) {
196            if (iNextInfo == null || millis < iNextInfo.iPeriodStart) {
197                if (iOffset == Integer.MIN_VALUE) {
198                    iOffset = iZoneRef.getOffset(iPeriodStart);
199                }
200                return iOffset;
201            }
202            return iNextInfo.getOffset(millis);
203        }
204
205        public int getStandardOffset(long millis) {
206            if (iNextInfo == null || millis < iNextInfo.iPeriodStart) {
207                if (iStandardOffset == Integer.MIN_VALUE) {
208                    iStandardOffset = iZoneRef.getStandardOffset(iPeriodStart);
209                }
210                return iStandardOffset;
211            }
212            return iNextInfo.getStandardOffset(millis);
213        }
214    }
215}