/** * Copyright (c) 2009--2012 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package com.redhat.rhn.common.util; import org.apache.struts.action.DynaActionForm; import java.text.DateFormat; import java.text.DateFormatSymbols; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.TimeZone; /** * A bean to support date picking in the UI. To add a date picker to a form, * add inputs for the year, day, month etc. to the form, and name them with * a common prefix; to support a date picker with name <code>date</code>, * you would add inputs with names <code>date_year, date_month, date_day, * date_hour, date_minute, and date_am_pm</code> to the form. All this form * variables need to be declared as type <code>java.util.Integer</code> * * <p> In your Struts action, you can initialize the form fields with * <pre> * Date d = ...; * DynaActionForm dynaForm = ...; * DatePicker p = new DatePicker("date", timeZone, locale, yearDirection); * p.setDate(d); * p.writeForm(dynaForm.getMap()); * </pre> * * <p> Once the form is submitted, you can extract the date with * <pre> * DynaActionForm dynaForm = ...; * DatePicker p = new DatePicker("date", timeZone, locale, yearDirection); * p.readForm(dynaForm.getMap()); * Date result = p.getDate(); * if ( result == null ) { * ... tell user that date was incorrect ... * } * </pre> * * @version $Rev$ */ public class DatePicker { /** * Typical form field when dealing with a date picker. * @see com.redhat.rhn.frontend.struts.StrutsDelegate */ public static final String USE_DATE = "use_date"; // // Names of the subfields for the date picker // public static final String YEAR = "year"; public static final String MONTH = "month"; public static final String DAY = "day"; public static final String HOUR = "hour"; public static final String MINUTE = "minute"; public static final String AM_PM = "am_pm"; public static final int YEAR_RANGE_POSITIVE = 0; public static final int YEAR_RANGE_NEGATIVE = 1; private static final int YEAR_RANGE_SIZE = 5; private static final Map FIELD_CALENDAR_MAP = new HashMap(); static { FIELD_CALENDAR_MAP.put(Boolean.TRUE, makeFieldCalendarMap(true)); FIELD_CALENDAR_MAP.put(Boolean.FALSE, makeFieldCalendarMap(false)); } private String name; private Calendar cal; private Locale locale; private boolean isLatin; private boolean isDayBeforeMonth; private DateFormatSymbols dateFormatSymbols; private int currentYear; private int yearRangeDirection; private boolean disableTime; private boolean disableDate; /** * Create a new date picker that extracts fields prefixed with * <code>name0 + "_"</code> and works with the given time zone * and locale. * * @param name0 the prefix for the subfields for the date picker * @param tz the timezone in which values are to be interpreted * @param locale0 the locale to use * @param yearRangeDirection0 direction of the year range to use. * YEAR_RANGE_POSATIVE means the year selection will go from now * until YEAR_RANGE_SIZE in the future (2005-2010). YEAR_RANGE_NEGATIVE * will include a range from now until YEAR_RANGE_SIZE in the * past (2000 - 2005) */ public DatePicker(String name0, TimeZone tz, Locale locale0, int yearRangeDirection0) { name = name0; locale = locale0; cal = new GregorianCalendar(tz, locale0); cal.setLenient(false); currentYear = cal.get(Calendar.YEAR); analyzeDateFormat(); yearRangeDirection = yearRangeDirection0; disableTime = false; disableDate = false; } /** * Create a new date picker that extracts fields prefixed with * <code>name0 + "_"</code> and works with the given locale. * * @param name0 the prefix for the subfields for the date picker * @param locale0 the locale to use * @param yearRangeDirection0 direction of the year range to use. * YEAR_RANGE_POSATIVE means the year selection will go from now * until YEAR_RANGE_SIZE in the future (2005-2010). YEAR_RANGE_NEGATIVE * will include a range from now until YEAR_RANGE_SIZE in the * past (2000 - 2005) */ public DatePicker(String name0, Locale locale0, int yearRangeDirection0) { name = name0; locale = locale0; cal = new GregorianCalendar(locale0); cal.setLenient(false); currentYear = cal.get(Calendar.YEAR); analyzeDateFormat(); yearRangeDirection = yearRangeDirection0; disableTime = true; disableDate = false; } /** * Return <code>true</code> if the locale uses 12 hour time formats * with the additional am/pm designation (like certain anglo-saxon countries). * A return value of <code>false</code> indicates that the locale uses * a 24 hour clock. * * @return <code>true</code> if the locale uses 12 hour times with am/pm, * <code>false</code> if it uses a 24 hour clock. */ public boolean isLatin() { return isLatin; } /** * Return <code>true</code> if in the given locale the day * is written before the month, <false> otherwise. * @return <code>true</code> if in the given locale the day * is written before the month, <false> otherwise. */ public boolean isDayBeforeMonth() { return isDayBeforeMonth; } /** * Return the name of this picker. * * @return the name of this picker */ public String getName() { return name; } /** * Return the month, a number from 0 to 12. * @return the month, a number from 0 to 12. */ public Integer getMonth() { return getField(MONTH); } /** * Return the day, a number from 1 to 31. * @return the day, a number from 1 to 31. */ public Integer getDay() { return getField(DAY); } /** * Return the year * @return the year */ public Integer getYear() { return getField(YEAR); } /** * Return the hour, a number between 1 and 12 if {@link #isLatin} * is <code>true</code>, and a number from 0 to 23 otherwise. * @return the hour, a number between 1 and 12 if {@link #isLatin} * is <code>true</code>, and a number from 0 to 23 otherwise. */ public Integer getHour() { return getField(HOUR); } /** * @return The hour in 24 hour format (0 to 23) */ public Integer getHourOfDay() { return cal.get(Calendar.HOUR_OF_DAY); } /** * Return the minute, a number from 0 to 59. * @return the minute, a number from 0 to 59. */ public Integer getMinute() { return getField(MINUTE); } /** * Return <code>0</code> to indicate AM and <code>1</code> * to indicate PM. * @return <code>0</code> to indicate AM and <code>1</code> * to indicate PM. */ public Integer getAmPm() { return getField(AM_PM); } /** * Set the month. * @param v the month, a number from 0 to 11 */ public void setMonth(Integer v) { setField(MONTH, v); } /** * Set the day. * @param v the day, a number from 1 to 31 */ public void setDay(Integer v) { setField(DAY, v); } /** * Set the year * @param v the year */ public void setYear(Integer v) { setField(YEAR, v); } /** * Set the hour * @param v the hour * @see #getHour */ public void setHour(Integer v) { setField(HOUR, v); } /** * set the hour of the day * * This also sets the am/pm flag * @param v the hour (0 to 23) */ public void setHourOfDay(Integer v) { cal.set(Calendar.HOUR_OF_DAY, v); } /** * Set the minute * @param v the minute, a number from 0 to 59 */ public void setMinute(Integer v) { setField(MINUTE, v); } /** * Set am or pm * @param v <code>0</code> to indicate AM and <code>1</code> * to indicate PM. */ public void setAmPm(Integer v) { setField(AM_PM, v); } /** * Get a list of years for display. The list starts with the current * year and descends <code>YEAR_RANGE_SIZE</code> years down. * @return a list of years for display */ public int[] getYearRange() { int[] result = new int[YEAR_RANGE_SIZE]; for (int i = 0; i < result.length; i++) { if (yearRangeDirection == YEAR_RANGE_NEGATIVE) { result[i] = currentYear - i; } else if (yearRangeDirection == YEAR_RANGE_POSITIVE) { result[i] = currentYear + i; } else { throw new IllegalArgumentException("yearRangeDirection isn't set " + "properly: " + yearRangeDirection + " must be YEAR_RANGE_NEGATIVE or YEAR_RANGE_POSITIVE"); } } return result; } /** * Get the range of valid hour values, 1 to 12 if {@link #isLatin} is * <code>true</code> and 0 to 23 otherwise. * @return the range of valid hour values, 1 to 12 if {@link #isLatin} is * <code>true</code> and 0 to 23 otherwise. */ public int[] getHourRange() { int[] result = isLatin() ? new int[12] : new int[24]; for (int i = 0; i < result.length; i++) { result[i] = isLatin() ? i + 1 : i; } return result; } /** * @return The date constructed from the individual field values * of this bean instance, or <code>null</code> if the date is invalid. */ public Date getDate() { try { return cal.getTime(); } catch (IllegalArgumentException e) { // Ignore and return null to indicate invalid date return null; } } /** * Set the internal date of the picker to <code>date</code> * @param date the date to which the internal date should be set to */ public void setDate(Date date) { cal.setTime(date); } /** * The calendar underlying this date picker. It will use * the timezone and locale that was given to the picker. * @return the calendar underlying this picker */ public Calendar getCalendar() { return cal; } /** * Parse the values in <code>map</code> into the internal date. The * <code>map</code> must map the names of the date widget fields like * <code>date_year</code> etc. to <code>Integer</code> or parsable * <code>String</code> values. * * If the map does not contain all of the required fields, the default * date of now will be used. * * @param map a map from date widget field names to <code>Integer</code> * or <code>String</code> values. */ public void readMap(Map map) { cal.clear(); Map fieldCalMap = getFieldCalMap(); //go through and read all of the fields we need. for (Iterator i = fieldCalMap.keySet().iterator(); i.hasNext();) { String field = (String) i.next(); Object value = map.get(propertyName(field)); Integer fieldValue; if (value == null) { fieldValue = null; } else if (value instanceof Integer) { fieldValue = (Integer)value; } else if (value instanceof String) { fieldValue = new Integer(Integer.parseInt((String)value)); } //this is necessary for reading request parameters. else if (value instanceof String[]) { String [] s = (String[])value; if (s[0] == null) { fieldValue = null; } else { fieldValue = new Integer(Integer.parseInt(s[0])); } } else { throw new IllegalArgumentException("Form contains a date picker field" + " that is the wrong type: " + value.getClass()); } if (fieldValue == null) { //This means that one of the required fields wasn't found //Therefore, we can't really build up a date, so fall back // on the default date, now. cal.clear(); setDate(new Date()); break; //stop looking for the rest of the fields. } setField(field, fieldValue); } } /** * Reads the form fields to populate date fields. * If a form does not have all of the fields, the inital date * will be now. * @param form The form containing date picker fields. */ public void readForm(DynaActionForm form) { readMap(form.getMap()); } /** * Write the internal date into <code>map</code>. The * <code>map</code> will map the names of the date widget fields like * <code>date_year</code> etc. to <code>Integer</code> values. * * @param map a map from date widget field names to <code>Integer</code> values */ public void writeToMap(Map map) { Map fieldCalMap = getFieldCalMap(); for (Iterator i = fieldCalMap.keySet().iterator(); i.hasNext();) { String field = (String) i.next(); map.put(propertyName(field), getField(field)); } } /** * Write the internal date into <code>form</code>. The * <code>form</code> will map the names of the date widget fields like * <code>date_year</code> etc. to <code>Integer</code> values. * * @param form a dyna action form with date widget field names * to <code>Integer</code> values */ public void writeToForm(DynaActionForm form) { writeToMap(form.getMap()); } /** * Return the value of a particular field from the internal date. Note * that the value for <code>HOUR</code> will be between <code>0-12</code> * if the locale uses a latin date format, and between <code>0-24</code> * if it doesn't. * * @param field the name of the field, must be one of the constants * defined by this class * @return the value in the internal date associated with the field. */ private Integer getField(String field) { int calField = getCalField(field); try { //HACK: instituted for UI's that display 1:00 - 12:00 for hours //instead of 0:00 - 11:00 like the Java calendar int result = cal.get(calField); if (isLatin() && field.equals(HOUR) && result == 0) { return new Integer(12); } return new Integer(result); } catch (IllegalArgumentException e) { // Ignore and return null to indicate invalid date return null; } } /** * Set the value of a <code>field</code> to the given <code>value</code>. Note * that setting a field to an invalid value, e.g., setting <code>MONTH</code> to * <code>13</code> will not immediately cause an error. To check that the date is * still valid, call {@link #getDate}. * * @param field the name of the field to set * @param value the value to set for that field */ private void setField(String field, Integer value) { int calField = getCalField(field); //HACK: instituted for UI's that display 1:00 - 12:00 for hours //instead of 0:00 - 11:00 like the Java calendar if (isLatin() && field.equals(HOUR) && value != null && value.intValue() == 12) { cal.set(calField, 0); } else { cal.set(calField, value == null ? -1 : value.intValue()); } } /** * Return date format symbols for the locale associated with this date picker. * This method is mainly provided as a conveniece for generating month names * and am/pm designations in the user interface. * * @return the date format symbols for the locale associated with this picker */ public DateFormatSymbols getDateFormatSymbols() { if (dateFormatSymbols == null) { dateFormatSymbols = new DateFormatSymbols(locale); } return dateFormatSymbols; } private String propertyName(String field) { return name + "_" + field; } private int getCalField(String field) { Map fieldCalMap = getFieldCalMap(); return ((Integer) fieldCalMap.get(field)).intValue(); } private Map getFieldCalMap() { return (Map) FIELD_CALENDAR_MAP.get(Boolean.valueOf(isLatin())); } /** * @return Returns the yearRangeDirection. */ public int getYearRangeDirection() { return yearRangeDirection; } private void analyzeDateFormat() { // HACK: This checks whether the am/pm indicator is // in the default date format for this locale SimpleDateFormat sdf = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale); String pattern = sdf.toPattern(); isLatin = (pattern.indexOf('a') >= 0); // HACK: check whether month or date comes first isDayBeforeMonth = pattern.indexOf('d') < pattern.indexOf('M'); } private static Map makeFieldCalendarMap(boolean isLatin) { Map result = new HashMap(); result.put(YEAR, new Integer(Calendar.YEAR)); result.put(MONTH, new Integer(Calendar.MONTH)); result.put(DAY, new Integer(Calendar.DAY_OF_MONTH)); if (isLatin) { result.put(HOUR, new Integer(Calendar.HOUR)); result.put(AM_PM, new Integer(Calendar.AM_PM)); } else { result.put(HOUR, new Integer(Calendar.HOUR_OF_DAY)); } result.put(MINUTE, new Integer(Calendar.MINUTE)); return result; } /** * Set disableTime property (Picker doesn't offer to set time) */ public void setDisableTime() { this.disableTime = true; } /** * Returns disableTime property * @return disableTime property */ public boolean getDisableTime() { return this.disableTime; } /** * Set disableDate property (Picker doesn't offer to set date) */ public void setDisableDate() { this.disableDate = true; } /** * Returns disableDate property * @return disableDate property */ public boolean getDisableDate() { return this.disableDate; } /** * @return locale for this picker */ public Locale getLocale() { return this.locale; } }