4745761: (cal) RFE: Support builder for constructing Calendar
authorokutsu
Mon, 21 Jan 2013 12:04:55 +0900
changeset 15280 d9e473e85f3c
parent 15279 1821066bde82
child 15281 dd43cb9be0e1
child 15285 c4e057665d7c
4745761: (cal) RFE: Support builder for constructing Calendar Reviewed-by: peytoia
jdk/src/share/classes/java/util/Calendar.java
jdk/src/share/classes/java/util/GregorianCalendar.java
jdk/src/share/classes/java/util/JapaneseImperialCalendar.java
jdk/test/java/util/Calendar/Builder/BuilderTest.java
jdk/test/java/util/Calendar/CalendarTypeTest.java
--- a/jdk/src/share/classes/java/util/Calendar.java	Sun Jan 20 09:37:51 2013 +0000
+++ b/jdk/src/share/classes/java/util/Calendar.java	Mon Jan 21 12:04:55 2013 +0900
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -151,9 +151,9 @@
  * calendar field values to determine the date and time in the
  * following way.
  *
- * <p>If there is any conflict in calendar field values,
+ * <p><a name="resolution">If there is any conflict in calendar field values,
  * <code>Calendar</code> gives priorities to calendar fields that have been set
- * more recently. The following are the default combinations of the
+ * more recently.</a> The following are the default combinations of the
  * calendar fields. The most recent combination, as determined by the
  * most recently set single field, will be used.
  *
