blob: becfaede10a39a734c4f854119d0a2eab831ca4c [file] [log] [blame]
/*
* 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
public 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);
}
};
}
}