1 /*
   2  * Copyright (c) 2012, 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 /*
  27  * Copyright (c) 2012, Stephen Colebourne & Michael Nascimento Santos
  28  *
  29  * All rights reserved.
  30  *
  31  * Redistribution and use in source and binary forms, with or without
  32  * modification, are permitted provided that the following conditions are met:
  33  *
  34  *  * Redistributions of source code must retain the above copyright notice,
  35  *    this list of conditions and the following disclaimer.
  36  *
  37  *  * Redistributions in binary form must reproduce the above copyright notice,
  38  *    this list of conditions and the following disclaimer in the documentation
  39  *    and/or other materials provided with the distribution.
  40  *
  41  *  * Neither the name of JSR-310 nor the names of its contributors
  42  *    may be used to endorse or promote products derived from this software
  43  *    without specific prior written permission.
  44  *
  45  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  46  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  47  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  48  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  49  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
  50  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
  51  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
  52  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
  53  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
  54  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  55  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  56  */
  57 
  58 package java.time.chrono;
  59 
  60 import static java.time.temporal.ChronoField.EPOCH_DAY;
  61 
  62 import java.io.File;
  63 import java.io.FileInputStream;
  64 import java.io.IOException;
  65 import java.io.InputStream;
  66 import java.io.Serializable;
  67 import java.security.AccessController;
  68 import java.security.PrivilegedActionException;
  69 import java.time.Clock;
  70 import java.time.DateTimeException;
  71 import java.time.Instant;
  72 import java.time.LocalDate;
  73 import java.time.ZoneId;
  74 import java.time.format.ResolverStyle;
  75 import java.time.temporal.ChronoField;
  76 import java.time.temporal.TemporalAccessor;
  77 import java.time.temporal.TemporalField;
  78 import java.time.temporal.ValueRange;
  79 import java.util.Arrays;
  80 import java.util.HashMap;
  81 import java.util.List;
  82 import java.util.Map;
  83 import java.util.Objects;
  84 import java.util.Properties;
  85 
  86 import sun.util.logging.PlatformLogger;
  87 
  88 /**
  89  * The Hijrah calendar is a lunar calendar supporting Islamic calendars.
  90  * <p>
  91  * The HijrahChronology follows the rules of the Hijrah calendar system. The Hijrah
  92  * calendar has several variants based on differences in when the new moon is
  93  * determined to have occurred and where the observation is made.
  94  * In some variants the length of each month is
  95  * computed algorithmically from the astronomical data for the moon and earth and
  96  * in others the length of the month is determined by an authorized sighting
  97  * of the new moon. For the algorithmically based calendars the calendar
  98  * can project into the future.
  99  * For sighting based calendars only historical data from past
 100  * sightings is available.
 101  * <p>
 102  * The length of each month is 29 or 30 days.
 103  * Ordinary years have 354 days; leap years have 355 days.
 104  *
 105  * <p>
 106  * CLDR and LDML identify variants:
 107  * <table cellpadding="2" summary="Variants of Hijrah Calendars">
 108  * <thead>
 109  * <tr class="tableSubHeadingColor">
 110  * <th class="colFirst" align="left" >Chronology ID</th>
 111  * <th class="colFirst" align="left" >Calendar Type</th>
 112  * <th class="colFirst" align="left" >Locale extension, see {@link java.util.Locale}</th>
 113  * <th class="colLast" align="left" >Description</th>
 114  * </tr>
 115  * </thead>
 116  * <tbody>
 117  * <tr class="altColor">
 118  * <td>Hijrah-umalqura</td>
 119  * <td>islamic-umalqura</td>
 120  * <td>ca-islamic-umalqura</td>
 121  * <td>Islamic - Umm Al-Qura calendar of Saudi Arabia</td>
 122  * </tr>
 123  * </tbody>
 124  * </table>
 125  * <p>Additional variants may be available through {@link Chronology#getAvailableChronologies()}.
 126  *
 127  * <p>Example</p>
 128  * <p>
 129  * Selecting the chronology from the locale uses {@link Chronology#ofLocale}
 130  * to find the Chronology based on Locale supported BCP 47 extension mechanism
 131  * to request a specific calendar ("ca"). For example,
 132  * </p>
 133  * <pre>
 134  *      Locale locale = Locale.forLanguageTag("en-US-u-ca-islamic-umalqura");
 135  *      Chronology chrono = Chronology.ofLocale(locale);
 136  * </pre>
 137  *
 138  * @implSpec
 139  * This class is immutable and thread-safe.
 140  *
 141  * @implNote
 142  * Each Hijrah variant is configured individually. Each variant is defined by a
 143  * property resource that defines the {@code ID}, the {@code calendar type},
 144  * the start of the calendar, the alignment with the
 145  * ISO calendar, and the length of each month for a range of years.
 146  * The variants are identified in the {@code calendars.properties} file.
 147  * The new properties are prefixed with {@code "calendars.hijrah."}:
 148  * <table cellpadding="2" border="0" summary="Configuration of Hijrah Calendar Variants">
 149  * <thead>
 150  * <tr class="tableSubHeadingColor">
 151  * <th class="colFirst" align="left">Property Name</th>
 152  * <th class="colFirst" align="left">Property value</th>
 153  * <th class="colLast" align="left">Description </th>
 154  * </tr>
 155  * </thead>
 156  * <tbody>
 157  * <tr class="altColor">
 158  * <td>calendars.hijrah.{ID}</td>
 159  * <td>The property resource defining the {@code {ID}} variant</td>
 160  * <td>The property resource is located with the {@code calendars.properties} file</td>
 161  * </tr>
 162  * <tr class="rowColor">
 163  * <td>calendars.hijrah.{ID}.type</td>
 164  * <td>The calendar type</td>
 165  * <td>LDML defines the calendar type names</td>
 166  * </tr>
 167  * </tbody>
 168  * </table>
 169  * <p>
 170  * The Hijrah property resource is a set of properties that describe the calendar.
 171  * The syntax is defined by {@code java.util.Properties#load(Reader)}.
 172  * <table cellpadding="2" summary="Configuration of Hijrah Calendar">
 173  * <thead>
 174  * <tr class="tableSubHeadingColor">
 175  * <th class="colFirst" align="left" > Property Name</th>
 176  * <th class="colFirst" align="left" > Property value</th>
 177  * <th class="colLast" align="left" > Description </th>
 178  * </tr>
 179  * </thead>
 180  * <tbody>
 181  * <tr class="altColor">
 182  * <td>id</td>
 183  * <td>Chronology Id, for example, "Hijrah-umalqura"</td>
 184  * <td>The Id of the calendar in common usage</td>
 185  * </tr>
 186  * <tr class="rowColor">
 187  * <td>type</td>
 188  * <td>Calendar type, for example, "islamic-umalqura"</td>
 189  * <td>LDML defines the calendar types</td>
 190  * </tr>
 191  * <tr class="altColor">
 192  * <td>version</td>
 193  * <td>Version, for example: "1.8.0_1"</td>
 194  * <td>The version of the Hijrah variant data</td>
 195  * </tr>
 196  * <tr class="rowColor">
 197  * <td>iso-start</td>
 198  * <td>ISO start date, formatted as {@code yyyy-MM-dd}, for example: "1900-04-30"</td>
 199  * <td>The ISO date of the first day of the minimum Hijrah year.</td>
 200  * </tr>
 201  * <tr class="altColor">
 202  * <td>yyyy - a numeric 4 digit year, for example "1434"</td>
 203  * <td>The value is a sequence of 12 month lengths,
 204  * for example: "29 30 29 30 29 30 30 30 29 30 29 29"</td>
 205  * <td>The lengths of the 12 months of the year separated by whitespace.
 206  * A numeric year property must be present for every year without any gaps.
 207  * The month lengths must be between 29-32 inclusive.
 208  * </td>
 209  * </tr>
 210  * </tbody>
 211  * </table>
 212  *
 213  * @since 1.8
 214  */
 215 public final class HijrahChronology extends Chronology implements Serializable {
 216 
 217     /**
 218      * The Hijrah Calendar id.
 219      */
 220     private final String typeId;
 221     /**
 222      * The Hijrah calendarType.
 223      */
 224     private transient final String calendarType;
 225     /**
 226      * Serialization version.
 227      */
 228     private static final long serialVersionUID = 3127340209035924785L;
 229     /**
 230      * Singleton instance of the Islamic Umm Al-Qura calendar of Saudi Arabia.
 231      * Other Hijrah chronology variants may be available from
 232      * {@link Chronology#getAvailableChronologies}.
 233      */
 234     public static final HijrahChronology INSTANCE;
 235     /**
 236      * Flag to indicate the initialization of configuration data is complete.
 237      * @see #checkCalendarInit()
 238      */
 239     private volatile boolean initComplete;
 240     /**
 241      * Array of epoch days indexed by Hijrah Epoch month.
 242      * Computed by {@link #loadCalendarData}.
 243      */
 244     private transient int[] hijrahEpochMonthStartDays;
 245     /**
 246      * The minimum epoch day of this Hijrah calendar.
 247      * Computed by {@link #loadCalendarData}.
 248      */
 249     private transient int minEpochDay;
 250     /**
 251      * The maximum epoch day for which calendar data is available.
 252      * Computed by {@link #loadCalendarData}.
 253      */
 254     private transient int maxEpochDay;
 255     /**
 256      * The minimum epoch month.
 257      * Computed by {@link #loadCalendarData}.
 258      */
 259     private transient int hijrahStartEpochMonth;
 260     /**
 261      * The minimum length of a month.
 262      * Computed by {@link #createEpochMonths}.
 263      */
 264     private transient int minMonthLength;
 265     /**
 266      * The maximum length of a month.
 267      * Computed by {@link #createEpochMonths}.
 268      */
 269     private transient int maxMonthLength;
 270     /**
 271      * The minimum length of a year in days.
 272      * Computed by {@link #createEpochMonths}.
 273      */
 274     private transient int minYearLength;
 275     /**
 276      * The maximum length of a year in days.
 277      * Computed by {@link #createEpochMonths}.
 278      */
 279     private transient int maxYearLength;
 280     /**
 281      * A reference to the properties stored in
 282      * ${java.home}/lib/calendars.properties
 283      */
 284     private transient final static Properties calendarProperties;
 285 
 286     /**
 287      * Prefix of property names for Hijrah calendar variants.
 288      */
 289     private static final String PROP_PREFIX = "calendar.hijrah.";
 290     /**
 291      * Suffix of property names containing the calendar type of a variant.
 292      */
 293     private static final String PROP_TYPE_SUFFIX = ".type";
 294 
 295     /**
 296      * Static initialization of the predefined calendars found in the
 297      * lib/calendars.properties file.
 298      */
 299     static {
 300         try {
 301             calendarProperties = sun.util.calendar.BaseCalendar.getCalendarProperties();
 302         } catch (IOException ioe) {
 303             throw new InternalError("Can't initialize lib/calendars.properties", ioe);
 304         }
 305 
 306         try {
 307             INSTANCE = new HijrahChronology("Hijrah-umalqura");
 308             // Register it by its aliases
 309             Chronology.registerChrono(INSTANCE, "Hijrah");
 310             Chronology.registerChrono(INSTANCE, "islamic");
 311         } catch (DateTimeException ex) {
 312             // Absence of Hijrah calendar is fatal to initializing this class.
 313             PlatformLogger logger = PlatformLogger.getLogger("java.time.chrono");
 314             logger.severe("Unable to initialize Hijrah calendar: Hijrah-umalqura", ex);
 315             throw new RuntimeException("Unable to initialize Hijrah-umalqura calendar", ex.getCause());
 316         }
 317         registerVariants();
 318     }
 319 
 320     /**
 321      * For each Hijrah variant listed, create the HijrahChronology and register it.
 322      * Exceptions during initialization are logged but otherwise ignored.
 323      */
 324     private static void registerVariants() {
 325         for (String name : calendarProperties.stringPropertyNames()) {
 326             if (name.startsWith(PROP_PREFIX)) {
 327                 String id = name.substring(PROP_PREFIX.length());
 328                 if (id.indexOf('.') >= 0) {
 329                     continue;   // no name or not a simple name of a calendar
 330                 }
 331                 if (id.equals(INSTANCE.getId())) {
 332                     continue;           // do not duplicate the default
 333                 }
 334                 try {
 335                     // Create and register the variant
 336                     HijrahChronology chrono = new HijrahChronology(id);
 337                     Chronology.registerChrono(chrono);
 338                 } catch (DateTimeException ex) {
 339                     // Log error and continue
 340                     PlatformLogger logger = PlatformLogger.getLogger("java.time.chrono");
 341                     logger.severe("Unable to initialize Hijrah calendar: " + id, ex);
 342                 }
 343             }
 344         }
 345     }
 346 
 347     /**
 348      * Create a HijrahChronology for the named variant.
 349      * The resource and calendar type are retrieved from properties
 350      * in the {@code calendars.properties}.
 351      * The property names are {@code "calendar.hijrah." + id}
 352      * and  {@code "calendar.hijrah." + id + ".type"}
 353      * @param id the id of the calendar
 354      * @throws DateTimeException if the calendar type is missing from the properties file.
 355      * @throws IllegalArgumentException if the id is empty
 356      */
 357     private HijrahChronology(String id) throws DateTimeException {
 358         if (id.isEmpty()) {
 359             throw new IllegalArgumentException("calendar id is empty");
 360         }
 361         String propName = PROP_PREFIX + id + PROP_TYPE_SUFFIX;
 362         String calType = calendarProperties.getProperty(propName);
 363         if (calType == null || calType.isEmpty()) {
 364             throw new DateTimeException("calendarType is missing or empty for: " + propName);
 365         }
 366         this.typeId = id;
 367         this.calendarType = calType;
 368     }
 369 
 370     /**
 371      * Check and ensure that the calendar data has been initialized.
 372      * The initialization check is performed at the boundary between
 373      * public and package methods.  If a public calls another public method
 374      * a check is not necessary in the caller.
 375      * The constructors of HijrahDate call {@link #getEpochDay} or
 376      * {@link #getHijrahDateInfo} so every call from HijrahDate to a
 377      * HijrahChronology via package private methods has been checked.
 378      *
 379      * @throws DateTimeException if the calendar data configuration is
 380      *     malformed or IOExceptions occur loading the data
 381      */
 382     private void checkCalendarInit() {
 383         // Keep this short so it can be inlined for performance
 384         if (initComplete == false) {
 385             loadCalendarData();
 386             initComplete = true;
 387         }
 388     }
 389 
 390     //-----------------------------------------------------------------------
 391     /**
 392      * Gets the ID of the chronology.
 393      * <p>
 394      * The ID uniquely identifies the {@code Chronology}. It can be used to
 395      * lookup the {@code Chronology} using {@link #of(String)}.
 396      *
 397      * @return the chronology ID, non-null
 398      * @see #getCalendarType()
 399      */
 400     @Override
 401     public String getId() {
 402         return typeId;
 403     }
 404 
 405     /**
 406      * Gets the calendar type of the Islamic calendar.
 407      * <p>
 408      * The calendar type is an identifier defined by the
 409      * <em>Unicode Locale Data Markup Language (LDML)</em> specification.
 410      * It can be used to lookup the {@code Chronology} using {@link #of(String)}.
 411      *
 412      * @return the calendar system type; non-null if the calendar has
 413      *    a standard type, otherwise null
 414      * @see #getId()
 415      */
 416     @Override
 417     public String getCalendarType() {
 418         return calendarType;
 419     }
 420 
 421     //-----------------------------------------------------------------------
 422     /**
 423      * Obtains a local date in Hijrah calendar system from the
 424      * era, year-of-era, month-of-year and day-of-month fields.
 425      *
 426      * @param era  the Hijrah era, not null
 427      * @param yearOfEra  the year-of-era
 428      * @param month  the month-of-year
 429      * @param dayOfMonth  the day-of-month
 430      * @return the Hijrah local date, not null
 431      * @throws DateTimeException if unable to create the date
 432      * @throws ClassCastException if the {@code era} is not a {@code HijrahEra}
 433      */
 434     @Override
 435     public HijrahDate date(Era era, int yearOfEra, int month, int dayOfMonth) {
 436         return date(prolepticYear(era, yearOfEra), month, dayOfMonth);
 437     }
 438 
 439     /**
 440      * Obtains a local date in Hijrah calendar system from the
 441      * proleptic-year, month-of-year and day-of-month fields.
 442      *
 443      * @param prolepticYear  the proleptic-year
 444      * @param month  the month-of-year
 445      * @param dayOfMonth  the day-of-month
 446      * @return the Hijrah local date, not null
 447      * @throws DateTimeException if unable to create the date
 448      */
 449     @Override
 450     public HijrahDate date(int prolepticYear, int month, int dayOfMonth) {
 451         return HijrahDate.of(this, prolepticYear, month, dayOfMonth);
 452     }
 453 
 454     /**
 455      * Obtains a local date in Hijrah calendar system from the
 456      * era, year-of-era and day-of-year fields.
 457      *
 458      * @param era  the Hijrah era, not null
 459      * @param yearOfEra  the year-of-era
 460      * @param dayOfYear  the day-of-year
 461      * @return the Hijrah local date, not null
 462      * @throws DateTimeException if unable to create the date
 463      * @throws ClassCastException if the {@code era} is not a {@code HijrahEra}
 464      */
 465     @Override
 466     public HijrahDate dateYearDay(Era era, int yearOfEra, int dayOfYear) {
 467         return dateYearDay(prolepticYear(era, yearOfEra), dayOfYear);
 468     }
 469 
 470     /**
 471      * Obtains a local date in Hijrah calendar system from the
 472      * proleptic-year and day-of-year fields.
 473      *
 474      * @param prolepticYear  the proleptic-year
 475      * @param dayOfYear  the day-of-year
 476      * @return the Hijrah local date, not null
 477      * @throws DateTimeException if the value of the year is out of range,
 478      *  or if the day-of-year is invalid for the year
 479      */
 480     @Override
 481     public HijrahDate dateYearDay(int prolepticYear, int dayOfYear) {
 482         HijrahDate date = HijrahDate.of(this, prolepticYear, 1, 1);
 483         if (dayOfYear > date.lengthOfYear()) {
 484             throw new DateTimeException("Invalid dayOfYear: " + dayOfYear);
 485         }
 486         return date.plusDays(dayOfYear - 1);
 487     }
 488 
 489     /**
 490      * Obtains a local date in the Hijrah calendar system from the epoch-day.
 491      *
 492      * @param epochDay  the epoch day
 493      * @return the Hijrah local date, not null
 494      * @throws DateTimeException if unable to create the date
 495      */
 496     @Override  // override with covariant return type
 497     public HijrahDate dateEpochDay(long epochDay) {
 498         return HijrahDate.ofEpochDay(this, epochDay);
 499     }
 500 
 501     @Override
 502     public HijrahDate dateNow() {
 503         return dateNow(Clock.systemDefaultZone());
 504     }
 505 
 506     @Override
 507     public HijrahDate dateNow(ZoneId zone) {
 508         return dateNow(Clock.system(zone));
 509     }
 510 
 511     @Override
 512     public HijrahDate dateNow(Clock clock) {
 513         return date(LocalDate.now(clock));
 514     }
 515 
 516     @Override
 517     public HijrahDate date(TemporalAccessor temporal) {
 518         if (temporal instanceof HijrahDate) {
 519             return (HijrahDate) temporal;
 520         }
 521         return HijrahDate.ofEpochDay(this, temporal.getLong(EPOCH_DAY));
 522     }
 523 
 524     @Override
 525     @SuppressWarnings("unchecked")
 526     public ChronoLocalDateTime<HijrahDate> localDateTime(TemporalAccessor temporal) {
 527         return (ChronoLocalDateTime<HijrahDate>) super.localDateTime(temporal);
 528     }
 529 
 530     @Override
 531     @SuppressWarnings("unchecked")
 532     public ChronoZonedDateTime<HijrahDate> zonedDateTime(TemporalAccessor temporal) {
 533         return (ChronoZonedDateTime<HijrahDate>) super.zonedDateTime(temporal);
 534     }
 535 
 536     @Override
 537     @SuppressWarnings("unchecked")
 538     public ChronoZonedDateTime<HijrahDate> zonedDateTime(Instant instant, ZoneId zone) {
 539         return (ChronoZonedDateTime<HijrahDate>) super.zonedDateTime(instant, zone);
 540     }
 541 
 542     //-----------------------------------------------------------------------
 543     @Override
 544     public boolean isLeapYear(long prolepticYear) {
 545         checkCalendarInit();
 546         int epochMonth = yearToEpochMonth((int) prolepticYear);
 547         if (epochMonth < 0 || epochMonth > maxEpochDay) {
 548             throw new DateTimeException("Hijrah date out of range");
 549         }
 550         int len = getYearLength((int) prolepticYear);
 551         return (len > 354);
 552     }
 553 
 554     @Override
 555     public int prolepticYear(Era era, int yearOfEra) {
 556         if (era instanceof HijrahEra == false) {
 557             throw new ClassCastException("Era must be HijrahEra");
 558         }
 559         return yearOfEra;
 560     }
 561 
 562     @Override
 563     public HijrahEra eraOf(int eraValue) {
 564         switch (eraValue) {
 565             case 1:
 566                 return HijrahEra.AH;
 567             default:
 568                 throw new DateTimeException("invalid Hijrah era");
 569         }
 570     }
 571 
 572     @Override
 573     public List<Era> eras() {
 574         return Arrays.<Era>asList(HijrahEra.values());
 575     }
 576 
 577     //-----------------------------------------------------------------------
 578     @Override
 579     public ValueRange range(ChronoField field) {
 580         checkCalendarInit();
 581         if (field instanceof ChronoField) {
 582             ChronoField f = field;
 583             switch (f) {
 584                 case DAY_OF_MONTH:
 585                     return ValueRange.of(1, 1, getMinimumMonthLength(), getMaximumMonthLength());
 586                 case DAY_OF_YEAR:
 587                     return ValueRange.of(1, getMaximumDayOfYear());
 588                 case ALIGNED_WEEK_OF_MONTH:
 589                     return ValueRange.of(1, 5);
 590                 case YEAR:
 591                 case YEAR_OF_ERA:
 592                     return ValueRange.of(getMinimumYear(), getMaximumYear());
 593                 case ERA:
 594                     return ValueRange.of(1, 1);
 595                 default:
 596                     return field.range();
 597             }
 598         }
 599         return field.range();
 600     }
 601 
 602     //-----------------------------------------------------------------------
 603     @Override  // override for return type
 604     public HijrahDate resolveDate(Map<TemporalField, Long> fieldValues, ResolverStyle resolverStyle) {
 605         return (HijrahDate) super.resolveDate(fieldValues, resolverStyle);
 606     }
 607 
 608     //-----------------------------------------------------------------------
 609     /**
 610      * Check the validity of a year.
 611      *
 612      * @param prolepticYear the year to check
 613      */
 614     int checkValidYear(long prolepticYear) {
 615         if (prolepticYear < getMinimumYear() || prolepticYear > getMaximumYear()) {
 616             throw new DateTimeException("Invalid Hijrah year: " + prolepticYear);
 617         }
 618         return (int) prolepticYear;
 619     }
 620 
 621     void checkValidDayOfYear(int dayOfYear) {
 622         if (dayOfYear < 1 || dayOfYear > getMaximumDayOfYear()) {
 623             throw new DateTimeException("Invalid Hijrah day of year: " + dayOfYear);
 624         }
 625     }
 626 
 627     void checkValidMonth(int month) {
 628         if (month < 1 || month > 12) {
 629             throw new DateTimeException("Invalid Hijrah month: " + month);
 630         }
 631     }
 632 
 633     //-----------------------------------------------------------------------
 634     /**
 635      * Returns an array containing the Hijrah year, month and day
 636      * computed from the epoch day.
 637      *
 638      * @param epochDay  the EpochDay
 639      * @return int[0] = YEAR, int[1] = MONTH, int[2] = DATE
 640      */
 641     int[] getHijrahDateInfo(int epochDay) {
 642         checkCalendarInit();    // ensure that the chronology is initialized
 643         if (epochDay < minEpochDay || epochDay >= maxEpochDay) {
 644             throw new DateTimeException("Hijrah date out of range");
 645         }
 646 
 647         int epochMonth = epochDayToEpochMonth(epochDay);
 648         int year = epochMonthToYear(epochMonth);
 649         int month = epochMonthToMonth(epochMonth);
 650         int day1 = epochMonthToEpochDay(epochMonth);
 651         int date = epochDay - day1; // epochDay - dayOfEpoch(year, month);
 652 
 653         int dateInfo[] = new int[3];
 654         dateInfo[0] = year;
 655         dateInfo[1] = month + 1; // change to 1-based.
 656         dateInfo[2] = date + 1; // change to 1-based.
 657         return dateInfo;
 658     }
 659 
 660     /**
 661      * Return the epoch day computed from Hijrah year, month, and day.
 662      *
 663      * @param prolepticYear the year to represent, 0-origin
 664      * @param monthOfYear the month-of-year to represent, 1-origin
 665      * @param dayOfMonth the day-of-month to represent, 1-origin
 666      * @return the epoch day
 667      */
 668     long getEpochDay(int prolepticYear, int monthOfYear, int dayOfMonth) {
 669         checkCalendarInit();    // ensure that the chronology is initialized
 670         checkValidMonth(monthOfYear);
 671         int epochMonth = yearToEpochMonth(prolepticYear) + (monthOfYear - 1);
 672         if (epochMonth < 0 || epochMonth >= hijrahEpochMonthStartDays.length) {
 673             throw new DateTimeException("Invalid Hijrah date, year: " +
 674                     prolepticYear +  ", month: " + monthOfYear);
 675         }
 676         if (dayOfMonth < 1 || dayOfMonth > getMonthLength(prolepticYear, monthOfYear)) {
 677             throw new DateTimeException("Invalid Hijrah day of month: " + dayOfMonth);
 678         }
 679         return epochMonthToEpochDay(epochMonth) + (dayOfMonth - 1);
 680     }
 681 
 682     /**
 683      * Returns day of year for the year and month.
 684      *
 685      * @param prolepticYear a proleptic year
 686      * @param month a month, 1-origin
 687      * @return the day of year, 1-origin
 688      */
 689     int getDayOfYear(int prolepticYear, int month) {
 690         return yearMonthToDayOfYear(prolepticYear, (month - 1));
 691     }
 692 
 693     /**
 694      * Returns month length for the year and month.
 695      *
 696      * @param prolepticYear a proleptic year
 697      * @param monthOfYear a month, 1-origin.
 698      * @return the length of the month
 699      */
 700     int getMonthLength(int prolepticYear, int monthOfYear) {
 701         int epochMonth = yearToEpochMonth(prolepticYear) + (monthOfYear - 1);
 702         if (epochMonth < 0 || epochMonth >= hijrahEpochMonthStartDays.length) {
 703             throw new DateTimeException("Invalid Hijrah date, year: " +
 704                     prolepticYear +  ", month: " + monthOfYear);
 705         }
 706         return epochMonthLength(epochMonth);
 707     }
 708 
 709     /**
 710      * Returns year length.
 711      * Note: The 12th month must exist in the data.
 712      *
 713      * @param prolepticYear a proleptic year
 714      * @return year length in days
 715      */
 716     int getYearLength(int prolepticYear) {
 717         return yearMonthToDayOfYear(prolepticYear, 12);
 718     }
 719 
 720     /**
 721      * Return the minimum supported Hijrah year.
 722      *
 723      * @return the minimum
 724      */
 725     int getMinimumYear() {
 726         return epochMonthToYear(0);
 727     }
 728 
 729     /**
 730      * Return the maximum supported Hijrah ear.
 731      *
 732      * @return the minimum
 733      */
 734     int getMaximumYear() {
 735         return epochMonthToYear(hijrahEpochMonthStartDays.length - 1) - 1;
 736     }
 737 
 738     /**
 739      * Returns maximum day-of-month.
 740      *
 741      * @return maximum day-of-month
 742      */
 743     int getMaximumMonthLength() {
 744         return maxMonthLength;
 745     }
 746 
 747     /**
 748      * Returns smallest maximum day-of-month.
 749      *
 750      * @return smallest maximum day-of-month
 751      */
 752     int getMinimumMonthLength() {
 753         return minMonthLength;
 754     }
 755 
 756     /**
 757      * Returns maximum day-of-year.
 758      *
 759      * @return maximum day-of-year
 760      */
 761     int getMaximumDayOfYear() {
 762         return maxYearLength;
 763     }
 764 
 765     /**
 766      * Returns smallest maximum day-of-year.
 767      *
 768      * @return smallest maximum day-of-year
 769      */
 770     int getSmallestMaximumDayOfYear() {
 771         return minYearLength;
 772     }
 773 
 774     /**
 775      * Returns the epochMonth found by locating the epochDay in the table. The
 776      * epochMonth is the index in the table
 777      *
 778      * @param epochDay
 779      * @return The index of the element of the start of the month containing the
 780      * epochDay.
 781      */
 782     private int epochDayToEpochMonth(int epochDay) {
 783         // binary search
 784         int ndx = Arrays.binarySearch(hijrahEpochMonthStartDays, epochDay);
 785         if (ndx < 0) {
 786             ndx = -ndx - 2;
 787         }
 788         return ndx;
 789     }
 790 
 791     /**
 792      * Returns the year computed from the epochMonth
 793      *
 794      * @param epochMonth the epochMonth
 795      * @return the Hijrah Year
 796      */
 797     private int epochMonthToYear(int epochMonth) {
 798         return (epochMonth + hijrahStartEpochMonth) / 12;
 799     }
 800 
 801     /**
 802      * Returns the epochMonth for the Hijrah Year.
 803      *
 804      * @param year the HijrahYear
 805      * @return the epochMonth for the beginning of the year.
 806      */
 807     private int yearToEpochMonth(int year) {
 808         return (year * 12) - hijrahStartEpochMonth;
 809     }
 810 
 811     /**
 812      * Returns the Hijrah month from the epochMonth.
 813      *
 814      * @param epochMonth the epochMonth
 815      * @return the month of the Hijrah Year
 816      */
 817     private int epochMonthToMonth(int epochMonth) {
 818         return (epochMonth + hijrahStartEpochMonth) % 12;
 819     }
 820 
 821     /**
 822      * Returns the epochDay for the start of the epochMonth.
 823      *
 824      * @param epochMonth the epochMonth
 825      * @return the epochDay for the start of the epochMonth.
 826      */
 827     private int epochMonthToEpochDay(int epochMonth) {
 828         return hijrahEpochMonthStartDays[epochMonth];
 829 
 830     }
 831 
 832     /**
 833      * Returns the day of year for the requested HijrahYear and month.
 834      *
 835      * @param prolepticYear the Hijrah year
 836      * @param month the Hijrah month
 837      * @return the day of year for the start of the month of the year
 838      */
 839     private int yearMonthToDayOfYear(int prolepticYear, int month) {
 840         int epochMonthFirst = yearToEpochMonth(prolepticYear);
 841         return epochMonthToEpochDay(epochMonthFirst + month)
 842                 - epochMonthToEpochDay(epochMonthFirst);
 843     }
 844 
 845     /**
 846      * Returns the length of the epochMonth. It is computed from the start of
 847      * the following month minus the start of the requested month.
 848      *
 849      * @param epochMonth the epochMonth; assumed to be within range
 850      * @return the length in days of the epochMonth
 851      */
 852     private int epochMonthLength(int epochMonth) {
 853         // The very last entry in the epochMonth table is not the start of a month
 854         return hijrahEpochMonthStartDays[epochMonth + 1]
 855                 - hijrahEpochMonthStartDays[epochMonth];
 856     }
 857 
 858     //-----------------------------------------------------------------------
 859     private static final String KEY_ID = "id";
 860     private static final String KEY_TYPE = "type";
 861     private static final String KEY_VERSION = "version";
 862     private static final String KEY_ISO_START = "iso-start";
 863 
 864     /**
 865      * Return the configuration properties from the resource.
 866      * <p>
 867      * The default location of the variant configuration resource is:
 868      * <pre>
 869      *   "$java.home/lib/" + resource-name
 870      * </pre>
 871      *
 872      * @param resource the name of the calendar property resource
 873      * @return a Properties containing the properties read from the resource.
 874      * @throws Exception if access to the property resource fails
 875      */
 876     private static Properties readConfigProperties(final String resource) throws Exception {
 877         try {
 878             return AccessController
 879                     .doPrivileged((java.security.PrivilegedExceptionAction<Properties>)
 880                         () -> {
 881                         String libDir = System.getProperty("java.home") + File.separator + "lib";
 882                         File file = new File(libDir, resource);
 883                         Properties props = new Properties();
 884                         try (InputStream is = new FileInputStream(file)) {
 885                             props.load(is);
 886                         }
 887                         return props;
 888                     });
 889         } catch (PrivilegedActionException pax) {
 890             throw pax.getException();
 891         }
 892     }
 893 
 894     /**
 895      * Loads and processes the Hijrah calendar properties file for this calendarType.
 896      * The starting Hijrah date and the corresponding ISO date are
 897      * extracted and used to calculate the epochDate offset.
 898      * The version number is identified and ignored.
 899      * Everything else is the data for a year with containing the length of each
 900      * of 12 months.
 901      *
 902      * @throws DateTimeException if initialization of the calendar data from the
 903      *     resource fails
 904      */
 905     private void loadCalendarData() {
 906         try {
 907             String resourceName = calendarProperties.getProperty(PROP_PREFIX + typeId);
 908             Objects.requireNonNull(resourceName, "Resource missing for calendar: " + PROP_PREFIX + typeId);
 909             Properties props = readConfigProperties(resourceName);
 910 
 911             Map<Integer, int[]> years = new HashMap<>();
 912             int minYear = Integer.MAX_VALUE;
 913             int maxYear = Integer.MIN_VALUE;
 914             String id = null;
 915             String type = null;
 916             String version = null;
 917             int isoStart = 0;
 918             for (Map.Entry<Object, Object> entry : props.entrySet()) {
 919                 String key = (String) entry.getKey();
 920                 switch (key) {
 921                     case KEY_ID:
 922                         id = (String)entry.getValue();
 923                         break;
 924                     case KEY_TYPE:
 925                         type = (String)entry.getValue();
 926                         break;
 927                     case KEY_VERSION:
 928                         version = (String)entry.getValue();
 929                         break;
 930                     case KEY_ISO_START: {
 931                         int[] ymd = parseYMD((String) entry.getValue());
 932                         isoStart = (int) LocalDate.of(ymd[0], ymd[1], ymd[2]).toEpochDay();
 933                         break;
 934                     }
 935                     default:
 936                         try {
 937                             // Everything else is either a year or invalid
 938                             int year = Integer.valueOf(key);
 939                             int[] months = parseMonths((String) entry.getValue());
 940                             years.put(year, months);
 941                             maxYear = Math.max(maxYear, year);
 942                             minYear = Math.min(minYear, year);
 943                         } catch (NumberFormatException nfe) {
 944                             throw new IllegalArgumentException("bad key: " + key);
 945                         }
 946                 }
 947             }
 948 
 949             if (!getId().equals(id)) {
 950                 throw new IllegalArgumentException("Configuration is for a different calendar: " + id);
 951             }
 952             if (!getCalendarType().equals(type)) {
 953                 throw new IllegalArgumentException("Configuration is for a different calendar type: " + type);
 954             }
 955             if (version == null || version.isEmpty()) {
 956                 throw new IllegalArgumentException("Configuration does not contain a version");
 957             }
 958             if (isoStart == 0) {
 959                 throw new IllegalArgumentException("Configuration does not contain a ISO start date");
 960             }
 961 
 962             // Now create and validate the array of epochDays indexed by epochMonth
 963             hijrahStartEpochMonth = minYear * 12;
 964             minEpochDay = isoStart;
 965             hijrahEpochMonthStartDays = createEpochMonths(minEpochDay, minYear, maxYear, years);
 966             maxEpochDay = hijrahEpochMonthStartDays[hijrahEpochMonthStartDays.length - 1];
 967 
 968             // Compute the min and max year length in days.
 969             for (int year = minYear; year < maxYear; year++) {
 970                 int length = getYearLength(year);
 971                 minYearLength = Math.min(minYearLength, length);
 972                 maxYearLength = Math.max(maxYearLength, length);
 973             }
 974         } catch (Exception ex) {
 975             // Log error and throw a DateTimeException
 976             PlatformLogger logger = PlatformLogger.getLogger("java.time.chrono");
 977             logger.severe("Unable to initialize Hijrah calendar proxy: " + typeId, ex);
 978             throw new DateTimeException("Unable to initialize HijrahCalendar: " + typeId, ex);
 979         }
 980     }
 981 
 982     /**
 983      * Converts the map of year to month lengths ranging from minYear to maxYear
 984      * into a linear contiguous array of epochDays. The index is the hijrahMonth
 985      * computed from year and month and offset by minYear. The value of each
 986      * entry is the epochDay corresponding to the first day of the month.
 987      *
 988      * @param minYear The minimum year for which data is provided
 989      * @param maxYear The maximum year for which data is provided
 990      * @param years a Map of year to the array of 12 month lengths
 991      * @return array of epochDays for each month from min to max
 992      */
 993     private int[] createEpochMonths(int epochDay, int minYear, int maxYear, Map<Integer, int[]> years) {
 994         // Compute the size for the array of dates
 995         int numMonths = (maxYear - minYear + 1) * 12 + 1;
 996 
 997         // Initialize the running epochDay as the corresponding ISO Epoch day
 998         int epochMonth = 0; // index into array of epochMonths
 999         int[] epochMonths = new int[numMonths];
1000         minMonthLength = Integer.MAX_VALUE;
1001         maxMonthLength = Integer.MIN_VALUE;
1002 
1003         // Only whole years are valid, any zero's in the array are illegal
1004         for (int year = minYear; year <= maxYear; year++) {
1005             int[] months = years.get(year);// must not be gaps
1006             for (int month = 0; month < 12; month++) {
1007                 int length = months[month];
1008                 epochMonths[epochMonth++] = epochDay;
1009 
1010                 if (length < 29 || length > 32) {
1011                     throw new IllegalArgumentException("Invalid month length in year: " + minYear);
1012                 }
1013                 epochDay += length;
1014                 minMonthLength = Math.min(minMonthLength, length);
1015                 maxMonthLength = Math.max(maxMonthLength, length);
1016             }
1017         }
1018 
1019         // Insert the final epochDay
1020         epochMonths[epochMonth++] = epochDay;
1021 
1022         if (epochMonth != epochMonths.length) {
1023             throw new IllegalStateException("Did not fill epochMonths exactly: ndx = " + epochMonth
1024                     + " should be " + epochMonths.length);
1025         }
1026 
1027         return epochMonths;
1028     }
1029 
1030     /**
1031      * Parses the 12 months lengths from a property value for a specific year.
1032      *
1033      * @param line the value of a year property
1034      * @return an array of int[12] containing the 12 month lengths
1035      * @throws IllegalArgumentException if the number of months is not 12
1036      * @throws NumberFormatException if the 12 tokens are not numbers
1037      */
1038     private int[] parseMonths(String line) {
1039         int[] months = new int[12];
1040         String[] numbers = line.split("\\s");
1041         if (numbers.length != 12) {
1042             throw new IllegalArgumentException("wrong number of months on line: " + Arrays.toString(numbers) + "; count: " + numbers.length);
1043         }
1044         for (int i = 0; i < 12; i++) {
1045             try {
1046                 months[i] = Integer.valueOf(numbers[i]);
1047             } catch (NumberFormatException nfe) {
1048                 throw new IllegalArgumentException("bad key: " + numbers[i]);
1049             }
1050         }
1051         return months;
1052     }
1053 
1054     /**
1055      * Parse yyyy-MM-dd into a 3 element array [yyyy, mm, dd].
1056      *
1057      * @param string the input string
1058      * @return the 3 element array with year, month, day
1059      */
1060     private int[] parseYMD(String string) {
1061         // yyyy-MM-dd
1062         string = string.trim();
1063         try {
1064             if (string.charAt(4) != '-' || string.charAt(7) != '-') {
1065                 throw new IllegalArgumentException("date must be yyyy-MM-dd");
1066             }
1067             int[] ymd = new int[3];
1068             ymd[0] = Integer.valueOf(string.substring(0, 4));
1069             ymd[1] = Integer.valueOf(string.substring(5, 7));
1070             ymd[2] = Integer.valueOf(string.substring(8, 10));
1071             return ymd;
1072         } catch (NumberFormatException ex) {
1073             throw new IllegalArgumentException("date must be yyyy-MM-dd", ex);
1074         }
1075     }
1076 }