@@ -1020,6 +1020,556 @@
     final static int DST_OFFSET_MASK    = (1 << DST_OFFSET);
 
     /**
+     * {@code Calendar.Builder} is used for creating a {@code Calendar} from
+     * various date-time parameters.
+     *
+     * <p>There are two ways to set a {@code Calendar} to a date-time value. One
+     * is to set the instant parameter to a millisecond offset from the <a
+     * href="Calendar.html#Epoch">Epoch</a>. The other is to set individual
+     * field parameters, such as {@link Calendar#YEAR YEAR}, to their desired
+     * values. These two ways can't be mixed. Trying to set both the instant and
+     * individual fields will cause an {@link IllegalStateException} to be
+     * thrown. However, it is permitted to override previous values of the
+     * instant or field parameters.
+     *
+     * <p>If no enough field parameters are given for determining date and/or
+     * time, calendar specific default values are used when building a
+     * {@code Calendar}. For example, if the {@link Calendar#YEAR YEAR} value
+     * isn't given for the Gregorian calendar, 1970 will be used. If there are
+     * any conflicts among field parameters, the <a
+     * href="Calendar.html#resolution"> resolution rules</a> are applied.
+     * Therefore, the order of field setting matters.
+     *
+     * <p>In addition to the date-time parameters,
+     * the {@linkplain #setLocale(Locale) locale},
+     * {@linkplain #setTimeZone(TimeZone) time zone},
+     * {@linkplain #setWeekDefinition(int, int) week definition}, and
+     * {@linkplain #setLenient(boolean) leniency mode} parameters can be set.
+     *
+     * <p><b>Examples</b>
+     * <p>The following are sample usages. Sample code assumes that the
+     * {@code Calendar} constants are statically imported.
+     *
+     * <p>The following code produces a {@code Calendar} with date 2012-12-31
+     * (Gregorian) because Monday is the first day of a week with the <a
+     * href="GregorianCalendar.html#iso8601_compatible_setting"> ISO 8601
+     * compatible week parameters</a>.
+     * <pre>
+     *   Calendar cal = new Calendar.Builder().setCalendarType("iso8601")
+     *                        .setWeekDate(2013, 1, MONDAY).build();</pre>
+     * <p>The following code produces a Japanese {@code Calendar} with date
+     * 1989-01-08 (Gregorian), assuming that the default {@link Calendar#ERA ERA}
+     * is <em>Heisei</em> that started on that day.
+     * <pre>
+     *   Calendar cal = new Calendar.Builder().setCalendarType("japanese")
+     *                        .setFields(YEAR, 1, DAY_OF_YEAR, 1).build();</pre>
+     *
+     * @since 1.8
+     * @see Calendar#getInstance(TimeZone, Locale)
+     * @see Calendar#fields
+     */
+    public static class Builder {
+        private static final int NFIELDS = FIELD_COUNT + 1; // +1 for WEEK_YEAR
+        private static final int WEEK_YEAR = FIELD_COUNT;
+
+        private long instant;
+        // Calendar.stamp[] (lower half) and Calendar.fields[] (upper half) combined
+        private int[] fields;
+        // Pseudo timestamp starting from MINIMUM_USER_STAMP.
+        // (COMPUTED is used to indicate that the instant has been set.)
+        private int nextStamp;
+        // maxFieldIndex keeps the max index of fields which have been set.
+        // (WEEK_YEAR is never included.)
+        private int maxFieldIndex;
+        private String type;
+        private TimeZone zone;
+        private boolean lenient = true;
+        private Locale locale;
+        private int firstDayOfWeek, minimalDaysInFirstWeek;
+
+        /**
+         * Constructs a {@code Calendar.Builder}.
+         */
+        public Builder() {
+        }
+
+        /**
+         * Sets the instant parameter to the given {@code instant} value that is
+         * a millisecond offset from <a href="Calendar.html#Epoch">the
+         * Epoch</a>.
+         *
+         * @param instant a millisecond offset from the Epoch
+         * @return this {@code Calendar.Builder}
+         * @throws IllegalStateException if any of the field parameters have
+         *                               already been set
+         * @see Calendar#setTime(Date)
+         * @see Calendar#setTimeInMillis(long)
+         * @see Calendar#time
+         */
+        public Builder setInstant(long instant) {
+            if (fields != null) {
+                throw new IllegalStateException();
+            }
+            this.instant = instant;
+            nextStamp = COMPUTED;
+            return this;
+        }
+
+        /**
+         * Sets the instant parameter to the {@code instant} value given by a
+         * {@link Date}. This method is equivalent to a call to
+         * {@link #setInstant(long) setInstant(instant.getTime())}.
+         *
+         * @param instant a {@code Date} representing a millisecond offset from
+         *                the Epoch
+         * @return this {@code Calendar.Builder}
+         * @throws NullPointerException  if {@code instant} is {@code null}
+         * @throws IllegalStateException if any of the field parameters have
+         *                               already been set
+         * @see Calendar#setTime(Date)
+         * @see Calendar#setTimeInMillis(long)
+         * @see Calendar#time
+         */
+        public Builder setInstant(Date instant) {
+            return setInstant(instant.getTime()); // NPE if instant == null
+        }
+
+        /**
+         * Sets the {@code field} parameter to the given {@code value}.
+         * {@code field} is an index to the {@link Calendar#fields}, such as
+         * {@link Calendar#DAY_OF_MONTH DAY_OF_MONTH}. Field value validation is
+         * not performed in this method. Any out of range values are either
+         * normalized in lenient mode or detected as an invalid value in
+         * non-lenient mode when building a {@code Calendar}.
+         *
+         * @param field an index to the {@code Calendar} fields
+         * @param value the field value
+         * @return this {@code Calendar.Builder}
+         * @throws IllegalArgumentException if {@code field} is invalid
+         * @throws IllegalStateException if the instant value has already been set,
+         *                      or if fields have been set too many
+         *                      (approximately {@link Integer#MAX_VALUE}) times.
+         * @see Calendar#set(int, int)
+         */
+        public Builder set(int field, int value) {
+            // Note: WEEK_YEAR can't be set with this method.
+            if (field < 0 || field >= FIELD_COUNT) {
+                throw new IllegalArgumentException("field is invalid");
+            }
+            if (isInstantSet()) {
+                throw new IllegalStateException("instant has been set");
+            }
+            allocateFields();
+            internalSet(field, value);
+            return this;
+        }
+
+        /**
+         * Sets field parameters to their values given by
+         * {@code fieldValuePairs} that are pairs of a field and its value.
+         * For example,
+         * <pre>
+         *   setFeilds(Calendar.YEAR, 2013,
+         *             Calendar.MONTH, Calendar.DECEMBER,
+         *             Calendar.DAY_OF_MONTH, 23);</pre>
+         * is equivalent to the sequence of the following
+         * {@link #set(int, int) set} calls:
+         * <pre>
+         *   set(Calendar.YEAR, 2013)
+         *   .set(Calendar.MONTH, Calendar.DECEMBER)
+         *   .set(Calendar.DAY_OF_MONTH, 23);</pre>
+         *
+         * @param fieldValuePairs field-value pairs
+         * @return this {@code Calendar.Builder}
+         * @throws NullPointerException if {@code fieldValuePairs} is {@code null}
+         * @throws IllegalArgumentException if any of fields are invalid,
+         *             or if {@code fieldValuePairs.length} is an odd number.
+         * @throws IllegalStateException    if the instant value has been set,
+         *             or if fields have been set too many (approximately
+         *             {@link Integer#MAX_VALUE}) times.
+         */
+        public Builder setFields(int... fieldValuePairs) {
+            int len = fieldValuePairs.length;
+            if ((len % 2) != 0) {
+                throw new IllegalArgumentException();
+            }
+            if (isInstantSet()) {
+                throw new IllegalStateException("instant has been set");
+            }
+            if ((nextStamp + len / 2) < 0) {
+                throw new IllegalStateException("stamp counter overflow");
+            }
+            allocateFields();
+            for (int i = 0; i < len; ) {
+                int field = fieldValuePairs[i++];
+                // Note: WEEK_YEAR can't be set with this method.
+                if (field < 0 || field >= FIELD_COUNT) {
+                    throw new IllegalArgumentException("field is invalid");
+                }
+                internalSet(field, fieldValuePairs[i++]);
+            }
+            return this;
+        }
+
+        /**
+         * Sets the date field parameters to the values given by {@code year},
+         * {@code month}, and {@code dayOfMonth}. This method is equivalent to
+         * a call to:
+         * <pre>
+         *   setFields(Calendar.YEAR, year,
+         *             Calendar.MONTH, month,
+         *             Calendar.DAY_OF_MONTH, dayOfMonth);</pre>
+         *
+         * @param year       the {@link Calendar#YEAR YEAR} value
+         * @param month      the {@link Calendar#MONTH MONTH} value
+         *                   (the month numbering is <em>0-based</em>).
+         * @param dayOfMonth the {@link Calendar#DAY_OF_MONTH DAY_OF_MONTH} value
+         * @return this {@code Calendar.Builder}
+         */
+        public Builder setDate(int year, int month, int dayOfMonth) {
+            return setFields(YEAR, year, MONTH, month, DAY_OF_MONTH, dayOfMonth);
+        }
+
+        /**
+         * Sets the time of day field parameters to the values given by
+         * {@code hourOfDay}, {@code minute}, and {@code second}. This method is
+         * equivalent to a call to:
+         * <pre>
+         *   setTimeOfDay(hourOfDay, minute, second, 0);</pre>
+         *
+         * @param hourOfDay the {@link Calendar#HOUR_OF_DAY HOUR_OF_DAY} value
+         *                  (24-hour clock)
+         * @param minute    the {@link Calendar#MINUTE MINUTE} value
+         * @param second    the {@link Calendar#SECOND SECOND} value
+         * @return this {@code Calendar.Builder}
+         */
+        public Builder setTimeOfDay(int hourOfDay, int minute, int second) {
+            return setTimeOfDay(hourOfDay, minute, second, 0);
+        }
+
+        /**
+         * Sets the time of day field parameters to the values given by
+         * {@code hourOfDay}, {@code minute}, {@code second}, and
+         * {@code millis}. This method is equivalent to a call to:
+         * <pre>
+         *   setFields(Calendar.HOUR_OF_DAY, hourOfDay,
+         *             Calendar.MINUTE, minute,
+         *             Calendar.SECOND, second,
+         *             Calendar.MILLISECOND, millis);</pre>
+         *
+         * @param hourOfDay the {@link Calendar#HOUR_OF_DAY HOUR_OF_DAY} value
+         *                  (24-hour clock)
+         * @param minute    the {@link Calendar#MINUTE MINUTE} value
+         * @param second    the {@link Calendar#SECOND SECOND} value
+         * @param millis    the {@link Calendar#MILLISECOND MILLISECOND} value
+         * @return this {@code Calendar.Builder}
+         */
+        public Builder setTimeOfDay(int hourOfDay, int minute, int second, int millis) {
+            return setFields(HOUR_OF_DAY, hourOfDay, MINUTE, minute,
+                             SECOND, second, MILLISECOND, millis);
+        }
+
+        /**
+         * Sets the week-based date parameters to the values with the given
+         * date specifiers - week year, week of year, and day of week.
+         *
+         * <p>If the specified calendar doesn't support week dates, the
+         * {@link #build() build} method will throw an {@link IllegalArgumentException}.
+         *
+         * @param weekYear   the week year
+         * @param weekOfYear the week number based on {@code weekYear}
+         * @param dayOfWeek  the day of week value: one of the constants
+         *     for the {@link Calendar#DAY_OF_WEEK DAY_OF_WEEK} field:
+         *     {@link Calendar#SUNDAY SUNDAY}, ..., {@link Calendar#SATURDAY SATURDAY}.
+         * @return this {@code Calendar.Builder}
+         * @see Calendar#setWeekDate(int, int, int)
+         * @see Calendar#isWeekDateSupported()
+         */
+        public Builder setWeekDate(int weekYear, int weekOfYear, int dayOfWeek) {
+            allocateFields();
+            internalSet(WEEK_YEAR, weekYear);
+            internalSet(WEEK_OF_YEAR, weekOfYear);
+            internalSet(DAY_OF_WEEK, dayOfWeek);
+            return this;
+        }
+
+        /**
+         * Sets the time zone parameter to the given {@code zone}. If no time
+         * zone parameter is given to this {@code Caledar.Builder}, the
+         * {@linkplain TimeZone#getDefault() default
+         * <code>TimeZone</code>} will be used in the {@link #build() build}
+         * method.
+         *
+         * @param zone the {@link TimeZone}
+         * @return this {@code Calendar.Builder}
+         * @throws NullPointerException if {@code zone} is {@code null}
+         * @see Calendar#setTimeZone(TimeZone)
+         */
+        public Builder setTimeZone(TimeZone zone) {
+            if (zone == null) {
+                throw new NullPointerException();
+            }
+            this.zone = zone;
+            return this;
+        }
+
+        /**
+         * Sets the lenient mode parameter to the value given by {@code lenient}.
+         * If no lenient parameter is given to this {@code Calendar.Builder},
+         * lenient mode will be used in the {@link #build() build} method.
+         *
+         * @param lenient {@code true} for lenient mode;
+         *                {@code false} for non-lenient mode
+         * @return this {@code Calendar.Builder}
+         * @see Calendar#setLenient(boolean)
+         */
+        public Builder setLenient(boolean lenient) {
+            this.lenient = lenient;
+            return this;
+        }
+
+        /**
+         * Sets the calendar type parameter to the given {@code type}. The
+         * calendar type given by this method has precedence over any explicit
+         * or implicit calendar type given by the
+         * {@linkplain #setLocale(Locale) locale}.
+         *
+         * <p>In addition to the available calendar types returned by the
+         * {@link Calendar#getAvailableCalendarTypes() Calendar.getAvailableCalendarTypes}
+         * method, {@code "gregorian"} and {@code "iso8601"} as aliases of
+         * {@code "gregory"} can be used with this method.
+         *
+         * @param type the calendar type
+         * @return this {@code Calendar.Builder}
+         * @throws NullPointerException if {@code type} is {@code null}
+         * @throws IllegalArgumentException if {@code type} is unknown
+         * @throws IllegalStateException if another calendar type has already been set
+         * @see Calendar#getCalendarType()
+         * @see Calendar#getAvailableCalendarTypes()
+         */
+        public Builder setCalendarType(String type) {
+            if (type.equals("gregorian")) { // NPE if type == null
+                type = "gregory";
+            }
+            if (!Calendar.getAvailableCalendarTypes().contains(type)
+                    && !type.equals("iso8601")) {
+                throw new IllegalArgumentException("unknown calendar type: " + type);
+            }
+            if (this.type == null) {
+                this.type = type;
+            } else {
+                if (!this.type.equals(type)) {
+                    throw new IllegalStateException("calendar type override");
+                }
+            }
+            return this;
+        }
+
+        /**
+         * Sets the locale parameter to the given {@code locale}. If no locale
+         * is given to this {@code Calendar.Builder}, the {@linkplain
+         * Locale#getDefault(Locale.Category) default <code>Locale</code>}
+         * for {@link Locale.Category#FORMAT} will be used.
+         *
+         * <p>If no calendar type is explicitly given by a call to the
+         * {@link #setCalendarType(String) setCalendarType} method,
+         * the {@code Locale} value is used to determine what type of
+         * {@code Calendar} to be built.
+         *
+         * <p>If no week definition parameters are explicitly given by a call to
+         * the {@link #setWeekDefinition(int,int) setWeekDefinition} method, the
+         * {@code Locale}'s default values are used.
+         *
+         * @param locale the {@link Locale}
+         * @throws NullPointerException if {@code locale} is {@code null}
+         * @return this {@code Calendar.Builder}
+         * @see Calendar#getInstance(Locale)
+         */
+        public Builder setLocale(Locale locale) {
+            if (locale == null) {
+                throw new NullPointerException();
+            }
+            this.locale = locale;
+            return this;
+        }
+
+        /**
+         * Sets the week definition parameters to the values given by
+         * {@code firstDayOfWeek} and {@code minimalDaysInFirstWeek} that are
+         * used to determine the <a href="Calendar.html#First_Week">first
+         * week</a> of a year. The parameters given by this method have
+         * precedence over the default values given by the
+         * {@linkplain #setLocale(Locale) locale}.
+         *
+         * @param firstDayOfWeek the first day of a week; one of
+         *                       {@link Calendar#SUNDAY} to {@link Calendar#SATURDAY}
+         * @param minimalDaysInFirstWeek the minimal number of days in the first
+         *                               week (1..7)
+         * @return this {@code Calendar.Builder}
+         * @throws IllegalArgumentException if {@code firstDayOfWeek} or
+         *                                  {@code minimalDaysInFirstWeek} is invalid
+         * @see Calendar#getFirstDayOfWeek()
+         * @see Calendar#getMinimalDaysInFirstWeek()
+         */
+        public Builder setWeekDefinition(int firstDayOfWeek, int minimalDaysInFirstWeek) {
+            if (!isValidWeekParameter(firstDayOfWeek)
+                    || !isValidWeekParameter(minimalDaysInFirstWeek)) {
+                throw new IllegalArgumentException();
+            }
+            this.firstDayOfWeek = firstDayOfWeek;
+            this.minimalDaysInFirstWeek = minimalDaysInFirstWeek;
+            return this;
+        }
+
+        /**
+         * Returns a {@code Calendar} built from the parameters set by the
+         * setter methods. The calendar type given by the {@link #setCalendarType(String)
+         * setCalendarType} method or the {@linkplain #setLocale(Locale) locale} is
+         * used to determine what {@code Calendar} to be created. If no explicit
+         * calendar type is given, the locale's default calendar is created.
+         *
+         * <p>If the calendar type is {@code "iso8601"}, the
+         * {@linkplain GregorianCalendar#setGregorianChange(Date) Gregorian change date}
+         * of a {@link GregorianCalendar} is set to {@code Date(Long.MIN_VALUE)}
+         * to be the <em>proleptic</em> Gregorian calendar. Its week definition
+         * parameters are also set to be <a
+         * href="GregorianCalendar.html#iso8601_compatible_setting">compatible
+         * with the ISO 8601 standard</a>. Note that the
+         * {@link GregorianCalendar#getCalendarType() getCalendarType} method of
+         * a {@code GregorianCalendar} created with {@code "iso8601"} returns
+         * {@code "gregory"}.
+         *
+         * <p>The default values are used for locale and time zone if these
+         * parameters haven't been given explicitly.
+         *
+         * <p>Any out of range field values are either normalized in lenient
+         * mode or detected as an invalid value in non-lenient mode.
+         *
+         * @return a {@code Calendar} built with parameters of this {@code
+         *         Calendar.Builder}
+         * @throws IllegalArgumentException if the calendar type is unknown, or
+         *             if any invalid field values are given in non-lenient mode, or
+         *             if a week date is given for the calendar type that doesn't
+         *             support week dates.
+         * @see Calendar#getInstance(TimeZone, Locale)
+         * @see Locale#getDefault(Locale.Category)
+         * @see TimeZone#getDefault()
+         */
+        public Calendar build() {
+            if (locale == null) {
+                locale = Locale.getDefault();
+            }
+            if (zone == null) {
+                zone = TimeZone.getDefault();
+            }
+            Calendar cal;
+            if (type == null) {
+                type = locale.getUnicodeLocaleType("ca");
+            }
+            if (type == null) {
+                if (locale.getCountry() == "TH"
+                    && locale.getLanguage() == "th") {
+                    type = "buddhist";
+                } else {
+                    type = "gregory";
+                }
+            }
+            switch (type) {
+            case "gregory":
+                cal = new GregorianCalendar(zone, locale, true);
+                break;
+            case "iso8601":
+                GregorianCalendar gcal = new GregorianCalendar(zone, locale, true);
+                // make gcal a proleptic Gregorian
+                gcal.setGregorianChange(new Date(Long.MIN_VALUE));
+                // and week definition to be compatible with ISO 8601
+                setWeekDefinition(MONDAY, 4);
+                cal = gcal;
+                break;
+            case "buddhist":
+                cal = new BuddhistCalendar(zone, locale);
+                cal.clear();
+                break;
+            case "japanese":
+                cal = new JapaneseImperialCalendar(zone, locale, true);
+                break;
+            default:
+                throw new IllegalArgumentException("unknown calendar type: " + type);
+            }
+            cal.setLenient(lenient);
+            if (firstDayOfWeek != 0) {
+                cal.setFirstDayOfWeek(firstDayOfWeek);
+                cal.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);
+            }
+            if (isInstantSet()) {
+                cal.setTimeInMillis(instant);
+                cal.complete();
+                return cal;
+            }
+
+            if (fields != null) {
+                boolean weekDate = isSet(WEEK_YEAR)
+                                       && fields[WEEK_YEAR] > fields[YEAR];
+                if (weekDate && !cal.isWeekDateSupported()) {
+                    throw new IllegalArgumentException("week date is unsupported by " + type);
+                }
+
+                // Set the fields from the min stamp to the max stamp so that
+                // the fields resolution works in the Calendar.
+                for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
+                    for (int index = 0; index <= maxFieldIndex; index++) {
+                        if (fields[index] == stamp) {
+                            cal.set(index, fields[NFIELDS + index]);
+                            break;
+                        }
+                    }
+                }
+
+                if (weekDate) {
+                    int weekOfYear = isSet(WEEK_OF_YEAR) ? fields[NFIELDS + WEEK_OF_YEAR] : 1;
+                    int dayOfWeek = isSet(DAY_OF_WEEK)
+                                    ? fields[NFIELDS + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
+                    cal.setWeekDate(fields[NFIELDS + WEEK_YEAR], weekOfYear, dayOfWeek);
+                }
+                cal.complete();
+            }
+
+            return cal;
+        }
+
+        private void allocateFields() {
+            if (fields == null) {
+                fields = new int[NFIELDS * 2];
+                nextStamp = MINIMUM_USER_STAMP;
+                maxFieldIndex = -1;
+            }
+        }
+
+        private void internalSet(int field, int value) {
+            fields[field] = nextStamp++;
+            if (nextStamp < 0) {
+                throw new IllegalStateException("stamp counter overflow");
+            }
+            fields[NFIELDS + field] = value;
+            if (field > maxFieldIndex && field < WEEK_YEAR) {
+                maxFieldIndex = field;
+            }
+        }
+
+        private boolean isInstantSet() {
+            return nextStamp == COMPUTED;
+        }
+
+        private boolean isSet(int index) {
+            return fields != null && fields[index] > UNSET;
+        }
+
+        private boolean isValidWeekParameter(int value) {
+            return value > 0 && value <= 7;
+        }
+    }
+
+    /**
      * Constructs a Calendar with the default time zone
      * and locale.
      * @see     TimeZone#getDefault
@@ -1109,7 +1659,7 @@
         Calendar cal = null;
 
         if (aLocale.hasExtensions()) {
-        String caltype = aLocale.getUnicodeLocaleType("ca");
+            String caltype = aLocale.getUnicodeLocaleType("ca");
             if (caltype != null) {
                 switch (caltype) {
                 case "buddhist":
@@ -2002,6 +2552,38 @@
     }
 
     /**
+     * Returns an unmodifiable {@code Set} containing all calendar types
+     * supported by {@code Calendar} in the runtime environment. The available
+     * calendar types can be used for the <a
+     * href="Locale.html#def_locale_extension">Unicode locale extensions</a>.
+     * The {@code Set} returned contains at least {@code "gregory"}. The
+     * calendar types don't include aliases, such as {@code "gregorian"} for
+     * {@code "gregory"}.
+     *
+     * @return an unmodifiable {@code Set} containing all available calendar types
+     * @since 1.8
+     * @see #getCalendarType()
+     * @see Calendar.Builder#setCalendarType(String)
+     * @see Locale#getUnicodeLocaleType(String)
+     */
+    public static Set<String> getAvailableCalendarTypes() {
+        return AvailableCalendarTypes.SET;
+    }
+
+    private static class AvailableCalendarTypes {
+        private static final Set<String> SET;
+        static {
+            Set<String> set = new HashSet<>(3);
+            set.add("gregory");
+            set.add("buddhist");
+            set.add("japanese");
+            SET = Collections.unmodifiableSet(set);
+        }
+        private AvailableCalendarTypes() {
+        }
+    }
+
+    /**
      * Returns the calendar type of this {@code Calendar}. Calendar types are
      * defined by the <em>Unicode Locale Data Markup Language (LDML)</em>
      * specification.
--- a/jdk/src/share/classes/java/util/GregorianCalendar.java	Sun Jan 20 09:37:51 2013 +0000
+++ b/jdk/src/share/classes/java/util/GregorianCalendar.java	Mon Jan 21 12:04:55 2013 +0900
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -40,7 +40,6 @@
 
 import java.io.IOException;
 import java.io.ObjectInputStream;
-import sun.util.locale.provider.CalendarDataUtility;
 import sun.util.calendar.BaseCalendar;
 import sun.util.calendar.CalendarDate;
 import sun.util.calendar.CalendarSystem;
@@ -722,6 +721,18 @@
         this.internalSet(MILLISECOND, millis);
     }
 
+    /**
+     * Constructs an empty GregorianCalendar.
+     *
+     * @param zone    the given time zone
+     * @param aLocale the given locale
+     * @param flag    the flag requesting an empty instance
+     */
+    GregorianCalendar(TimeZone zone, Locale locale, boolean flag) {
+        super(zone, locale);
+        gdate = (BaseCalendar.Date) gcal.newCalendarDate(getZone());
+    }
+
 /////////////////
 // Public methods
 /////////////////
