/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket.extensions.yui.calendar; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import org.apache.wicket.Session; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.core.request.ClientInfo; import org.apache.wicket.datetime.markup.html.form.DateTextField; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.FormComponentPanel; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.protocol.http.request.WebClientInfo; import org.apache.wicket.util.convert.IConverter; import org.apache.wicket.util.convert.converter.IntegerConverter; import org.apache.wicket.validation.validator.RangeValidator; import org.joda.time.DateTimeFieldType; import org.joda.time.DateTimeZone; import org.joda.time.MutableDateTime; import org.joda.time.format.DateTimeFormat; /** * Works on a {@link java.util.Date} object. Displays a date field and a {@link DatePicker}, a field * for hours and a field for minutes, and an AM/PM field. The format (12h/24h) of the hours field * depends on the time format of this {@link DateTimeField}'s {@link Locale}, as does the visibility * of the AM/PM field (see {@link DateTimeField#use12HourFormat}). * <p> * <strong>Ajaxifying the DateTimeField</strong>: If you want to update a DateTimeField with an * {@link AjaxFormComponentUpdatingBehavior}, you have to attach it to the contained * {@link DateTextField} by overriding {@link #newDateTextField(String, PropertyModel)} and calling * {@link #processInput()}: * * <pre>{@code * DateTimeField dateTimeField = new DateTimeField(...) { * protected DateTextField newDateTextField(String id, PropertyModel<Date> dateFieldModel) * { * DateTextField dateField = super.newDateTextField(id, dateFieldModel); * dateField.add(new AjaxFormComponentUpdatingBehavior("change") { * protected void onUpdate(AjaxRequestTarget target) { * processInput(); // let DateTimeField process input too * * ... * } * }); * return recorder; * } * } * }</pre> * * @author eelcohillenius * @see DateField for a variant with just the date field and date picker */ public class DateTimeField extends FormComponentPanel<Date> { /** * Enumerated type for different ways of handling the render part of requests. */ public static enum AM_PM { /** */ AM("AM"), /** */ PM("PM"); /** */ private String value; AM_PM(final String name) { value = name; } /** * @see java.lang.Enum#toString() */ @Override public String toString() { return value; } } private static final long serialVersionUID = 1L; // Component-IDs protected static final String DATE = "date"; protected static final String HOURS = "hours"; protected static final String MINUTES = "minutes"; protected static final String AM_OR_PM_CHOICE = "amOrPmChoice"; // PropertyModel string to access getAmOrPm private static final String AM_OR_PM = "amOrPm"; private static final IConverter<Integer> MINUTES_CONVERTER = new IntegerConverter() { protected NumberFormat newNumberFormat(Locale locale) { return new DecimalFormat("00"); } }; // The dropdown list for AM/PM and it's associated model object private DropDownChoice<AM_PM> amOrPmChoice; private AM_PM amOrPm = AM_PM.AM; // The date TextField and it's associated model object // Note that any time information in date will be ignored private DateTextField dateField; private Date date; // The TextField for "hours" and it's associated model object private TextField<Integer> hoursField; private Integer hours; // The TextField for "minutes" and it's associated model object private TextField<Integer> minutesField; private Integer minutes; /** * Construct. * * @param id */ public DateTimeField(final String id) { this(id, null); } /** * Construct. * * @param id * @param model */ public DateTimeField(final String id, final IModel<Date> model) { super(id, model); // Sets the type that will be used when updating the model for this component. setType(Date.class); // Create and add the date TextField PropertyModel<Date> dateFieldModel = new PropertyModel<>(this, DATE); add(dateField = newDateTextField(DATE, dateFieldModel)); // Add a date picker to the date TextField dateField.add(newDatePicker()); // Create and add the "hours" TextField add(hoursField = newHoursTextField(HOURS, new PropertyModel<Integer>(this, HOURS), Integer.class)); // Create and add the "minutes" TextField add(minutesField = newMinutesTextField(MINUTES, new PropertyModel<Integer>(this, MINUTES), Integer.class)); // Create and add the "AM/PM" Listbox add(amOrPmChoice = new DropDownChoice<AM_PM>(AM_OR_PM_CHOICE, new PropertyModel<AM_PM>( this, AM_OR_PM), Arrays.asList(AM_PM.values()))); add(new WebMarkupContainer("hoursSeparator") { private static final long serialVersionUID = 1L; @Override public boolean isVisible() { return minutesField.determineVisibility(); } }); } /** * create a new {@link TextField} instance for hours to be added to this panel. * * @param id * the component id * @param model * model that should be used by the {@link TextField} * @param type * the type of the text field * @return a new text field instance */ protected TextField<Integer> newHoursTextField(final String id, IModel<Integer> model, Class<Integer> type) { TextField<Integer> hoursTextField = new TextField<>(id, model, type); hoursTextField.add(getMaximumHours() == 24 ? RangeValidator.range(0, 23) : RangeValidator .range(1, 12)); hoursTextField.setLabel(new Model<>(HOURS)); return hoursTextField; } /** * create a new {@link TextField} instance for minutes to be added to this panel. * * @param id * the component id * @param model * model that should be used by the {@link TextField} * @param type * the type of the text field * @return a new text field instance */ protected TextField<Integer> newMinutesTextField(final String id, IModel<Integer> model, Class<Integer> type) { TextField<Integer> minutesField = new TextField<Integer>(id, model, type) { private static final long serialVersionUID = 1L; @Override protected IConverter<?> createConverter(Class<?> type) { if (Integer.class.isAssignableFrom(type)) { return MINUTES_CONVERTER; } return null; } }; minutesField.add(new RangeValidator<>(0, 59)); minutesField.setLabel(new Model<>(MINUTES)); return minutesField; } /** * * @return The date TextField */ protected final DateTextField getDateTextField() { return dateField; } /** * Gets the amOrPm model object of the drop down choice. * * @return amOrPm * * @deprecated valid during rendering only */ public final AM_PM getAmOrPm() { return amOrPm; } /** * Gets the date model object for the date TextField. Any associated time information will be * ignored. * * @return date * * @deprecated valid during rendering only */ public final Date getDate() { return date; } /** * Gets the hours model object for the TextField * * @return hours * * @deprecated valid during rendering only */ public final Integer getHours() { return hours; } /** * Gets the minutes model object for the TextField * * @return minutes * * @deprecated valid during rendering only */ public final Integer getMinutes() { return minutes; } /** * Gives overriding classes the option of adding (or even changing/ removing) configuration * properties for the javascript widget. See <a * href="http://developer.yahoo.com/yui/calendar/">the widget's documentation</a> for the * available options. If you want to override/ remove properties, you should call * super.configure(properties) first. If you don't call that, be aware that you will have to * call {@link #configure(java.util.Map)} manually if you like localized strings to be added. * * @param widgetProperties * the current widget properties */ protected void configure(Map<String, Object> widgetProperties) { } @Override public String getInput() { // since we override convertInput, we can let this method return a value // that is just suitable for error reporting return dateField.getInput() + ", " + hoursField.getInput() + ":" + minutesField.getInput(); } /** * Sets the amOrPm model object associated with the drop down choice. * * @param amOrPm * amOrPm */ public final void setAmOrPm(final AM_PM amOrPm) { this.amOrPm = amOrPm; } /** * Sets the date model object associated with the date TextField. It does not affect hours or * minutes. * * @param date * date */ public final void setDate(final Date date) { this.date = date; } /** * Sets hours. * * @param hours * hours */ public final void setHours(final Integer hours) { this.hours = hours; } /** * Sets minutes. * * @param minutes * minutes */ public final void setMinutes(final Integer minutes) { this.minutes = minutes; } /** * Gets the client's time zone. * * @return The client's time zone or null */ protected TimeZone getClientTimeZone() { ClientInfo info = Session.get().getClientInfo(); if (info instanceof WebClientInfo) { return ((WebClientInfo)info).getProperties().getTimeZone(); } return null; } /** * Sets the converted input, which is an instance of {@link Date}, possibly null. It combines * the inputs of the nested date, hours, minutes and am/pm fields and constructs a date from it. * <p> * Note that overriding this method is a better option than overriding {@link #updateModel()} * like the first versions of this class did. The reason for that is that this method can be * used by form validators without having to depend on the actual model being updated, and this * method is called by the default implementation of {@link #updateModel()} anyway (so we don't * have to override that anymore). */ @Override public void convertInput() { try { // Get the converted input values Date dateFieldInput = dateField.getConvertedInput(); Integer hoursInput = hoursField.getConvertedInput(); Integer minutesInput = minutesField.getConvertedInput(); AM_PM amOrPmInput = amOrPmChoice.getConvertedInput(); if (dateFieldInput == null) { return; } // Get year, month and day ignoring any timezone of the Date object Calendar cal = Calendar.getInstance(); cal.setTime(dateFieldInput); int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH) + 1; int day = cal.get(Calendar.DAY_OF_MONTH); int hours = (hoursInput == null ? 0 : hoursInput % 24); int minutes = (minutesInput == null ? 0 : minutesInput); // Use the input to create a date object with proper timezone MutableDateTime date = new MutableDateTime(year, month, day, hours, minutes, 0, 0, DateTimeZone.forTimeZone(getClientTimeZone())); // Adjust for halfday if needed if (use12HourFormat()) { int halfday = (amOrPmInput == AM_PM.PM ? 1 : 0); date.set(DateTimeFieldType.halfdayOfDay(), halfday); date.set(DateTimeFieldType.hourOfHalfday(), hours % 12); } // The date will be in the server's timezone setConvertedInput(newDateInstance(date.getMillis())); } catch (RuntimeException e) { DateTimeField.this.error(e.getMessage()); invalid(); } } /** * A factory method for the DateTextField's model object. * * @return any specialization of java.util.Date */ protected Date newDateInstance() { return new Date(); } /** * A factory method for the DateTextField's model object. * * @param time * the time in milliseconds * @return any specialization of java.util.Date */ protected Date newDateInstance(long time) { return new Date(time); } /** * create a new {@link DateTextField} instance to be added to this panel. * * @param id * the component id * @param dateFieldModel * model that should be used by the {@link DateTextField} * @return a new date text field instance */ protected DateTextField newDateTextField(String id, PropertyModel<Date> dateFieldModel) { return DateTextField.forShortStyle(id, dateFieldModel, false); } /** * @see org.apache.wicket.Component#onBeforeRender() */ @Override protected void onBeforeRender() { dateField.setRequired(isRequired()); hoursField.setRequired(isRequired()); minutesField.setRequired(isRequired()); boolean use12HourFormat = use12HourFormat(); amOrPmChoice.setVisible(use12HourFormat); Date modelObject = (Date)getDefaultModelObject(); if (modelObject == null) { date = null; hours = null; minutes = null; } else { MutableDateTime mDate = new MutableDateTime(modelObject); // convert date to the client's time zone if we have that info TimeZone zone = getClientTimeZone(); if (zone != null) { mDate.setZone(DateTimeZone.forTimeZone(zone)); } date = mDate.toDateTime().toLocalDate().toDate(); if (use12HourFormat) { int hourOfHalfDay = mDate.get(DateTimeFieldType.hourOfHalfday()); hours = hourOfHalfDay == 0 ? 12 : hourOfHalfDay; } else { hours = mDate.get(DateTimeFieldType.hourOfDay()); } amOrPm = (mDate.get(DateTimeFieldType.halfdayOfDay()) == 0) ? AM_PM.AM : AM_PM.PM; minutes = mDate.getMinuteOfHour(); } super.onBeforeRender(); } /** * Change a date in another timezone * * @param date * The input date. * @param zone * The target timezone. * @return A new converted date. */ public static Date changeTimeZone(Date date, TimeZone zone) { Calendar first = Calendar.getInstance(zone); first.setTimeInMillis(date.getTime()); Calendar output = Calendar.getInstance(); output.set(Calendar.YEAR, first.get(Calendar.YEAR)); output.set(Calendar.MONTH, first.get(Calendar.MONTH)); output.set(Calendar.DAY_OF_MONTH, first.get(Calendar.DAY_OF_MONTH)); output.set(Calendar.HOUR_OF_DAY, first.get(Calendar.HOUR_OF_DAY)); output.set(Calendar.MINUTE, first.get(Calendar.MINUTE)); output.set(Calendar.SECOND, first.get(Calendar.SECOND)); output.set(Calendar.MILLISECOND, first.get(Calendar.MILLISECOND)); return output.getTime(); } /** * Checks whether the current {@link Locale} uses the 12h or 24h time format. This method can be * overridden to e.g. always use 24h format. * * @return true, if the current {@link Locale} uses the 12h format.<br/> * false, otherwise */ protected boolean use12HourFormat() { String pattern = DateTimeFormat.patternForStyle("-S", getLocale()); return pattern.indexOf('a') != -1 || pattern.indexOf('h') != -1 || pattern.indexOf('K') != -1; } /** * @return either 12 or 24, depending on the hour format of the current {@link Locale} */ private int getMaximumHours() { return getMaximumHours(use12HourFormat()); } /** * Convenience method (mainly for optimization purposes), in case {@link #use12HourFormat()} has * already been stored in a local variable and thus doesn't need to be computed again. * * @param use12HourFormat * the hour format to use * @return either 12 or 24, depending on the parameter <code>use12HourFormat</code> */ private int getMaximumHours(boolean use12HourFormat) { return use12HourFormat ? 12 : 24; } /** * The DatePicker that gets added to the DateTimeField component. Users may override this method * with a DatePicker of their choice. * * @return a new {@link DatePicker} instance */ protected DatePicker newDatePicker() { return new DatePicker() { private static final long serialVersionUID = 1L; @Override protected void configure(final Map<String, Object> widgetProperties, final IHeaderResponse response, final Map<String, Object> initVariables) { super.configure(widgetProperties, response, initVariables); DateTimeField.this.configure(widgetProperties); } }; } }