64 import java.io.ByteArrayInputStream; |
64 import java.io.ByteArrayInputStream; |
65 import java.io.DataInputStream; |
65 import java.io.DataInputStream; |
66 import java.io.File; |
66 import java.io.File; |
67 import java.io.IOException; |
67 import java.io.IOException; |
68 import java.io.StreamCorruptedException; |
68 import java.io.StreamCorruptedException; |
69 import java.nio.file.FileSystems; |
|
70 import java.security.AccessController; |
|
71 import java.security.PrivilegedExceptionAction; |
|
72 import java.time.DateTimeException; |
|
73 import java.util.Arrays; |
69 import java.util.Arrays; |
74 import java.util.HashSet; |
70 import java.util.HashSet; |
|
71 import java.util.List; |
|
72 import java.util.Map; |
75 import java.util.NavigableMap; |
73 import java.util.NavigableMap; |
76 import java.util.Objects; |
74 import java.util.Objects; |
77 import java.util.Set; |
75 import java.util.Set; |
78 import java.util.TreeMap; |
76 import java.util.TreeMap; |
79 import java.util.concurrent.ConcurrentNavigableMap; |
77 import java.util.concurrent.ConcurrentHashMap; |
80 import java.util.concurrent.ConcurrentSkipListMap; |
|
81 import java.util.concurrent.CopyOnWriteArraySet; |
|
82 import java.util.concurrent.atomic.AtomicReferenceArray; |
|
83 import java.util.zip.ZipFile; |
78 import java.util.zip.ZipFile; |
84 |
79 |
85 /** |
80 /** |
86 * Loads time-zone rules for 'TZDB'. |
81 * Loads time-zone rules for 'TZDB'. |
87 * <p> |
|
88 * This class is public for the service loader to access. |
|
89 * |
|
90 * <h3>Specification for implementors</h3> |
|
91 * This class is immutable and thread-safe. |
|
92 * |
82 * |
93 * @since 1.8 |
83 * @since 1.8 |
94 */ |
84 */ |
95 final class TzdbZoneRulesProvider extends ZoneRulesProvider { |
85 final class TzdbZoneRulesProvider extends ZoneRulesProvider { |
96 // service loader seems to need it to be public |
|
97 |
86 |
98 /** |
87 /** |
99 * All the regions that are available. |
88 * All the regions that are available. |
100 */ |
89 */ |
101 private final Set<String> regionIds = new CopyOnWriteArraySet<>(); |
90 private List<String> regionIds; |
102 /** |
91 /** |
103 * All the versions that are available. |
92 * Version Id of this tzdb rules |
104 */ |
93 */ |
105 private final ConcurrentNavigableMap<String, Version> versions = new ConcurrentSkipListMap<>(); |
94 private String versionId; |
|
95 /** |
|
96 * Region to rules mapping |
|
97 */ |
|
98 private final Map<String, Object> regionToRules = new ConcurrentHashMap<>(); |
106 |
99 |
107 /** |
100 /** |
108 * Creates an instance. |
101 * Creates an instance. |
109 * Created by the {@code ServiceLoader}. |
102 * Created by the {@code ServiceLoader}. |
110 * |
103 * |
111 * @throws ZoneRulesException if unable to load |
104 * @throws ZoneRulesException if unable to load |
112 */ |
105 */ |
113 public TzdbZoneRulesProvider() { |
106 public TzdbZoneRulesProvider() { |
114 super(); |
107 try { |
115 if (load(ClassLoader.getSystemClassLoader()) == false) { |
108 String libDir = System.getProperty("java.home") + File.separator + "lib"; |
116 throw new ZoneRulesException("No time-zone rules found for 'TZDB'"); |
109 File tzdbJar = new File(libDir, "tzdb.jar"); |
117 } |
110 try (ZipFile zf = new ZipFile(tzdbJar); |
118 } |
111 DataInputStream dis = new DataInputStream( |
119 |
112 zf.getInputStream(zf.getEntry("TZDB.dat")))) { |
120 //----------------------------------------------------------------------- |
113 load(dis); |
|
114 } |
|
115 } catch (Exception ex) { |
|
116 throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex); |
|
117 } |
|
118 } |
|
119 |
121 @Override |
120 @Override |
122 protected Set<String> provideZoneIds() { |
121 protected Set<String> provideZoneIds() { |
123 return new HashSet<>(regionIds); |
122 return new HashSet<>(regionIds); |
124 } |
123 } |
125 |
124 |
126 @Override |
125 @Override |
127 protected ZoneRules provideRules(String zoneId) { |
126 protected ZoneRules provideRules(String zoneId, boolean forCaching) { |
128 Objects.requireNonNull(zoneId, "zoneId"); |
127 // forCaching flag is ignored because this is not a dynamic provider |
129 ZoneRules rules = versions.lastEntry().getValue().getRules(zoneId); |
128 Object obj = regionToRules.get(zoneId); |
130 if (rules == null) { |
129 if (obj == null) { |
131 throw new ZoneRulesException("Unknown time-zone ID: " + zoneId); |
130 throw new ZoneRulesException("Unknown time-zone ID: " + zoneId); |
132 } |
131 } |
133 return rules; |
132 try { |
|
133 if (obj instanceof byte[]) { |
|
134 byte[] bytes = (byte[]) obj; |
|
135 DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes)); |
|
136 obj = Ser.read(dis); |
|
137 regionToRules.put(zoneId, obj); |
|
138 } |
|
139 return (ZoneRules) obj; |
|
140 } catch (Exception ex) { |
|
141 throw new ZoneRulesException("Invalid binary time-zone data: TZDB:" + zoneId + ", version: " + versionId, ex); |
|
142 } |
134 } |
143 } |
135 |
144 |
136 @Override |
145 @Override |
137 protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) { |
146 protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) { |
138 TreeMap<String, ZoneRules> map = new TreeMap<>(); |
147 TreeMap<String, ZoneRules> map = new TreeMap<>(); |
139 for (Version version : versions.values()) { |
148 ZoneRules rules = getRules(zoneId, false); |
140 ZoneRules rules = version.getRules(zoneId); |
149 if (rules != null) { |
141 if (rules != null) { |
150 map.put(versionId, rules); |
142 map.put(version.versionId, rules); |
|
143 } |
|
144 } |
151 } |
145 return map; |
152 return map; |
146 } |
|
147 |
|
148 //------------------------------------------------------------------------- |
|
149 /** |
|
150 * Loads the rules. |
|
151 * |
|
152 * @param classLoader the class loader to use, not null |
|
153 * @return true if updated |
|
154 * @throws ZoneRulesException if unable to load |
|
155 */ |
|
156 private boolean load(ClassLoader classLoader) { |
|
157 Object updated = Boolean.FALSE; |
|
158 try { |
|
159 updated = AccessController.doPrivileged(new PrivilegedExceptionAction() { |
|
160 public Object run() throws IOException, ClassNotFoundException { |
|
161 File tzdbJar = null; |
|
162 // TBD: workaround for now, so test/java/time tests can be |
|
163 // run against Java runtime that does not have tzdb |
|
164 String tzdbProp = System.getProperty("java.time.zone.tzdbjar"); |
|
165 if (tzdbProp != null) { |
|
166 tzdbJar = new File(tzdbProp); |
|
167 } else { |
|
168 String libDir = System.getProperty("java.home") + File.separator + "lib"; |
|
169 try { |
|
170 libDir = FileSystems.getDefault().getPath(libDir).toRealPath().toString(); |
|
171 } catch(Exception e) {} |
|
172 tzdbJar = new File(libDir, "tzdb.jar"); |
|
173 } |
|
174 try (ZipFile zf = new ZipFile(tzdbJar); |
|
175 DataInputStream dis = new DataInputStream( |
|
176 zf.getInputStream(zf.getEntry("TZDB.dat")))) { |
|
177 Iterable<Version> loadedVersions = load(dis); |
|
178 for (Version loadedVersion : loadedVersions) { |
|
179 if (versions.putIfAbsent(loadedVersion.versionId, loadedVersion) != null) { |
|
180 throw new DateTimeException( |
|
181 "Data already loaded for TZDB time-zone rules version: " + |
|
182 loadedVersion.versionId); |
|
183 } |
|
184 } |
|
185 } |
|
186 return Boolean.TRUE; |
|
187 } |
|
188 }); |
|
189 } catch (Exception ex) { |
|
190 throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex); |
|
191 } |
|
192 return updated == Boolean.TRUE; |
|
193 } |
153 } |
194 |
154 |
195 /** |
155 /** |
196 * Loads the rules from a DateInputStream, often in a jar file. |
156 * Loads the rules from a DateInputStream, often in a jar file. |
197 * |
157 * |
198 * @param dis the DateInputStream to load, not null |
158 * @param dis the DateInputStream to load, not null |
199 * @throws Exception if an error occurs |
159 * @throws Exception if an error occurs |
200 */ |
160 */ |
201 private Iterable<Version> load(DataInputStream dis) throws ClassNotFoundException, IOException { |
161 private void load(DataInputStream dis) throws Exception { |
202 if (dis.readByte() != 1) { |
162 if (dis.readByte() != 1) { |
203 throw new StreamCorruptedException("File format not recognised"); |
163 throw new StreamCorruptedException("File format not recognised"); |
204 } |
164 } |
205 // group |
165 // group |
206 String groupId = dis.readUTF(); |
166 String groupId = dis.readUTF(); |
207 if ("TZDB".equals(groupId) == false) { |
167 if ("TZDB".equals(groupId) == false) { |
208 throw new StreamCorruptedException("File format not recognised"); |
168 throw new StreamCorruptedException("File format not recognised"); |
209 } |
169 } |
210 // versions |
170 // versions |
211 int versionCount = dis.readShort(); |
171 int versionCount = dis.readShort(); |
212 String[] versionArray = new String[versionCount]; |
|
213 for (int i = 0; i < versionCount; i++) { |
172 for (int i = 0; i < versionCount; i++) { |
214 versionArray[i] = dis.readUTF(); |
173 versionId = dis.readUTF(); |
215 } |
174 } |
216 // regions |
175 // regions |
217 int regionCount = dis.readShort(); |
176 int regionCount = dis.readShort(); |
218 String[] regionArray = new String[regionCount]; |
177 String[] regionArray = new String[regionCount]; |
219 for (int i = 0; i < regionCount; i++) { |
178 for (int i = 0; i < regionCount; i++) { |
220 regionArray[i] = dis.readUTF(); |
179 regionArray[i] = dis.readUTF(); |
221 } |
180 } |
222 regionIds.addAll(Arrays.asList(regionArray)); |
181 regionIds = Arrays.asList(regionArray); |
223 // rules |
182 // rules |
224 int ruleCount = dis.readShort(); |
183 int ruleCount = dis.readShort(); |
225 Object[] ruleArray = new Object[ruleCount]; |
184 Object[] ruleArray = new Object[ruleCount]; |
226 for (int i = 0; i < ruleCount; i++) { |
185 for (int i = 0; i < ruleCount; i++) { |
227 byte[] bytes = new byte[dis.readShort()]; |
186 byte[] bytes = new byte[dis.readShort()]; |
228 dis.readFully(bytes); |
187 dis.readFully(bytes); |
229 ruleArray[i] = bytes; |
188 ruleArray[i] = bytes; |
230 } |
189 } |
231 AtomicReferenceArray<Object> ruleData = new AtomicReferenceArray<>(ruleArray); |
|
232 // link version-region-rules |
190 // link version-region-rules |
233 Set<Version> versionSet = new HashSet<Version>(versionCount); |
|
234 for (int i = 0; i < versionCount; i++) { |
191 for (int i = 0; i < versionCount; i++) { |
235 int versionRegionCount = dis.readShort(); |
192 int versionRegionCount = dis.readShort(); |
236 String[] versionRegionArray = new String[versionRegionCount]; |
193 regionToRules.clear(); |
237 short[] versionRulesArray = new short[versionRegionCount]; |
|
238 for (int j = 0; j < versionRegionCount; j++) { |
194 for (int j = 0; j < versionRegionCount; j++) { |
239 versionRegionArray[j] = regionArray[dis.readShort()]; |
195 String region = regionArray[dis.readShort()]; |
240 versionRulesArray[j] = dis.readShort(); |
196 Object rule = ruleArray[dis.readShort() & 0xffff]; |
|
197 regionToRules.put(region, rule); |
241 } |
198 } |
242 versionSet.add(new Version(versionArray[i], versionRegionArray, versionRulesArray, ruleData)); |
199 } |
243 } |
|
244 return versionSet; |
|
245 } |
200 } |
246 |
201 |
247 @Override |
202 @Override |
248 public String toString() { |
203 public String toString() { |
249 return "TZDB"; |
204 return "TZDB[" + versionId + "]"; |
250 } |
205 } |
251 |
|
252 //----------------------------------------------------------------------- |
|
253 /** |
|
254 * A version of the TZDB rules. |
|
255 */ |
|
256 static class Version { |
|
257 private final String versionId; |
|
258 private final String[] regionArray; |
|
259 private final short[] ruleIndices; |
|
260 private final AtomicReferenceArray<Object> ruleData; |
|
261 |
|
262 Version(String versionId, String[] regionIds, short[] ruleIndices, AtomicReferenceArray<Object> ruleData) { |
|
263 this.ruleData = ruleData; |
|
264 this.versionId = versionId; |
|
265 this.regionArray = regionIds; |
|
266 this.ruleIndices = ruleIndices; |
|
267 } |
|
268 |
|
269 ZoneRules getRules(String regionId) { |
|
270 int regionIndex = Arrays.binarySearch(regionArray, regionId); |
|
271 if (regionIndex < 0) { |
|
272 return null; |
|
273 } |
|
274 try { |
|
275 return createRule(ruleIndices[regionIndex]); |
|
276 } catch (Exception ex) { |
|
277 throw new ZoneRulesException("Invalid binary time-zone data: TZDB:" + regionId + ", version: " + versionId, ex); |
|
278 } |
|
279 } |
|
280 |
|
281 ZoneRules createRule(short index) throws Exception { |
|
282 Object obj = ruleData.get(index); |
|
283 if (obj instanceof byte[]) { |
|
284 byte[] bytes = (byte[]) obj; |
|
285 DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes)); |
|
286 obj = Ser.read(dis); |
|
287 ruleData.set(index, obj); |
|
288 } |
|
289 return (ZoneRules) obj; |
|
290 } |
|
291 |
|
292 @Override |
|
293 public String toString() { |
|
294 return versionId; |
|
295 } |
|
296 } |
|
297 |
|
298 } |
206 } |