--- a/jdk/src/share/classes/java/util/JapaneseImperialCalendar.java	Sun Jan 20 09:37:51 2013 +0000
+++ b/jdk/src/share/classes/java/util/JapaneseImperialCalendar.java	Mon Jan 21 12:04:55 2013 +0900
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2005, 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -302,6 +302,18 @@
     }
 
     /**
+     * Constructs an "empty" {@code JapaneseImperialCalendar}.
+     *
+     * @param zone    the given time zone
+     * @param aLocale the given locale
+     * @param flag    the flag requesting an empty instance
+     */
+    JapaneseImperialCalendar(TimeZone zone, Locale aLocale, boolean flag) {
+        super(zone, aLocale);
+        jdate = jcal.newCalendarDate(zone);
+    }
+
+    /**
      * Returns {@code "japanese"} as the calendar type of this {@code
      * JapaneseImperialCalendar}.
      *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/java/util/Calendar/Builder/BuilderTest.java	Mon Jan 21 12:04:55 2013 +0900
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 4745761
+ * @summary Unit test for Calendar.Builder.
+ */
+
+import java.util.*;
+import static java.util.Calendar.*;
+
+public class BuilderTest {
+    private static final Locale jaJPJP = new Locale("ja", "JP", "JP");
+    private static final Locale thTH = new Locale("th", "TH");
+    private static final TimeZone LA = TimeZone.getTimeZone("America/Los_Angeles");
+    private static final TimeZone TOKYO = TimeZone.getTimeZone("Asia/Tokyo");
+    private static int error;
+
+    public static void main(String[] args) {
+        TimeZone tz = TimeZone.getDefault();
+        Locale loc = Locale.getDefault();
+        try {
+            TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
+            Locale.setDefault(Locale.US);
+            Calendar.Builder calb;
+            Calendar cal, expected;
+
+            // set instant
+            calb = builder();
+            long time = System.currentTimeMillis();
+            cal = calb.setInstant(time).build();
+            expected = new GregorianCalendar();
+            expected.setTimeInMillis(time);
+            check(cal, expected);
+
+            calb = builder();
+            cal = calb.setInstant(new Date(time)).build();
+            check(cal, expected);
+
+            // set time zone
+            calb = builder();
+            cal = calb.setTimeZone(LA).setInstant(time).build();
+            expected = new GregorianCalendar(LA, Locale.US);
+            expected.setTimeInMillis(time);
+            check(cal, expected);
+
+            calb = builder();
+            cal = calb.setTimeZone(TOKYO).setInstant(time).build();
+            expected = new GregorianCalendar(TOKYO, Locale.US);
+            expected.setTimeInMillis(time);
+            check(cal, expected);
+
+            // set vs. setFields
+            calb = builder();
+            cal = calb.set(YEAR, 2013).set(MONTH, JANUARY).set(DAY_OF_MONTH, 31)
+                      .set(HOUR_OF_DAY, 10).set(MINUTE, 20).set(SECOND, 30).set(MILLISECOND, 40).build();
+            expected = new GregorianCalendar(2013, JANUARY, 31, 10, 20, 30);
+            expected.set(MILLISECOND, 40);
+            check(cal, expected);
+
+            calb = builder();
+            cal = calb.setFields(YEAR, 2013, MONTH, JANUARY, DAY_OF_MONTH, 31,
+                                 HOUR_OF_DAY, 10, MINUTE, 20, SECOND, 30, MILLISECOND, 40).build();
+            check(cal, expected);
+
+            // field resolution
+            calb = builder();
+            cal = calb.setFields(YEAR, 2013, MONTH, DECEMBER, DAY_OF_MONTH, 31,
+                                 HOUR_OF_DAY, 10, MINUTE, 20, SECOND, 30, MILLISECOND, 40)
+                      .set(DAY_OF_YEAR, 31).build();  // DAY_OF_YEAR wins.
+            check(cal, expected);
+
+            // setDate/setTimeOfDay
+            calb = builder();
+            cal = calb.setDate(2013, JANUARY, 31).setTimeOfDay(10, 20, 30, 40).build();
+            check(cal, expected);
+
+            // week date (ISO 8601)
+            calb = builder().setCalendarType("iso8601");
+            cal = calb.setWeekDate(2013, 1, MONDAY).setTimeOfDay(10, 20, 30).build();
+            expected = getISO8601();
+            expected.set(2012, DECEMBER, 31, 10, 20, 30);
+            check(cal, expected);
+
+            // default YEAR == 1970
+            cal = builder().setFields(MONTH, JANUARY,
+                                                   DAY_OF_MONTH, 9).build();
+            check(cal, new GregorianCalendar(1970, JANUARY, 9));
+
+            // no parameters are given.
+            calb = builder();
+            cal = calb.build();
+            expected = new GregorianCalendar();
+            expected.clear();
+            check(cal, expected);
+
+            // Thai Buddhist calendar
+            calb = builder();
+            cal = calb.setCalendarType("buddhist").setDate(2556, JANUARY, 31).build();
+            expected = Calendar.getInstance(thTH);
+            expected.clear();
+            expected.set(2556, JANUARY, 31);
+            check(cal, expected);
+            // setLocale
+            calb = builder();
+            cal = calb.setLocale(thTH).setDate(2556, JANUARY, 31).build();
+            check(cal, expected);
+
+            // Japanese Imperial calendar
+            cal = builder().setCalendarType("japanese")
+                .setFields(YEAR, 1, DAY_OF_YEAR, 1).build();
+            expected = Calendar.getInstance(jaJPJP);
+            expected.clear();
+            expected.set(1, JANUARY, 8);
+            check(cal, expected);
+            // setLocale
+            calb = builder();
+            cal = calb.setLocale(jaJPJP).setFields(YEAR, 1, DAY_OF_YEAR, 1).build();
+            check(cal, expected);
+
+            testExceptions();
+        } finally {
+            // Restore default Locale and TimeZone
+            Locale.setDefault(loc);
+            TimeZone.setDefault(tz);
+        }
+        if (error > 0) {
+            throw new RuntimeException("Failed");
+        }
+    }
+
+    private static void testExceptions() {
+        Calendar.Builder calb;
+        Calendar cal;
+
+        // NPE
+        try {
+            calb = builder().setInstant((Date)null);
+            noException("setInstant((Date)null)");
+        } catch (NullPointerException npe) {
+        }
+        try {
+            calb = builder().setCalendarType(null);
+            noException("setCalendarType(null)");
+        } catch (NullPointerException npe) {
+        }
+        try {
+            calb = builder().setLocale(null);
+            noException("setLocale(null)");
+        } catch (NullPointerException npe) {
+        }
+        try {
+            calb = builder().setTimeZone(null);
+            noException("setTimeZone(null)");
+        } catch (NullPointerException npe) {
+        }
+
+        // IllegalArgumentException
+        try {
+            // invalid field index in set
+            calb = builder().set(100, 2013);
+            noException("set(100, 2013)");
+        } catch (IllegalArgumentException e) {
+        }
+        try {
+            // invalid field index in setField
+            calb = builder().setFields(100, 2013);
+            noException("setFields(100, 2013)");
+        } catch (IllegalArgumentException e) {
+        }
+        try {
+            // odd number of arguments
+            calb = builder().setFields(YEAR, 2013, MONTH);
+            noException("setFields(YEAR, 2013, MONTH)");
+        } catch (IllegalArgumentException e) {
+        }
+        try {
+            // unknown calendar type
+            calb = builder().setCalendarType("foo");
+            noException("setCalendarType(\"foo\")");
+        } catch (IllegalArgumentException e) {
+        }
+        try {
+            // invalid week definition parameter
+            calb = builder().setWeekDefinition(8, 1);
+            noException("setWeekDefinition(8, 1)");
+        } catch (IllegalArgumentException e) {
+        }
+        try {
+            // invalid week definition parameter
+            calb = builder().setWeekDefinition(SUNDAY, 0);
+            noException("setWeekDefinition(8, 1)");
+        } catch (IllegalArgumentException e) {
+        }
+
+        try {
+            // sets both instant and field parameters
+            calb = builder().setInstant(new Date()).setDate(2013, JANUARY, 1);
+            noException("setInstant(new Date()).setDate(2013, JANUARY, 1)");
+        } catch (IllegalStateException e) {
+        }
+        try {
+            // sets both field parameters and instant
+            calb = builder().setDate(2013, JANUARY, 1).setInstant(new Date());
+            noException("setDate(2013, JANUARY, 1).setInstant(new Date())");
+        } catch (IllegalStateException e) {
+        }
+        try {
+            // sets inconsistent calendar types
+            calb = builder().setCalendarType("iso8601").setCalendarType("japanese");
+            noException("setCalendarType(\"iso8601\").setCalendarType(\"japanese\")");
+        } catch (IllegalStateException e) {
+        }
+
+        // IllegalArgumentException in build()
+        calb = nonLenientBuilder().set(MONTH, 100);
+        checkException(calb, IllegalArgumentException.class);
+        calb = nonLenientBuilder().setTimeOfDay(23, 59, 70);
+        checkException(calb, IllegalArgumentException.class);
+        calb = builder().setCalendarType("japanese").setWeekDate(2013, 1, MONDAY);
+        checkException(calb, IllegalArgumentException.class);
+    }
+
+    private static Calendar.Builder builder() {
+        return new Calendar.Builder();
+    }
+
+    private static Calendar.Builder nonLenientBuilder() {
+        return builder().setLenient(false);
+    }
+
+    private static Calendar getISO8601() {
+        GregorianCalendar cal = new GregorianCalendar();
+        cal.setFirstDayOfWeek(MONDAY);
+        cal.setMinimalDaysInFirstWeek(4);
+        cal.setGregorianChange(new Date(Long.MIN_VALUE));
+        cal.clear();
+        return cal;
+    }
+
+    private static void check(Calendar cal, Calendar expected) {
+        if (!cal.equals(expected)) {
+            error++;
+            System.err.println("FAILED:");
+            System.err.println("\t     cal = "+cal.getTime());
+            System.err.println("\texpected = "+expected.getTime());
+            System.err.printf("\tcal = %s%n\texp = %s%n", cal, expected);
+        }
+    }
+
+    private static void checkException(Calendar.Builder calb, Class<? extends Exception> exception) {
+        try {
+            Calendar cal = calb.build();
+            error++;
+            System.err.println("expected exception: " + exception);
+        } catch (Exception e) {
+            if (!e.getClass().equals(exception)) {
+                error++;
+                System.err.println("unexpected exception: " + e.getClass() + ", expected: " + exception);
+            }
+        }
+    }
+
+    private static void noException(String msg) {
+        error++;
+        System.err.println("no exception with "+msg);
+    }
+}
--- a/jdk/test/java/util/Calendar/CalendarTypeTest.java	Sun Jan 20 09:37:51 2013 +0000
+++ b/jdk/test/java/util/Calendar/CalendarTypeTest.java	Mon Jan 21 12:04:55 2013 +0900
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug 7151414
+ * @bug 7151414 4745761
  * @summary Unit test for calendar types
  */
 
