001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.extensions.markup.html.form.datetime; 018 019import java.text.DecimalFormat; 020import java.text.NumberFormat; 021import java.time.LocalTime; 022import java.time.chrono.IsoChronology; 023import java.time.format.DateTimeFormatterBuilder; 024import java.time.format.FormatStyle; 025import java.time.temporal.ChronoField; 026import java.util.Arrays; 027import java.util.Locale; 028 029import org.apache.wicket.core.util.string.CssUtils; 030import org.apache.wicket.markup.ComponentTag; 031import org.apache.wicket.markup.html.basic.Label; 032import org.apache.wicket.markup.html.form.DropDownChoice; 033import org.apache.wicket.markup.html.form.FormComponentPanel; 034import org.apache.wicket.markup.html.form.TextField; 035import org.apache.wicket.model.IModel; 036import org.apache.wicket.model.ResourceModel; 037import org.apache.wicket.util.convert.ConversionException; 038import org.apache.wicket.util.convert.IConverter; 039import org.apache.wicket.util.convert.converter.IntegerConverter; 040import org.apache.wicket.validation.validator.RangeValidator; 041 042/** 043 * Works on a {@link LocalTime} object. Displays a field for hours and a field for minutes, and an 044 * AM/PM field. The format (12h/24h) of the hours field depends on the time format of this 045 * {@link TimeField}'s {@link Locale}, as does the visibility of the AM/PM field (see 046 * {@link TimeField#use12HourFormat}). 047 * 048 * @author eelcohillenius 049 */ 050public class TimeField extends FormComponentPanel<LocalTime> 051{ 052 private static final long serialVersionUID = 1L; 053 054 public static final String HOURS_CSS_CLASS_KEY = CssUtils.key(TimeField.class, "hours"); 055 056 public static final String MINUTES_CSS_CLASS_KEY = CssUtils.key(TimeField.class, "minutes"); 057 058 /** 059 * Enumerated type for different ways of handling the render part of requests. 060 */ 061 public enum AM_PM 062 { 063 AM, PM; 064 } 065 066 private static final IConverter<Integer> MINUTES_CONVERTER = new IntegerConverter() 067 { 068 private static final long serialVersionUID = 1L; 069 070 @Override 071 protected NumberFormat newNumberFormat(Locale locale) 072 { 073 return new DecimalFormat("00"); 074 } 075 }; 076 077 // The TextField for "hours" and it's associated model object 078 private TextField<Integer> hoursField; 079 080 // The TextField for "minutes" and it's associated model object 081 private TextField<Integer> minutesField; 082 083 // The dropdown list for AM/PM and it's associated model object 084 private DropDownChoice<AM_PM> amOrPmChoice; 085 086 /** 087 * Construct. 088 * 089 * @param id 090 * the component id 091 */ 092 public TimeField(String id) 093 { 094 this(id, null); 095 } 096 097 /** 098 * Construct. 099 * 100 * @param id 101 * the component id 102 * @param model 103 * the component's model 104 */ 105 public TimeField(String id, IModel<LocalTime> model) 106 { 107 super(id, model); 108 109 // Sets the type that will be used when updating the model for this component. 110 setType(LocalTime.class); 111 112 add(new Label("hoursSeparator", new ResourceModel("TimeField.hoursSeparator")) 113 { 114 private static final long serialVersionUID = 1L; 115 116 @Override 117 protected void onConfigure() 118 { 119 super.onConfigure(); 120 121 minutesField.configure(); 122 123 setVisible(minutesField.isVisible()); 124 } 125 }); 126 } 127 128 @Override 129 protected void onInitialize() 130 { 131 super.onInitialize(); 132 133 // Create and add the "hours" TextField 134 add(hoursField = newHoursTextField("hours", new HoursModel(), Integer.class)); 135 136 // Create and add the "minutes" TextField 137 add(minutesField = newMinutesTextField("minutes", new MinutesModel(), Integer.class)); 138 139 // Create and add the "AM/PM" choice 140 add(amOrPmChoice = new DropDownChoice<AM_PM>("amOrPmChoice", new AmPmModel(), 141 Arrays.asList(AM_PM.values())) { 142 private static final long serialVersionUID = 1L; 143 144 @Override 145 protected boolean localizeDisplayValues() 146 { 147 return true; 148 } 149 }); 150 } 151 152 /** 153 * create a new {@link TextField} instance for hours to be added to this panel. 154 * 155 * @param id 156 * the component id 157 * @param model 158 * model that should be used by the {@link TextField} 159 * @param type 160 * the type of the text field 161 * @return a new text field instance 162 */ 163 protected TextField<Integer> newHoursTextField(final String id, IModel<Integer> model, 164 Class<Integer> type) 165 { 166 TextField<Integer> hoursTextField = new TextField<Integer>(id, model, type) 167 { 168 private static final long serialVersionUID = 1L; 169 170 @Override 171 protected String[] getInputTypes() 172 { 173 return new String[] { "number" }; 174 } 175 176 @Override 177 protected void onComponentTag(ComponentTag tag) 178 { 179 super.onComponentTag(tag); 180 181 tag.append("class", getString(HOURS_CSS_CLASS_KEY), " "); 182 183 tag.put("min", use12HourFormat() ? 1 : 0); 184 tag.put("max", use12HourFormat() ? 12 : 23); 185 } 186 }; 187 hoursTextField 188 .add(use12HourFormat() ? RangeValidator.range(1, 12) : RangeValidator.range(0, 23)); 189 return hoursTextField; 190 } 191 192 /** 193 * create a new {@link TextField} instance for minutes to be added to this panel. 194 * 195 * @param id 196 * the component id 197 * @param model 198 * model that should be used by the {@link TextField} 199 * @param type 200 * the type of the text field 201 * @return a new text field instance 202 */ 203 protected TextField<Integer> newMinutesTextField(final String id, IModel<Integer> model, 204 Class<Integer> type) 205 { 206 TextField<Integer> minutesField = new TextField<Integer>(id, model, type) 207 { 208 private static final long serialVersionUID = 1L; 209 210 @Override 211 protected IConverter<?> createConverter(Class<?> type) 212 { 213 if (Integer.class.isAssignableFrom(type)) 214 { 215 return MINUTES_CONVERTER; 216 } 217 return null; 218 } 219 220 @Override 221 protected String[] getInputTypes() 222 { 223 return new String[] { "number" }; 224 } 225 226 @Override 227 protected void onComponentTag(ComponentTag tag) 228 { 229 super.onComponentTag(tag); 230 231 tag.append("class", getString(MINUTES_CSS_CLASS_KEY), " "); 232 233 tag.put("min", 0); 234 tag.put("max", 59); 235 } 236 }; 237 minutesField.add(new RangeValidator<>(0, 59)); 238 return minutesField; 239 } 240 241 @Override 242 public String getInput() 243 { 244 // since we override convertInput, we can let this method return a value 245 // that is just suitable for error reporting 246 return String.format("%s:%s", hoursField.getInput(), minutesField.getInput()); 247 } 248 249 @Override 250 public void convertInput() 251 { 252 Integer hours = hoursField.getConvertedInput(); 253 Integer minutes = minutesField.getConvertedInput(); 254 AM_PM amOrPmInput = amOrPmChoice.getConvertedInput(); 255 256 LocalTime localTime; 257 if (hours == null && minutes == null) 258 { 259 localTime = null; 260 } 261 else if (hours != null && minutes != null) 262 { 263 // Use the input to create a LocalTime object 264 localTime = LocalTime.of(hours, minutes); 265 266 // Adjust for halfday if needed 267 if (use12HourFormat()) 268 { 269 int halfday = (amOrPmInput == AM_PM.PM ? 1 : 0); 270 localTime = localTime.with(ChronoField.AMPM_OF_DAY, halfday); 271 } 272 } 273 else 274 { 275 error(newValidationError(new ConversionException("Cannot parse time").setTargetType(getType()))); 276 return; 277 } 278 279 setConvertedInput(localTime); 280 } 281 282 @Override 283 protected void onConfigure() 284 { 285 super.onConfigure(); 286 287 hoursField.setRequired(isRequired()); 288 minutesField.setRequired(isRequired()); 289 290 amOrPmChoice.setVisible(use12HourFormat()); 291 } 292 293 /** 294 * Checks whether the current {@link Locale} uses the 12h or 24h time format. This method can be 295 * overridden to e.g. always use 24h format. 296 * 297 * @return {@code true}, if the current {@link Locale} uses the 12h format.<br/> 298 * {@code false}, otherwise 299 */ 300 protected boolean use12HourFormat() 301 { 302 String pattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(null, 303 FormatStyle.SHORT, IsoChronology.INSTANCE, getLocale()); 304 return pattern.indexOf('a') != -1 || pattern.indexOf('h') != -1 305 || pattern.indexOf('K') != -1; 306 } 307 308 protected class HoursModel implements IModel<Integer> 309 { 310 private static final long serialVersionUID = 1L; 311 312 @Override 313 public Integer getObject() 314 { 315 LocalTime t = TimeField.this.getModelObject(); 316 if (t == null) 317 { 318 return null; 319 } 320 return use12HourFormat() ? t.get(ChronoField.CLOCK_HOUR_OF_AMPM) : t.getHour(); 321 } 322 323 @Override 324 public void setObject(Integer hour) 325 { 326 // ignored 327 } 328 } 329 330 protected class MinutesModel implements IModel<Integer> 331 { 332 private static final long serialVersionUID = 1L; 333 334 @Override 335 public Integer getObject() 336 { 337 LocalTime t = TimeField.this.getModelObject(); 338 return t == null ? null : t.getMinute(); 339 } 340 341 @Override 342 public void setObject(Integer minute) 343 { 344 // ignored 345 } 346 } 347 348 protected class AmPmModel implements IModel<AM_PM> 349 { 350 private static final long serialVersionUID = 1L; 351 352 @Override 353 public AM_PM getObject() 354 { 355 LocalTime t = TimeField.this.getModelObject(); 356 int i = t == null ? 0 : t.get(ChronoField.AMPM_OF_DAY); 357 return i == 0 ? AM_PM.AM : AM_PM.PM; 358 } 359 360 @Override 361 public void setObject(AM_PM amPm) 362 { 363 // ignored 364 } 365 } 366}