http://git-wip-us.apache.org/repos/asf/wicket/blob/2bb684c1/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/TimeField.java
----------------------------------------------------------------------
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/TimeField.java
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/TimeField.java
new file mode 100644
index 0000000..68af251
--- /dev/null
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/TimeField.java
@@ -0,0 +1,520 @@
+/*
+ * 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.markup.html.form.datetime;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.text.SimpleDateFormat;
+import java.time.LocalTime;
+import java.time.chrono.IsoChronology;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.FormatStyle;
+import java.time.temporal.ChronoField;
+import java.util.Arrays;
+import java.util.Locale;
+
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import 
org.apache.wicket.markup.html.form.AbstractTextComponent.ITextFormatProvider;
+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.util.convert.IConverter;
+import org.apache.wicket.util.convert.converter.IntegerConverter;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.validation.validator.RangeValidator;
+
+/**
+ * Works on a {@link java.util.Date} object. Displays 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 TimeField}'s {@link Locale}, as does the visibility of the AM/PM 
field (see
+ * {@link TimeField#use12HourFormat}).
+ * 
+ * @author eelcohillenius
+ * @see TimeField for a variant with just the date field and date picker
+ */
+public class TimeField extends FormComponentPanel<LocalTime> implements 
ITextFormatProvider
+{
+       private static final long serialVersionUID = 1L;
+
+       /**
+        * Enumerated type for different ways of handling the render part of 
requests.
+        */
+       public enum AM_PM {
+               /** */
+               AM("AM"),
+
+               /** */
+               PM("PM");
+
+               private  final String value;
+
+               AM_PM(final String name)
+               {
+                       value = name;
+               }
+
+               @Override
+               public String toString()
+               {
+                       return value;
+               }
+       }
+       protected static final String HOURS = "hours";
+       protected static final String MINUTES = "minutes";
+       protected static final String AM_OR_PM_CHOICE = "amOrPmChoice";
+
+       private static final IConverter<Integer> MINUTES_CONVERTER = new 
IntegerConverter() {
+               private static final long serialVersionUID = 1L;
+
+               protected NumberFormat newNumberFormat(Locale locale) {
+                       return new DecimalFormat("00");
+               }
+       };
+
+       // The TextField for "hours" and it's associated model object
+       private TextField<Integer> hoursField;
+
+       // The TextField for "minutes" and it's associated model object
+       private TextField<Integer> minutesField;
+
+       // The dropdown list for AM/PM and it's associated model object
+       private DropDownChoice<AM_PM> amOrPmChoice;
+       private LocalTime time = LocalTime.now();
+
+       /**
+        * Creates a new TimeField defaulting to using a short date pattern
+        * 
+        * @param id
+        *            The id of the text field
+        * @param model
+        *            The model
+        * @param timePattern
+        *            The pattern to use. Must be not null. See {@link 
SimpleDateFormat} for available
+        *            patterns.
+        * @return TimeField
+        */
+       public static TimeField forTimePattern(String id, IModel<LocalTime> 
model, String timePattern)
+       {
+               return new TimeField(id, model, new 
PatternTimeConverter(timePattern));
+       }
+
+       /**
+        * Creates a new TimeField defaulting to using a short date pattern
+        * 
+        * @param id
+        *            The id of the text field
+        * @param timePattern
+        *            The pattern to use. Must be not null. See {@link 
SimpleDateFormat} for available
+        *            patterns.
+        * @return TimeField
+        */
+       public static TimeField forTimePattern(String id, String timePattern)
+       {
+               return forTimePattern(id, null, timePattern);
+       }
+
+       /**
+        * Creates a new TimeField using the provided date style.
+        * 
+        * @param id
+        *            The id of the text field
+        * @param model
+        *            The model
+        * @param timeStyle
+        *            Date style to use. The first character is the date style, 
and the second character
+        *            is the time style. Specify a character of 'S' for short 
style, 'M' for medium, 'L'
+        *            for long, and 'F' for full. A date or time may be 
ommitted by specifying a style
+        *            character '-'. See {@link 
org.joda.time.DateTimeFormat#forStyle(String)}.
+        * @return TimeField
+        */
+       public static TimeField forTimeStyle(String id, IModel<LocalTime> 
model, String timeStyle)
+       {
+               return new TimeField(id, model, new 
StyleTimeConverter(timeStyle));
+       }
+
+       /**
+        * Creates a new TimeField using the provided date style.
+        * 
+        * @param id
+        *            The id of the text field
+        * @param timeStyle
+        *            Date style to use. The first character is the date style, 
and the second character
+        *            is the time style. Specify a character of 'S' for short 
style, 'M' for medium, 'L'
+        *            for long, and 'F' for full. A date or time may be 
ommitted by specifying a style
+        *            character '-'. See {@link 
org.joda.time.DateTimeFormat#forStyle(String)}.
+        * @return TimeField
+        */
+       public static TimeField forTimeStyle(String id, String timeStyle)
+       {
+               return forTimeStyle(id, null, timeStyle);
+       }
+
+       /**
+        * Creates a new TimeField defaulting to using a short date pattern
+        * 
+        * @param id
+        *            The id of the text field
+        * @return TimeField
+        */
+       public static TimeField forShortStyle(String id)
+       {
+               return forShortStyle(id, null);
+       }
+
+       /**
+        * Creates a new TimeField defaulting to using a short date pattern
+        * 
+        * @param id
+        *            The id of the text field
+        * @param model
+        *            The model
+        * @return TimeField
+        */
+       public static TimeField forShortStyle(String id, IModel<LocalTime> 
model)
+       {
+               return new TimeField(id, model, new StyleTimeConverter());
+       }
+
+       /**
+        * Creates a new TimeField using the provided converter.
+        * 
+        * @param id
+        *            The id of the text field
+        * @param converter
+        *            the date converter
+        * @return TimeField
+        */
+       public static TimeField withConverter(String id, LocalTimeConverter 
converter)
+       {
+               return withConverter(id, null, converter);
+       }
+
+       /**
+        * Creates a new TimeField using the provided converter.
+        * 
+        * @param id
+        *            The id of the text field
+        * @param model
+        *            The model
+        * @param converter
+        *            the date converter
+        * @return TimeField
+        */
+       public static TimeField withConverter(String id, IModel<LocalTime> 
model, LocalTimeConverter converter)
+       {
+               return new TimeField(id, model, converter);
+       }
+
+       /**
+        * The converter for the TextField
+        */
+       private final LocalTimeConverter converter;
+
+       /**
+        * Construct.
+        * 
+        * @param id
+        *      the component id
+        */
+       public TimeField(String id, LocalTimeConverter converter)
+       {
+               this(id, null, converter);
+       }
+
+       /**
+        * Construct.
+        * 
+        * @param id
+        *      the component id
+        * @param model
+        *      the component's model
+        */
+       public TimeField(String id, IModel<LocalTime> model, LocalTimeConverter 
converter)
+       {
+               super(id, model);
+
+               Args.notNull(converter, "converter");
+               this.converter = converter;
+
+               // Sets the type that will be used when updating the model for 
this component.
+               setType(LocalTime.class);
+
+
+               // Create and add the "hours" TextField
+               add(hoursField = newHoursTextField(HOURS, new HoursModel(), 
Integer.class));
+
+               // Create and add the "minutes" TextField
+               add(minutesField = newMinutesTextField(MINUTES, new 
MinutesModel(), Integer.class));
+
+               // Create and add the "AM/PM" Listbox
+               add(amOrPmChoice = new DropDownChoice<>(AM_OR_PM_CHOICE, new 
AmPmModel(), 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<Integer>(id, 
model, type)
+               {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       protected String[] getInputTypes()
+                       {
+                               return new String[] {"number"};
+                       }
+               };
+               hoursTextField.add(AttributeModifier.append("min", 
getMaximumHours() == 24 ? 0 : 1));
+               hoursTextField.add(AttributeModifier.append("max", 
getMaximumHours() == 24 ? 23 : 12));
+               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;
+                       }
+
+                       @Override
+                       protected String[] getInputTypes()
+                       {
+                               return new String[] {"number"};
+                       }
+               };
+               minutesField.add(AttributeModifier.append("min", 0));
+               minutesField.add(AttributeModifier.append("max", 59));
+               minutesField.add(new RangeValidator<>(0, 59));
+               minutesField.setLabel(new Model<>(MINUTES));
+               return minutesField;
+       }
+
+       @Override
+       public String getInput()
+       {
+               // since we override convertInput, we can let this method 
return a value
+               // that is just suitable for error reporting
+               return String.format("%s:%s", hoursField.getInput(), 
minutesField.getInput());
+       }
+
+       @Override
+       public void convertInput()
+       {
+               Integer hours = hoursField.getConvertedInput();
+               Integer minutes = minutesField.getConvertedInput();
+               AM_PM amOrPmInput = amOrPmChoice.getConvertedInput();
+
+               LocalTime localTime = null;
+               if (hours != null && minutes != null)
+               {
+                       // Use the input to create a LocalTime object
+                       localTime = LocalTime.of(hours, minutes);
+
+                       // Adjust for halfday if needed
+                       if (use12HourFormat())
+                       {
+                               int halfday = (amOrPmInput == AM_PM.PM ? 1 : 0);
+                               localTime = 
localTime.with(ChronoField.AMPM_OF_DAY, halfday);
+                       }
+               }
+               setConvertedInput(localTime);
+       }
+
+       @Override
+       protected void onBeforeRender() {
+               hoursField.setRequired(isRequired());
+               minutesField.setRequired(isRequired());
+
+               boolean use12HourFormat = use12HourFormat();
+               amOrPmChoice.setVisible(use12HourFormat);
+               super.onBeforeRender();
+       }
+
+       /**
+        * 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 = 
DateTimeFormatterBuilder.getLocalizedDateTimePattern(null, FormatStyle.SHORT, 
IsoChronology.INSTANCE, 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;
+       }
+
+       protected class HoursModel implements IModel<Integer>
+       {
+               private static final long serialVersionUID = 1L;
+
+               @Override
+               public Integer getObject()
+               {
+                       LocalTime t = TimeField.this.getModelObject();
+                       if (t == null)
+                       {
+                               return null;
+                       }
+                       return getMaximumHours() == 24 ? t.getHour() : 
t.get(ChronoField.CLOCK_HOUR_OF_AMPM);
+               }
+
+               @Override
+               public void setObject(Integer hour)
+               {
+                       time = time.with(getMaximumHours() == 24 ? 
ChronoField.HOUR_OF_DAY : ChronoField.CLOCK_HOUR_OF_AMPM, hour);
+               }
+       }
+
+       protected class MinutesModel implements IModel<Integer>
+       {
+               private static final long serialVersionUID = 1L;
+
+               @Override
+               public Integer getObject()
+               {
+                       LocalTime t = TimeField.this.getModelObject();
+                       return t == null ? null : t.getMinute();
+               }
+
+               @Override
+               public void setObject(Integer minute)
+               {
+                       time = time.with(ChronoField.MINUTE_OF_HOUR, minute);
+               }
+       }
+
+       protected class AmPmModel implements IModel<AM_PM>
+       {
+               private static final long serialVersionUID = 1L;
+
+               @Override
+               public AM_PM getObject()
+               {
+                       LocalTime t = TimeField.this.getModelObject();
+                       int i = t == null ? 0 : t.get(ChronoField.AMPM_OF_DAY);
+                       return i == 0 ? AM_PM.AM : AM_PM.PM;
+               }
+
+               @Override
+               public void setObject(AM_PM amPm)
+               {
+                       int i = AM_PM.AM == amPm ? 0 : 1;
+                       time = time.with(ChronoField.AMPM_OF_DAY, i);
+               }
+       }
+
+       /**
+        * @return The specialized converter.
+        * @see org.apache.wicket.Component#createConverter(java.lang.Class)
+        */
+       @Override
+       protected IConverter<?> createConverter(Class<?> clazz)
+       {
+               if (LocalTime.class.isAssignableFrom(clazz))
+               {
+                       return converter;
+               }
+               return null;
+       }
+
+       /**
+        * @see 
org.apache.wicket.markup.html.form.AbstractTextComponent.ITextFormatProvider#getTextFormat()
+        */
+       @Override
+       public final String getTextFormat()
+       {
+               return converter.getPattern(getLocale());
+       }
+
+       public static FormatStyle parseFormatStyle(char style)
+       {
+               switch (style)
+               {
+                       case 'M':
+                               return FormatStyle.MEDIUM;
+                       case 'S':
+                       default:
+                               return FormatStyle.SHORT;
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/2bb684c1/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeConverter.java
----------------------------------------------------------------------
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeConverter.java
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeConverter.java
new file mode 100644
index 0000000..9286f2e
--- /dev/null
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeConverter.java
@@ -0,0 +1,203 @@
+/*
+ * 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.markup.html.form.datetime;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.wicket.Session;
+import org.apache.wicket.core.request.ClientInfo;
+import org.apache.wicket.protocol.http.request.WebClientInfo;
+import org.apache.wicket.util.convert.ConversionException;
+import org.apache.wicket.util.convert.IConverter;
+import org.apache.wicket.util.lang.Args;
+import org.apache.wicket.util.string.Strings;
+
+
+/**
+ * Base class for javax.time based date converters. It contains the logic to 
parse and format,
+ * optionally taking the time zone difference between clients and the server 
into account.
+ * <p>
+ * Converters of this class are best suited for per-component use.
+ * </p>
+ * 
+ * @author eelcohillenius
+ */
+public abstract class ZonedDateTimeConverter implements 
IConverter<ZonedDateTime>
+{
+       private static final long serialVersionUID = 1L;
+
+       /**
+        * Whether to apply the time zone difference when interpreting dates.
+        */
+       private final boolean applyTimeZoneDifference;
+
+       /**
+        * Construct. <p> When applyTimeZoneDifference is true, the current 
time is applied on the
+        * parsed date, and the date will be corrected for the time zone 
difference between the server
+        * and the client. For instance, if I'm in Seattle and the server I'm 
working on is in
+        * Amsterdam, the server is 9 hours ahead. So, if I'm inputting say 
12/24 at a couple of hours
+        * before midnight, at the server it is already 12/25. If this boolean 
is true, it will be
+        * transformed to 12/25, while the client sees 12/24. </p>
+        * 
+        * @param applyTimeZoneDifference
+        *            whether to apply the difference in time zones between 
client and server
+        */
+       public ZonedDateTimeConverter(boolean applyTimeZoneDifference)
+       {
+               this.applyTimeZoneDifference = applyTimeZoneDifference;
+       }
+
+       public ZonedDateTime convertToObject(String value, DateTimeFormatter 
format, Locale locale) {
+               try
+               {
+                       // parse date retaining the time of the submission
+                       return ZonedDateTime.parse(value, format);
+               }
+               catch (RuntimeException e)
+               {
+                       throw newConversionException(e, locale);
+               }
+       }
+
+       @Override
+       public ZonedDateTime convertToObject(String value, Locale locale)
+       {
+               if (Strings.isEmpty(value))
+               {
+                       return null;
+               }
+
+               DateTimeFormatter format = getFormat(locale);
+               Args.notNull(format, "format");
+
+               if (applyTimeZoneDifference)
+               {
+                       ZoneId zoneId = getClientTimeZone();
+
+                       // set time zone for client
+                       format = format.withZone(getTimeZone());
+
+                       ZonedDateTime dateTime = convertToObject(value, format, 
locale);
+                       // apply the server time zone to the parsed value
+                       if (zoneId != null)
+                       {
+                               dateTime = dateTime.withZoneSameInstant(zoneId);
+                       }
+
+                       return dateTime;
+               }
+               else
+               {
+                       return convertToObject(value, format, locale);
+               }
+       }
+
+       /**
+        * Creates a ConversionException and sets additional context 
information to it.
+        *
+        * @param cause
+        *            - {@link RuntimeException} cause
+        * @param locale
+        *            - {@link Locale} used to set 'format' variable with 
localized pattern
+        * @return {@link ConversionException}
+        */
+       ConversionException newConversionException(RuntimeException cause, 
Locale locale)
+       {
+               return new ConversionException(cause)
+                               .setVariable("format", getPattern(locale));
+       }
+
+       @Override
+       public String convertToString(ZonedDateTime dateTime, Locale locale)
+       {
+               DateTimeFormatter format = getFormat(locale);
+
+               if (applyTimeZoneDifference)
+               {
+                       ZoneId zoneId = getClientTimeZone();
+                       if (zoneId != null)
+                       {
+                               // apply time zone to formatter
+                               format = format.withZone(zoneId);
+                       }
+               }
+               return format.format(dateTime);
+       }
+
+       /**
+        * Gets whether to apply the time zone difference when interpreting 
dates.
+        * 
+        * </p> When true, the current time is applied on the parsed date, and 
the date will be
+        * corrected for the time zone difference between the server and the 
client. For instance, if
+        * I'm in Seattle and the server I'm working on is in Amsterdam, the 
server is 9 hours ahead.
+        * So, if I'm inputting say 12/24 at a couple of hours before midnight, 
at the server it is
+        * already 12/25. If this boolean is true, it will be transformed to 
12/25, while the client
+        * sees 12/24. </p>
+        * 
+        * @return whether to apply the difference in time zones between client 
and server
+        */
+       public final boolean getApplyTimeZoneDifference()
+       {
+               return applyTimeZoneDifference;
+       }
+
+       /**
+        * @param locale
+        *            The locale used to convert the value
+        * @return Gets the pattern that is used for printing and parsing
+        */
+       public abstract String getPattern(Locale locale);
+
+       /**
+        * Gets the client's time zone.
+        * 
+        * @return The client's time zone or null
+        */
+       protected ZoneId getClientTimeZone()
+       {
+               ClientInfo info = Session.get().getClientInfo();
+               if (info instanceof WebClientInfo)
+               {
+                       TimeZone timeZone = ((WebClientInfo) 
info).getProperties().getTimeZone();
+                       return timeZone.toZoneId();
+               }
+               return null;
+       }
+
+       /**
+        * @param locale
+        *            The locale used to convert the value
+        * 
+        * @return formatter The formatter for the current conversion
+        */
+       public abstract DateTimeFormatter getFormat(Locale locale);
+
+       /**
+        * Gets the server time zone. Override this method if you want to fix 
to a certain time zone,
+        * regardless of what actual time zone the server is in.
+        * 
+        * @return The server time zone
+        */
+       protected ZoneId getTimeZone()
+       {
+               return ZoneId.systemDefault();
+       }
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/2bb684c1/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeField.java
----------------------------------------------------------------------
diff --git 
a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeField.java
 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeField.java
new file mode 100644
index 0000000..a6814f8
--- /dev/null
+++ 
b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/form/datetime/ZonedDateTimeField.java
@@ -0,0 +1,154 @@
+/*
+ * 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.markup.html.form.datetime;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoField;
+import java.util.Locale;
+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.model.IModel;
+import org.apache.wicket.protocol.http.request.WebClientInfo;
+
+/**
+ * Works on a {@link java.time.ZonedDateTimeTime} object. Displays a date 
field and a 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 ZonedDateTimeField}'s {@link 
Locale}, as does the visibility
+ * of the AM/PM field (see {@link ZonedDateTimeField#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 DateField} by overriding {@link #newDateTextField(String, IModel)} 
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 ZonedDateTimeField extends AbstractDateTimeField<ZonedDateTime>
+{
+       private static final long serialVersionUID = 1L;
+
+       private ZonedDateTime dateTime = ZonedDateTime.now();
+
+       /**
+        * Construct.
+        * 
+        * @param id
+        */
+       public ZonedDateTimeField(final String id)
+       {
+               this(id, null);
+       }
+
+       /**
+        * Construct.
+        * 
+        * @param id
+        * @param model
+        */
+       public ZonedDateTimeField(final String id, final IModel<ZonedDateTime> 
model)
+       {
+               super(id, model);
+
+               // Sets the type that will be used when updating the model for 
this component.
+               setType(ZonedDateTime.class);
+       }
+
+       /**
+        * Gets the client's time zone.
+        * 
+        * @return The client's time zone or null
+        */
+       protected ZoneId getClientTimeZone()
+       {
+               ClientInfo info = Session.get().getClientInfo();
+               if (info instanceof WebClientInfo)
+               {
+                       TimeZone timeZone = ((WebClientInfo) 
info).getProperties().getTimeZone();
+                       return timeZone != null ? timeZone.toZoneId() : null;
+               }
+               return null;
+       }
+
+       ZonedDateTime performConvert(LocalDate date, LocalTime time) {
+               return ZonedDateTime.of(date, time, getClientTimeZone());
+       }
+
+       @Override
+       void prepareObject() {
+               ZonedDateTime modelObject = getModelObject();
+               if (modelObject == null)
+               {
+                       dateTime = null;
+               }
+               else
+               {
+                       // convert date to the client's time zone if we have 
that info
+                       ZoneId zone = getClientTimeZone();
+                       if (zone != null)
+                       {
+                               modelObject = 
modelObject.withZoneSameInstant(zone);
+                       }
+               }
+       }
+
+       LocalDate getLocalDate()
+       {
+               return dateTime.toLocalDate();
+       }
+
+       void setLocalDate(LocalDate date)
+       {
+               dateTime = dateTime.with(ChronoField.YEAR, date.getYear())
+                               .with(ChronoField.MONTH_OF_YEAR, 
date.getMonthValue())
+                               .with(ChronoField.DAY_OF_YEAR, 
date.getDayOfMonth());
+       }
+
+       LocalTime getLocalTime()
+       {
+               return dateTime.toLocalTime();
+       }
+
+       void setLocalTime(LocalTime time)
+       {
+               dateTime = dateTime.with(ChronoField.HOUR_OF_DAY, 
time.getHour())
+                               .with(ChronoField.MINUTE_OF_HOUR, 
time.getMinute());
+       }
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/2bb684c1/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/form/datetime/DateConverterTest.java
----------------------------------------------------------------------
diff --git 
a/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/form/datetime/DateConverterTest.java
 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/form/datetime/DateConverterTest.java
new file mode 100644
index 0000000..d590615
--- /dev/null
+++ 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/form/datetime/DateConverterTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.markup.html.form.datetime;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+import 
org.apache.wicket.extensions.markup.html.form.datetime.PatternZonedDateTimeConverter;
+import 
org.apache.wicket.extensions.markup.html.form.datetime.StyleZonedDateTimeConverter;
+import org.apache.wicket.util.convert.ConversionException;
+import org.apache.wicket.util.convert.IConverter;
+import org.apache.wicket.util.convert.converter.CalendarConverter;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests for {@link ZonedDateTimeConverter} and subclasses.
+ * 
+ * @author akiraly
+ */
+public class DateConverterTest
+{
+       /**
+        * WICKET-3598
+        */
+       @Test
+       public void testLocaleUsed()
+       {
+               Locale locale = Locale.GERMAN;
+
+               StyleZonedDateTimeConverter styleDateConverter = new 
StyleZonedDateTimeConverter("F-", false);
+               DateTimeFormatter styleFormatter = 
styleDateConverter.getFormat(locale);
+
+               Assert.assertEquals(locale, styleFormatter.getLocale());
+
+               PatternZonedDateTimeConverter patternDateConverter = new 
PatternZonedDateTimeConverter(
+                       styleDateConverter.getPattern(locale), false);
+               DateTimeFormatter patternFormatter = 
patternDateConverter.getFormat(locale);
+
+               Assert.assertEquals(locale, patternFormatter.getLocale());
+
+               Calendar now = Calendar.getInstance();
+
+               ZonedDateTime zNow = ZonedDateTime.ofInstant(now.toInstant(), 
ZoneId.systemDefault());
+               String actual = styleDateConverter.convertToString(zNow, 
locale);
+               String expected = patternDateConverter.convertToString(zNow, 
locale);
+
+               Assert.assertEquals(expected, actual);
+       }
+
+       /**
+        * WICKET-3658
+        */
+       @Test
+       public void testCalendarConverterWithDelegate()
+       {
+               Locale locale = Locale.GERMAN;
+
+               Calendar input = Calendar.getInstance(locale);
+               input.clear();
+               input.set(2011, Calendar.MAY, 7);
+
+               final StyleZonedDateTimeConverter styleDateConverter = new 
StyleZonedDateTimeConverter("F-", false);
+
+               CalendarConverter calendarConverter = new CalendarConverter(new 
IConverter<Date>()
+               {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       public Date convertToObject(String value, Locale 
locale) throws ConversionException {
+                               ZonedDateTime zd = 
styleDateConverter.convertToObject(value, locale);
+                               return zd == null ? null : 
Date.from(zd.toInstant());
+                       }
+
+                       @Override
+                       public String convertToString(Date value, Locale 
locale) {
+                               return 
styleDateConverter.convertToString(ZonedDateTime.ofInstant(value.toInstant(), 
ZoneId.systemDefault()), locale);
+                       }
+                       
+               });
+
+               String expected = 
styleDateConverter.convertToString(ZonedDateTime.ofInstant(input.toInstant(), 
ZoneId.systemDefault()), locale);
+               String actual = calendarConverter.convertToString(input, 
locale);
+
+               Assert.assertEquals(expected, actual);
+
+               Calendar revert = calendarConverter.convertToObject(actual, 
locale);
+
+               Assert.assertEquals(input, revert);
+       }
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/2bb684c1/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/form/datetime/DateTimeFieldTest.java
----------------------------------------------------------------------
diff --git 
a/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/form/datetime/DateTimeFieldTest.java
 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/form/datetime/DateTimeFieldTest.java
new file mode 100644
index 0000000..3b167e4
--- /dev/null
+++ 
b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/form/datetime/DateTimeFieldTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.markup.html.form.datetime;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.Locale;
+
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.FormComponent;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.FormTester;
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class DateTimeFieldTest extends WicketTestCase {
+       @Rule
+       public ExpectedException expectedException = ExpectedException.none();
+
+       @Test
+       public void timeNullTest() {
+               TestTimePage page = new TestTimePage(null);
+               tester.startPage(page);
+               FormTester formTester = tester.newFormTester("form", false);
+               formTester.submit();
+               assertNull(page.field.getModelObject());
+       }
+
+       @Test
+       public void timeNullHoursTest() {
+               TestTimePage page = new TestTimePage(null);
+               tester.startPage(page);
+               FormTester formTester = tester.newFormTester("form", false);
+               formTester.setValue("field:minutes", "8");
+               formTester.submit();
+               assertNull(page.field.getModelObject());
+       }
+
+       @Test
+       public void timeNullMinutesTest() {
+               TestTimePage page = new TestTimePage(null);
+               tester.startPage(page);
+               FormTester formTester = tester.newFormTester("form", false);
+               formTester.setValue("field:hours", "8");
+               formTester.submit();
+               assertNull(page.field.getModelObject());
+       }
+
+       @Test
+       public void timeNotNullTest() {
+               TestTimePage page = new TestTimePage(LocalTime.of(6, 15));
+               tester.startPage(page);
+               FormTester formTester = tester.newFormTester("form", false);
+               formTester.setValue("field:hours", "8");
+               formTester.submit();
+               LocalTime t = page.field.getModelObject();
+               assertNotNull(t);
+               assertEquals(8, t.getHour());
+               assertEquals(15, t.getMinute());
+       }
+
+       @Test
+       public void dateNullTest() {
+               TestDatePage page = new TestDatePage(null);
+               tester.startPage(page);
+               FormTester formTester = tester.newFormTester("form", false);
+               formTester.submit();
+               assertNull(page.field.getModelObject());
+       }
+
+       @Test
+       public void dateNotNullTest() {
+               LocalDate date = LocalDate.of(2017, 02, 13);
+               TestDatePage page = new TestDatePage(null);
+               tester.startPage(page);
+               FormTester formTester = tester.newFormTester("form", false);
+               formTester.setValue("field", new 
StyleDateConverter("F").convertToString(date, Locale.forLanguageTag("en-US")));
+               formTester.submit();
+               LocalDate d = page.field.getModelObject();
+               assertNotNull(d);
+               assertEquals(date, d);
+       }
+
+       public static class TestDatePage extends TestPage<LocalDate>
+       {
+               private static final long serialVersionUID = 1L;
+
+               TestDatePage(LocalDate val)
+               {
+                       super(val);
+                       tag = "input type=\"text\"";
+               }
+
+               @Override
+               FormComponent<LocalDate> newComponent()
+               {
+                       return DateField.forDateStyle("field", model, "F");
+               }
+       }
+
+       public static class TestTimePage extends TestPage<LocalTime>
+       {
+               private static final long serialVersionUID = 1L;
+
+               TestTimePage(LocalTime val)
+               {
+                       super(val);
+               }
+
+               @Override
+               FormComponent<LocalTime> newComponent()
+               {
+                       return TimeField.forTimeStyle("field", model, "F");
+               }
+       }
+
+       public abstract static class TestPage<T extends Serializable> extends 
WebPage implements IMarkupResourceStreamProvider
+       {
+               private static final long serialVersionUID = 1L;
+               Form<Void> form;
+               FormComponent<T> field;
+               IModel<T> model = new Model<T>();
+               String tag = "span";
+
+               /** */
+               public TestPage(T val)
+               {
+                       add(form = new Form<>("form"));
+                       model.setObject(val);
+                       form.add(field = newComponent());
+               }
+
+               abstract FormComponent<T> newComponent();
+
+               @Override
+               public IResourceStream getMarkupResourceStream(MarkupContainer 
container, Class<?> containerClass)
+               {
+                       return new 
StringResourceStream(String.format("<html><body>"
+                                       + "<form wicket:id=\"form\"><%s 
wicket:id=\"field\"/></form></body></html>", tag));
+               }
+
+               @Override
+               protected void onDetach()
+               {
+                       super.onDetach();
+                       model.detach();
+               }
+       }
+}

http://git-wip-us.apache.org/repos/asf/wicket/blob/2bb684c1/wicket-user-guide/src/main/asciidoc/helloWorld/helloWorld_1.adoc
----------------------------------------------------------------------
diff --git a/wicket-user-guide/src/main/asciidoc/helloWorld/helloWorld_1.adoc 
b/wicket-user-guide/src/main/asciidoc/helloWorld/helloWorld_1.adoc
index c24e4dd..5dac973 100644
--- a/wicket-user-guide/src/main/asciidoc/helloWorld/helloWorld_1.adoc
+++ b/wicket-user-guide/src/main/asciidoc/helloWorld/helloWorld_1.adoc
@@ -6,7 +6,6 @@ Wicket is available as a binary package on the main site  
http://wicket.apache.o
 | wicket-core | Contains the main classes of the framework, like class 
_Component_ and _Application_. | wicket-request, wicket-util
 | wicket-request | This module contains the classes involved into web request 
processing. | wicket-util
 | wicket-util | Contains general-purpose utility classes for functional areas 
such as I/O, lang, string manipulation, security, etc... | None
-| wicket-datetime | Contains special purpose components designed to work with 
date and time. | wicket-core
 | wicket-bean-validation | Provides support for JSR 303 standard validation. | 
wicket-core
 | wicket-devutils | Contains utility classes and components to help developers 
with tasks such as debugging, class inspection and so on. | wicket-core, 
wicket-extensions
 |wicket-extensions | Contains a vast set of built-in components to build a 
rich UI for our web application (Ajax support is part of this module). | 
wicket-core

Reply via email to