001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.corelib.components; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.annotations.Events; 017import org.apache.tapestry5.annotations.Import; 018import org.apache.tapestry5.annotations.Parameter; 019import org.apache.tapestry5.annotations.RequestParameter; 020import org.apache.tapestry5.commons.Messages; 021import org.apache.tapestry5.corelib.base.AbstractField; 022import org.apache.tapestry5.dom.Element; 023import org.apache.tapestry5.ioc.annotations.Inject; 024import org.apache.tapestry5.ioc.annotations.Symbol; 025import org.apache.tapestry5.ioc.internal.util.InternalUtils; 026import org.apache.tapestry5.json.JSONObject; 027import org.apache.tapestry5.services.ComponentDefaultProvider; 028import org.apache.tapestry5.services.compatibility.DeprecationWarning; 029 030import java.text.DateFormat; 031import java.text.ParseException; 032import java.text.SimpleDateFormat; 033import java.util.Date; 034import java.util.Locale; 035 036/** 037 * A component used to collect a provided date from the user using a client-side JavaScript calendar. Non-JavaScript 038 * clients can simply type into a text field. 039 * 040 * One aspect here is that, because client-side JavaScript formatting and parsing is so limited, we (currently) 041 * use Ajax to send the user's input to the server for parsing (before raising the popup) and formatting (after closing 042 * the popup). Weird and inefficient, but easier than writing client-side JavaScript for that purpose. 043 * 044 * Tapestry's DateField component is a wrapper around <a 045 * href="http://webfx.eae.net/dhtml/datepicker/datepicker.html">WebFX DatePicker</a>. 046 * 047 * @tapestrydoc 048 * @see Form 049 * @see TextField 050 */ 051// TODO: More testing; see https://issues.apache.org/jira/browse/TAPESTRY-1844 052@Import(stylesheet = "${tapestry.datepicker}/css/datepicker.css", 053 module = "t5/core/datefield") 054@Events(EventConstants.VALIDATE) 055public class DateField extends AbstractField 056{ 057 /** 058 * The value parameter of a DateField must be a {@link java.util.Date}. 059 */ 060 @Parameter(required = true, principal = true, autoconnect = true) 061 private Date value; 062 063 /** 064 * The format used to format <em>and parse</em> dates. This is typically specified as a string which is coerced to a 065 * DateFormat. You should be aware that using a date format with a two digit year is problematic: Java (not 066 * Tapestry) may get confused about the century. 067 */ 068 @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL) 069 private DateFormat format; 070 071 /** 072 * Allows the type of field to be output; normally this is "text", but can be updated to "date" or "datetime" 073 * as per the HTML 5 specification. 074 * 075 * @since 5.4 076 */ 077 @Parameter(allowNull = false, defaultPrefix = BindingConstants.LITERAL, value = "text") 078 private String type; 079 080 /** 081 * When the <code>format</code> parameter isn't used, this parameter defines whether the 082 * <code>DateFormat</code> created by this component will be lenient or not. 083 * The default value of this parameter is the value of the {@link SymbolConstants#LENIENT_DATE_FORMAT} 084 * symbol. 085 * 086 * @see DateFormat#setLenient(boolean) 087 * @see SymbolConstants#LENIENT_DATE_FORMAT 088 * @since 5.4 089 */ 090 @Parameter(principal = true) 091 private boolean lenient; 092 093 /** 094 * If true, then the text field will be hidden, and only the icon for the date picker will be visible. The default 095 * is false. 096 */ 097 @Parameter 098 private boolean hideTextField; 099 100 /** 101 * The object that will perform input validation (which occurs after translation). The translate binding prefix is 102 * generally used to provide this object in a declarative fashion. 103 */ 104 @Parameter(defaultPrefix = BindingConstants.VALIDATE) 105 @SuppressWarnings("unchecked") 106 private FieldValidator<Object> validate; 107 108 /** 109 * Icon used for the date field trigger button. This was used in Tapestry 5.3 and earlier and is now ignored. 110 * 111 * @deprecated Deprecated in 5.4 with no replacement. The component leverages the Twitter Bootstrap glyphicons support. 112 */ 113 @Parameter(defaultPrefix = BindingConstants.ASSET) 114 private Asset icon; 115 116 /** 117 * Used to override the component's message catalog. 118 * 119 * @since 5.2.0.0 120 * @deprecated Since 5.4; override the global message key "core-date-value-not-parsable" instead (see {@link org.apache.tapestry5.services.messages.ComponentMessagesSource}) 121 */ 122 @Parameter("componentResources.messages") 123 private Messages messages; 124 125 @Inject 126 private Locale locale; 127 128 @Inject 129 private DeprecationWarning deprecationWarning; 130 131 @Inject 132 @Symbol(SymbolConstants.LENIENT_DATE_FORMAT) 133 private boolean lenientDateFormatSymbolValue; 134 135 private static final String RESULT = "result"; 136 137 private static final String ERROR = "error"; 138 private static final String INPUT_PARAMETER = "input"; 139 140 void pageLoaded() 141 { 142 deprecationWarning.ignoredComponentParameters(resources, "icon"); 143 } 144 145 DateFormat defaultFormat() 146 { 147 DateFormat shortDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale); 148 149 if (shortDateFormat instanceof SimpleDateFormat) 150 { 151 SimpleDateFormat simpleDateFormat = (SimpleDateFormat) shortDateFormat; 152 153 String pattern = simpleDateFormat.toPattern(); 154 155 String revised = pattern.replaceAll("([^y])yy$", "$1yyyy"); 156 157 final SimpleDateFormat revisedDateFormat = new SimpleDateFormat(revised); 158 revisedDateFormat.setLenient(lenient); 159 return revisedDateFormat; 160 } 161 162 return shortDateFormat; 163 } 164 165 /** 166 * Computes a default value for the "validate" parameter using {@link ComponentDefaultProvider}. 167 */ 168 final Binding defaultValidate() 169 { 170 return defaultProvider.defaultValidatorBinding("value", resources); 171 } 172 173 final boolean defaultLenient() 174 { 175 return lenientDateFormatSymbolValue; 176 } 177 178 /** 179 * Ajax event handler, used when initiating the popup. The client sends the input value form the field to the server 180 * to parse it according to the server-side format. The response contains a "result" key of the formatted date in a 181 * format acceptable to the JavaScript Date() constructor. Alternately, an "error" key indicates the the input was 182 * not formatted correct. 183 */ 184 JSONObject onParse(@RequestParameter(INPUT_PARAMETER) 185 String input) 186 { 187 JSONObject response = new JSONObject(); 188 189 try 190 { 191 Date date = format.parse(input); 192 193 response.put(RESULT, new SimpleDateFormat("yyyy-MM-dd").format(date)); 194 } catch (ParseException ex) 195 { 196 response.put(ERROR, ex.getMessage()); 197 } 198 199 return response; 200 } 201 202 /** 203 * Ajax event handler, used after the client-side popup completes. The client sends the date, formatted as 204 * milliseconds since the epoch, to the server, which reformats it according to the server side format and returns 205 * the result. 206 * @throws ParseException 207 */ 208 JSONObject onFormat(@RequestParameter(INPUT_PARAMETER) 209 String input) throws ParseException 210 { 211 JSONObject response = new JSONObject(); 212 213 try 214 { 215 Date date = new SimpleDateFormat("yyyy-MM-dd").parse(input); 216 217 response.put(RESULT, format.format(date)); 218 } catch (NumberFormatException ex) 219 { 220 response.put(ERROR, ex.getMessage()); 221 } 222 223 return response; 224 } 225 226 void beginRender(MarkupWriter writer) 227 { 228 String value = validationTracker.getInput(this); 229 230 if (value == null) 231 { 232 value = formatCurrentValue(); 233 } 234 235 String clientId = getClientId(); 236 237 writer.element("div", 238 "data-component-type", "core/DateField", 239 "data-parse-url", resources.createEventLink("parse").toString(), 240 "data-format-url", resources.createEventLink("format").toString()); 241 242 if (!hideTextField) 243 { 244 writer.attributes("class", "input-group"); 245 } 246 247 Element field = writer.element("input", 248 249 "type", type, 250 251 "class", cssClass, 252 253 "name", getControlName(), 254 255 "id", clientId, 256 257 "value", value); 258 259 if (hideTextField) 260 { 261 field.attribute("class", "hide"); 262 } 263 264 writeDisabled(writer); 265 266 putPropertyNameIntoBeanValidationContext("value"); 267 268 validate.render(writer); 269 270 removePropertyNameFromBeanValidationContext(); 271 272 resources.renderInformalParameters(writer); 273 274 decorateInsideField(); 275 276 writer.end(); // input 277 278 if (!hideTextField) 279 { 280 writer.element("span", "class", "input-group-btn"); 281 } 282 283 writer.element("button", 284 "type", "button", 285 "class", "btn btn-default", 286 "alt", "[Show]"); 287 288 writer.element("span", "class", "glyphicon glyphicon-calendar"); 289 writer.end(); // span 290 291 writer.end(); // button 292 293 if (!hideTextField) 294 { 295 writer.end(); // span.input-group-btn 296 } 297 298 writer.end(); // outer div 299 } 300 301 private void writeDisabled(MarkupWriter writer) 302 { 303 if (isDisabled()) 304 writer.attributes("disabled", "disabled"); 305 } 306 307 private String formatCurrentValue() 308 { 309 if (value == null) 310 return ""; 311 312 return format.format(value); 313 } 314 315 @Override 316 protected void processSubmission(String controlName) 317 { 318 String value = request.getParameter(controlName); 319 320 validationTracker.recordInput(this, value); 321 322 Date parsedValue = null; 323 324 try 325 { 326 if (InternalUtils.isNonBlank(value)) 327 parsedValue = format.parse(value); 328 } catch (ParseException ex) 329 { 330 validationTracker.recordError(this, messages.format("core-date-value-not-parseable", value)); 331 return; 332 } 333 334 putPropertyNameIntoBeanValidationContext("value"); 335 try 336 { 337 fieldValidationSupport.validate(parsedValue, resources, validate); 338 339 this.value = parsedValue; 340 } catch (ValidationException ex) 341 { 342 validationTracker.recordError(this, ex.getMessage()); 343 } 344 345 removePropertyNameFromBeanValidationContext(); 346 } 347 348 void injectResources(ComponentResources resources) 349 { 350 this.resources = resources; 351 } 352 353 @Override 354 public boolean isRequired() 355 { 356 return validate.isRequired(); 357 } 358}