@@ -39,20 +39,21 @@
         new Locale("ja", "JP", "JP"),
         Locale.forLanguageTag("en-US-u-ca-japanese"),
     };
-    static String[] types = new String[] {
-        "gregory",
+    static final String[] TYPES = new String[] {
         "gregory",
         "buddhist",
-        "buddhist",
         "japanese",
-        "japanese",
+    };
+    static final String[] ALIASES = new String[] {
+        "gregorian",
+        "iso8601",
     };
 
     public static void main(String[] args) {
         for (int i = 0; i < locales.length; i++) {
             Calendar cal = Calendar.getInstance(locales[i]);
             String type = cal.getCalendarType();
-            checkValue("bad calendar type", type, types[i]);
+            checkValue("bad calendar type", type, TYPES[i/2]);
         }
 
         GregorianCalendar gcal = new GregorianCalendar();
@@ -63,6 +64,21 @@
 
         Calendar k = new Koyomi();
         checkValue("bad class name", k.getCalendarType(), k.getClass().getName());
+
+        Set<String> types = Calendar.getAvailableCalendarTypes();
+        if (types.size() != 3) {
+            throw new RuntimeException("size != 3");
+        }
+        for (String s : TYPES) {
+            if (!types.contains(s)) {
+                throw new RuntimeException(s + " not contained");
+            }
+        }
+        for (String s : ALIASES) {
+            if (types.contains(s)) {
+                throw new RuntimeException("alias " + s + " contained");
+            }
+        }
     }
 
     private static void checkValue(String msg, String got, String expected) {