View Javadoc
1   /*
2    * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package sun.util.calendar;
27  
28  import java.io.IOException;
29  import java.io.ObjectInputStream;
30  import java.lang.ref.SoftReference;
31  import java.security.AccessController;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Date;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.SimpleTimeZone;
40  import java.util.TimeZone;
41  
42  /**
43   * <code>ZoneInfo</code> is an implementation subclass of {@link
44   * java.util.TimeZone TimeZone} that represents GMT offsets and
45   * daylight saving time transitions of a time zone.
46   * <p>
47   * The daylight saving time transitions are described in the {@link
48   * #transitions transitions} table consisting of a chronological
49   * sequence of transitions of GMT offset and/or daylight saving time
50   * changes. Since all transitions are represented in UTC, in theory,
51   * <code>ZoneInfo</code> can be used with any calendar systems except
52   * for the {@link #getOffset(int,int,int,int,int,int) getOffset}
53   * method that takes Gregorian calendar date fields.
54   * <p>
55   * This table covers transitions from 1900 until 2037 (as of version
56   * 1.4), Before 1900, it assumes that there was no daylight saving
57   * time and the <code>getOffset</code> methods always return the
58   * {@link #getRawOffset} value. No Local Mean Time is supported. If a
59   * specified date is beyond the transition table and this time zone is
60   * supposed to observe daylight saving time in 2037, it delegates
61   * operations to a {@link java.util.SimpleTimeZone SimpleTimeZone}
62   * object created using the daylight saving time schedule as of 2037.
63   * <p>
64   * The date items, transitions, GMT offset(s), etc. are read from a database
65   * file. See {@link ZoneInfoFile} for details.
66   * @see java.util.SimpleTimeZone
67   * @since 1.4
68   */
69  
70  public class ZoneInfo extends TimeZone {
71  
72      private static final int UTC_TIME = 0;
73      private static final int STANDARD_TIME = 1;
74      private static final int WALL_TIME = 2;
75  
76      private static final long OFFSET_MASK = 0x0fL;
77      private static final long DST_MASK = 0xf0L;
78      private static final int DST_NSHIFT = 4;
79      // this bit field is reserved for abbreviation support
80      private static final long ABBR_MASK = 0xf00L;
81      private static final int TRANSITION_NSHIFT = 12;
82  
83      private static final CalendarSystem gcal = CalendarSystem.getGregorianCalendar();
84  
85      /**
86       * The raw GMT offset in milliseconds between this zone and GMT.
87       * Negative offsets are to the west of Greenwich.  To obtain local
88       * <em>standard</em> time, add the offset to GMT time.
89       * @serial
90       */
91      private int rawOffset;
92  
93      /**
94       * Difference in milliseconds from the original GMT offset in case
95       * the raw offset value has been modified by calling {@link
96       * #setRawOffset}. The initial value is 0.
97       * @serial
98       */
99      private int rawOffsetDiff = 0;
100 
101     /**
102      * A CRC32 value of all pairs of transition time (in milliseconds
103      * in <code>long</code>) in local time and its GMT offset (in
104      * seconds in <code>int</code>) in the chronological order. Byte
105      * values of each <code>long</code> and <code>int</code> are taken
106      * in the big endian order (i.e., MSB to LSB).
107      * @serial
108      */
109     private int checksum;
110 
111     /**
112      * The amount of time in milliseconds saved during daylight saving
113      * time. If <code>useDaylight</code> is false, this value is 0.
114      * @serial
115      */
116     private int dstSavings;
117 
118     /**
119      * This array describes transitions of GMT offsets of this time
120      * zone, including both raw offset changes and daylight saving
121      * time changes.
122      * A long integer consists of four bit fields.
123      * <ul>
124      * <li>The most significant 52-bit field represents transition
125      * time in milliseconds from Gregorian January 1 1970, 00:00:00
126      * GMT.</li>
127      * <li>The next 4-bit field is reserved and must be 0.</li>
128      * <li>The next 4-bit field is an index value to {@link #offsets
129      * offsets[]} for the amount of daylight saving at the
130      * transition. If this value is zero, it means that no daylight
131      * saving, not the index value zero.</li>
132      * <li>The least significant 4-bit field is an index value to
133      * {@link #offsets offsets[]} for <em>total</em> GMT offset at the
134      * transition.</li>
135      * </ul>
136      * If this time zone doesn't observe daylight saving time and has
137      * never changed any GMT offsets in the past, this value is null.
138      * @serial
139      */
140     private long[] transitions;
141 
142     /**
143      * This array holds all unique offset values in
144      * milliseconds. Index values to this array are stored in the
145      * transitions array elements.
146      * @serial
147      */
148     private int[] offsets;
149 
150     /**
151      * SimpleTimeZone parameter values. It has to have either 8 for
152      * {@link java.util.SimpleTimeZone#SimpleTimeZone(int, String,
153      * int, int , int , int , int , int , int , int , int) the
154      * 11-argument SimpleTimeZone constructor} or 10 for {@link
155      * java.util.SimpleTimeZone#SimpleTimeZone(int, String, int, int,
156      * int , int , int , int , int , int , int, int, int) the
157      * 13-argument SimpleTimeZone constructor} parameters.
158      * @serial
159      */
160     private int[] simpleTimeZoneParams;
161 
162     /**
163      * True if the raw GMT offset value would change after the time
164      * zone data has been generated; false, otherwise. The default
165      * value is false.
166      * @serial
167      */
168     private boolean willGMTOffsetChange = false;
169 
170     /**
171      * True if the object has been modified after its instantiation.
172      */
173     transient private boolean dirty = false;
174 
175     private static final long serialVersionUID = 2653134537216586139L;
176 
177     /**
178      * A constructor.
179      */
180     public ZoneInfo() {
181     }
182 
183     /**
184      * A Constructor for CustomID.
185      */
186     public ZoneInfo(String ID, int rawOffset) {
187         this(ID, rawOffset, 0, 0, null, null, null, false);
188     }
189 
190     /**
191      * Constructs a ZoneInfo instance.
192      *
193      * @param ID time zone name
194      * @param rawOffset GMT offset in milliseconds
195      * @param dstSavings daylight saving value in milliseconds or 0
196      * (zero) if this time zone doesn't observe Daylight Saving Time.
197      * @param checksum CRC32 value with all transitions table entry
198      * values
199      * @param transitions transition table
200      * @param offsets offset value table
201      * @param simpleTimeZoneParams parameter values for constructing
202      * SimpleTimeZone
203      * @param willGMTOffsetChange the value of willGMTOffsetChange
204      */
205     ZoneInfo(String ID,
206              int rawOffset,
207              int dstSavings,
208              int checksum,
209              long[] transitions,
210              int[] offsets,
211              int[] simpleTimeZoneParams,
212              boolean willGMTOffsetChange) {
213         setID(ID);
214         this.rawOffset = rawOffset;
215         this.dstSavings = dstSavings;
216         this.checksum = checksum;
217         this.transitions = transitions;
218         this.offsets = offsets;
219         this.simpleTimeZoneParams = simpleTimeZoneParams;
220         this.willGMTOffsetChange = willGMTOffsetChange;
221     }
222 
223     /**
224      * Returns the difference in milliseconds between local time and UTC
225      * of given time, taking into account both the raw offset and the
226      * effect of daylight savings.
227      *
228      * @param date the milliseconds in UTC
229      * @return the milliseconds to add to UTC to get local wall time
230      */
231     public int getOffset(long date) {
232         return getOffsets(date, null, UTC_TIME);
233     }
234 
235     public int getOffsets(long utc, int[] offsets) {
236         return getOffsets(utc, offsets, UTC_TIME);
237     }
238 
239     public int getOffsetsByStandard(long standard, int[] offsets) {
240         return getOffsets(standard, offsets, STANDARD_TIME);
241     }
242 
243     public int getOffsetsByWall(long wall, int[] offsets) {
244         return getOffsets(wall, offsets, WALL_TIME);
245     }
246 
247     private int getOffsets(long date, int[] offsets, int type) {
248         // if dst is never observed, there is no transition.
249         if (transitions == null) {
250             int offset = getLastRawOffset();
251             if (offsets != null) {
252                 offsets[0] = offset;
253                 offsets[1] = 0;
254             }
255             return offset;
256         }
257 
258         date -= rawOffsetDiff;
259         int index = getTransitionIndex(date, type);
260 
261         // prior to the transition table, returns the raw offset.
262         // FIXME: should support LMT.
263         if (index < 0) {
264             int offset = getLastRawOffset();
265             if (offsets != null) {
266                 offsets[0] = offset;
267                 offsets[1] = 0;
268             }
269             return offset;
270         }
271 
272         if (index < transitions.length) {
273             long val = transitions[index];
274             int offset = this.offsets[(int)(val & OFFSET_MASK)] + rawOffsetDiff;
275             if (offsets != null) {
276                 int dst = (int)((val >>> DST_NSHIFT) & 0xfL);
277                 int save = (dst == 0) ? 0 : this.offsets[dst];
278                 offsets[0] = offset - save;
279                 offsets[1] = save;
280             }
281             return offset;
282         }
283 
284         // beyond the transitions, delegate to SimpleTimeZone if there
285         // is a rule; otherwise, return rawOffset.
286         SimpleTimeZone tz = getLastRule();
287         if (tz != null) {
288             int rawoffset = tz.getRawOffset();
289             long msec = date;
290             if (type != UTC_TIME) {
291                 msec -= rawOffset;
292             }
293             int dstoffset = tz.getOffset(msec) - rawOffset;
294 
295             // Check if it's in a standard-to-daylight transition.
296             if (dstoffset > 0 && tz.getOffset(msec - dstoffset) == rawoffset) {
297                 dstoffset = 0;
298             }
299 
300             if (offsets != null) {
301                 offsets[0] = rawoffset;
302                 offsets[1] = dstoffset;
303             }
304             return rawoffset + dstoffset;
305         }
306         int offset = getLastRawOffset();
307         if (offsets != null) {
308             offsets[0] = offset;
309             offsets[1] = 0;
310         }
311         return offset;
312     }
313 
314     private int getTransitionIndex(long date, int type) {
315         int low = 0;
316         int high = transitions.length - 1;
317 
318         while (low <= high) {
319             int mid = (low + high) / 2;
320             long val = transitions[mid];
321             long midVal = val >> TRANSITION_NSHIFT; // sign extended
322             if (type != UTC_TIME) {
323                 midVal += offsets[(int)(val & OFFSET_MASK)]; // wall time
324             }
325             if (type == STANDARD_TIME) {
326                 int dstIndex = (int)((val >>> DST_NSHIFT) & 0xfL);
327                 if (dstIndex != 0) {
328                     midVal -= offsets[dstIndex]; // make it standard time
329                 }
330             }
331 
332             if (midVal < date) {
333                 low = mid + 1;
334             } else if (midVal > date) {
335                 high = mid - 1;
336             } else {
337                 return mid;
338             }
339         }
340 
341         // if beyond the transitions, returns that index.
342         if (low >= transitions.length) {
343             return low;
344         }
345         return low - 1;
346     }
347 
348    /**
349      * Returns the difference in milliseconds between local time and
350      * UTC, taking into account both the raw offset and the effect of
351      * daylight savings, for the specified date and time.  This method
352      * assumes that the start and end month are distinct.  This method
353      * assumes a Gregorian calendar for calculations.
354      * <p>
355      * <em>Note: In general, clients should use
356      * {@link Calendar#ZONE_OFFSET Calendar.get(ZONE_OFFSET)} +
357      * {@link Calendar#DST_OFFSET Calendar.get(DST_OFFSET)}
358      * instead of calling this method.</em>
359      *
360      * @param era       The era of the given date. The value must be either
361      *                  GregorianCalendar.AD or GregorianCalendar.BC.
362      * @param year      The year in the given date.
363      * @param month     The month in the given date. Month is 0-based. e.g.,
364      *                  0 for January.
365      * @param day       The day-in-month of the given date.
366      * @param dayOfWeek The day-of-week of the given date.
367      * @param millis    The milliseconds in day in <em>standard</em> local time.
368      * @return The milliseconds to add to UTC to get local time.
369      */
370     public int getOffset(int era, int year, int month, int day,
371                          int dayOfWeek, int milliseconds) {
372         if (milliseconds < 0 || milliseconds >= AbstractCalendar.DAY_IN_MILLIS) {
373             throw new IllegalArgumentException();
374         }
375 
376         if (era == java.util.GregorianCalendar.BC) { // BC
377             year = 1 - year;
378         } else if (era != java.util.GregorianCalendar.AD) {
379             throw new IllegalArgumentException();
380         }
381 
382         CalendarDate date = gcal.newCalendarDate(null);
383         date.setDate(year, month + 1, day);
384         if (gcal.validate(date) == false) {
385             throw new IllegalArgumentException();
386         }
387 
388         // bug-for-bug compatible argument checking
389         if (dayOfWeek < java.util.GregorianCalendar.SUNDAY
390             || dayOfWeek > java.util.GregorianCalendar.SATURDAY) {
391             throw new IllegalArgumentException();
392         }
393 
394         if (transitions == null) {
395             return getLastRawOffset();
396         }
397 
398         long dateInMillis = gcal.getTime(date) + milliseconds;
399         dateInMillis -= (long) rawOffset; // make it UTC
400         return getOffsets(dateInMillis, null, UTC_TIME);
401     }
402 
403     /**
404      * Sets the base time zone offset from GMT. This operation
405      * modifies all the transitions of this ZoneInfo object, including
406      * historical ones, if applicable.
407      *
408      * @param offsetMillis the base time zone offset to GMT.
409      * @see getRawOffset
410      */
411     public synchronized void setRawOffset(int offsetMillis) {
412         if (offsetMillis == rawOffset + rawOffsetDiff) {
413             return;
414         }
415         rawOffsetDiff = offsetMillis - rawOffset;
416         if (lastRule != null) {
417             lastRule.setRawOffset(offsetMillis);
418         }
419         dirty = true;
420     }
421 
422     /**
423      * Returns the GMT offset of the current date. This GMT offset
424      * value is not modified during Daylight Saving Time.
425      *
426      * @return the GMT offset value in milliseconds to add to UTC time
427      * to get local standard time
428      */
429     public int getRawOffset() {
430         if (!willGMTOffsetChange) {
431             return rawOffset + rawOffsetDiff;
432         }
433 
434         int[] offsets = new int[2];
435         getOffsets(System.currentTimeMillis(), offsets, UTC_TIME);
436         return offsets[0];
437     }
438 
439     public boolean isDirty() {
440         return dirty;
441     }
442 
443     private int getLastRawOffset() {
444         return rawOffset + rawOffsetDiff;
445     }
446 
447     /**
448      * Queries if this time zone uses Daylight Saving Time in the last known rule.
449      */
450     public boolean useDaylightTime() {
451         return (simpleTimeZoneParams != null);
452     }
453 
454     @Override
455     public boolean observesDaylightTime() {
456         if (simpleTimeZoneParams != null) {
457             return true;
458         }
459         if (transitions == null) {
460             return false;
461         }
462 
463         // Look up the transition table to see if it's in DST right
464         // now or if there's any standard-to-daylight transition at
465         // any future.
466         long utc = System.currentTimeMillis() - rawOffsetDiff;
467         int index = getTransitionIndex(utc, UTC_TIME);
468 
469         // before transitions in the transition table
470         if (index < 0) {
471             return false;
472         }
473 
474         // the time is in the table range.
475         for (int i = index; i < transitions.length; i++) {
476             if ((transitions[i] & DST_MASK) != 0) {
477                 return true;
478             }
479         }
480         // No further DST is observed.
481         return false;
482     }
483 
484     /**
485      * Queries if the specified date is in Daylight Saving Time.
486      */
487     public boolean inDaylightTime(Date date) {
488         if (date == null) {
489             throw new NullPointerException();
490         }
491 
492         if (transitions == null) {
493             return false;
494         }
495 
496         long utc = date.getTime() - rawOffsetDiff;
497         int index = getTransitionIndex(utc, UTC_TIME);
498 
499         // before transitions in the transition table
500         if (index < 0) {
501             return false;
502         }
503 
504         // the time is in the table range.
505         if (index < transitions.length) {
506             return (transitions[index] & DST_MASK) != 0;
507         }
508 
509         // beyond the transition table
510         SimpleTimeZone tz = getLastRule();
511         if (tz != null) {
512             return tz.inDaylightTime(date);
513        }
514         return false;
515     }
516 
517     /**
518      * Returns the amount of time in milliseconds that the clock is advanced
519      * during daylight saving time is in effect in its last daylight saving time rule.
520      *
521      * @return the number of milliseconds the time is advanced with respect to
522      * standard time when daylight saving time is in effect.
523      */
524     public int getDSTSavings() {
525         return dstSavings;
526     }
527 
528 //    /**
529 //     * @return the last year in the transition table or -1 if this
530 //     * time zone doesn't observe any daylight saving time.
531 //     */
532 //    public int getMaxTransitionYear() {
533 //      if (transitions == null) {
534 //          return -1;
535 //      }
536 //      long val = transitions[transitions.length - 1];
537 //      int offset = this.offsets[(int)(val & OFFSET_MASK)] + rawOffsetDiff;
538 //      val = (val >> TRANSITION_NSHIFT) + offset;
539 //      CalendarDate lastDate = Gregorian.getCalendarDate(val);
540 //      return lastDate.getYear();
541 //    }
542 
543     /**
544      * Returns a string representation of this time zone.
545      * @return the string
546      */
547     public String toString() {
548         return getClass().getName() +
549             "[id=\"" + getID() + "\"" +
550             ",offset=" + getLastRawOffset() +
551             ",dstSavings=" + dstSavings +
552             ",useDaylight=" + useDaylightTime() +
553             ",transitions=" + ((transitions != null) ? transitions.length : 0) +
554             ",lastRule=" + (lastRule == null ? getLastRuleInstance() : lastRule) +
555             "]";
556     }
557 
558     /**
559      * Gets all available IDs supported in the Java run-time.
560      *
561      * @return an array of time zone IDs.
562      */
563     public static String[] getAvailableIDs() {
564         return ZoneInfoFile.getZoneIds();
565     }
566 
567     /**
568      * Gets all available IDs that have the same value as the
569      * specified raw GMT offset.
570      *
571      * @param rawOffset the GMT offset in milliseconds. This
572      * value should not include any daylight saving time.
573      *
574      * @return an array of time zone IDs.
575      */
576     public static String[] getAvailableIDs(int rawOffset) {
577         return ZoneInfoFile.getZoneIds(rawOffset);
578     }
579 
580     /**
581      * Gets the ZoneInfo for the given ID.
582      *
583      * @param ID the ID for a ZoneInfo. See TimeZone for detail.
584      *
585      * @return the specified ZoneInfo object, or null if there is no
586      * time zone of the ID.
587      */
588     public static TimeZone getTimeZone(String ID) {
589         return ZoneInfoFile.getZoneInfo(ID);
590     }
591 
592     private transient SimpleTimeZone lastRule;
593 
594     /**
595      * Returns a SimpleTimeZone object representing the last GMT
596      * offset and DST schedule or null if this time zone doesn't
597      * observe DST.
598      */
599     private synchronized SimpleTimeZone getLastRule() {
600         if (lastRule == null) {
601             lastRule = getLastRuleInstance();
602         }
603         return lastRule;
604     }
605 
606     /**
607      * Returns a SimpleTimeZone object that represents the last
608      * known daylight saving time rules.
609      *
610      * @return a SimpleTimeZone object or null if this time zone
611      * doesn't observe DST.
612      */
613     public SimpleTimeZone getLastRuleInstance() {
614         if (simpleTimeZoneParams == null) {
615             return null;
616         }
617         if (simpleTimeZoneParams.length == 10) {
618             return new SimpleTimeZone(getLastRawOffset(), getID(),
619                                       simpleTimeZoneParams[0],
620                                       simpleTimeZoneParams[1],
621                                       simpleTimeZoneParams[2],
622                                       simpleTimeZoneParams[3],
623                                       simpleTimeZoneParams[4],
624                                       simpleTimeZoneParams[5],
625                                       simpleTimeZoneParams[6],
626                                       simpleTimeZoneParams[7],
627                                       simpleTimeZoneParams[8],
628                                       simpleTimeZoneParams[9],
629                                       dstSavings);
630         }
631         return new SimpleTimeZone(getLastRawOffset(), getID(),
632                                   simpleTimeZoneParams[0],
633                                   simpleTimeZoneParams[1],
634                                   simpleTimeZoneParams[2],
635                                   simpleTimeZoneParams[3],
636                                   simpleTimeZoneParams[4],
637                                   simpleTimeZoneParams[5],
638                                   simpleTimeZoneParams[6],
639                                   simpleTimeZoneParams[7],
640                                   dstSavings);
641     }
642 
643     /**
644      * Returns a copy of this <code>ZoneInfo</code>.
645      */
646     public Object clone() {
647         ZoneInfo zi = (ZoneInfo) super.clone();
648         zi.lastRule = null;
649         return zi;
650     }
651 
652     /**
653      * Returns a hash code value calculated from the GMT offset and
654      * transitions.
655      * @return a hash code of this time zone
656      */
657     public int hashCode() {
658         return getLastRawOffset() ^ checksum;
659     }
660 
661     /**
662      * Compares the equity of two ZoneInfo objects.
663      *
664      * @param obj the object to be compared with
665      * @return true if given object is same as this ZoneInfo object,
666      * false otherwise.
667      */
668     public boolean equals(Object obj) {
669         if (this == obj) {
670             return true;
671         }
672         if (!(obj instanceof ZoneInfo)) {
673             return false;
674         }
675         ZoneInfo that = (ZoneInfo) obj;
676         return (getID().equals(that.getID())
677                 && (getLastRawOffset() == that.getLastRawOffset())
678                 && (checksum == that.checksum));
679     }
680 
681     /**
682      * Returns true if this zone has the same raw GMT offset value and
683      * transition table as another zone info. If the specified
684      * TimeZone object is not a ZoneInfo instance, this method returns
685      * true if the specified TimeZone object has the same raw GMT
686      * offset value with no daylight saving time.
687      *
688      * @param other the ZoneInfo object to be compared with
689      * @return true if the given <code>TimeZone</code> has the same
690      * GMT offset and transition information; false, otherwise.
691      */
692     public boolean hasSameRules(TimeZone other) {
693         if (this == other) {
694             return true;
695         }
696         if (other == null) {
697             return false;
698         }
699         if (!(other instanceof ZoneInfo)) {
700             if (getRawOffset() != other.getRawOffset()) {
701                 return false;
702             }
703             // if both have the same raw offset and neither observes
704             // DST, they have the same rule.
705             if ((transitions == null)
706                 && (useDaylightTime() == false)
707                 && (other.useDaylightTime() == false)) {
708                 return true;
709             }
710             return false;
711         }
712         if (getLastRawOffset() != ((ZoneInfo)other).getLastRawOffset()) {
713             return false;
714         }
715         return (checksum == ((ZoneInfo)other).checksum);
716     }
717 
718     /**
719      * Returns a Map from alias time zone IDs to their standard
720      * time zone IDs.
721      *
722      * @return the Map that holds the mappings from alias time zone IDs
723      *    to their standard time zone IDs, or null if
724      *    <code>ZoneInfoMappings</code> file is not available.
725      */
726     public static Map<String, String> getAliasTable() {
727          return ZoneInfoFile.getAliasMap();
728     }
729 
730     private void readObject(ObjectInputStream stream)
731             throws IOException, ClassNotFoundException {
732         stream.defaultReadObject();
733         // We don't know how this object from 1.4.x or earlier has
734         // been mutated. So it should always be marked as `dirty'.
735         dirty = true;
736     }
737 }