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.corelib.base.AbstractField; 021import org.apache.tapestry5.dom.Element; 022import org.apache.tapestry5.ioc.Messages; 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, date.getTime()); 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 */ 207 JSONObject onFormat(@RequestParameter(INPUT_PARAMETER) 208 String input) 209 { 210 JSONObject response = new JSONObject(); 211 212 try 213 { 214 long millis = Long.parseLong(input); 215 216 Date date = new Date(millis); 217 218 response.put(RESULT, format.format(date)); 219 } catch (NumberFormatException ex) 220 { 221 response.put(ERROR, ex.getMessage()); 222 } 223 224 return response; 225 } 226 227 void beginRender(MarkupWriter writer) 228 { 229 String value = validationTracker.getInput(this); 230 231 if (value == null) 232 { 233 value = formatCurrentValue(); 234 } 235 236 String clientId = getClientId(); 237 238 writer.element("div", 239 "data-component-type", "core/DateField", 240 "data-parse-url", resources.createEventLink("parse").toString(), 241 "data-format-url", resources.createEventLink("format").toString()); 242 243 if (!hideTextField) 244 { 245 writer.attributes("class", "input-group"); 246 } 247 248 Element field = writer.element("input", 249 250 "type", type, 251 252 "class", cssClass, 253 254 "name", getControlName(), 255 256 "id", clientId, 257 258 "value", value); 259 260 if (hideTextField) 261 { 262 field.attribute("class", "hide"); 263 } 264 265 writeDisabled(writer); 266 267 putPropertyNameIntoBeanValidationContext("value"); 268 269 validate.render(writer); 270 271 removePropertyNameFromBeanValidationContext(); 272 273 resources.renderInformalParameters(writer); 274 275 decorateInsideField(); 276 277 writer.end(); // input 278 279 if (!hideTextField) 280 { 281 writer.element("span", "class", "input-group-btn"); 282 } 283 284 writer.element("button", 285 "type", "button", 286 "class", "btn btn-default", 287 "alt", "[Show]"); 288 289 writer.element("span", "class", "glyphicon glyphicon-calendar"); 290 writer.end(); // span 291 292 writer.end(); // button 293 294 if (!hideTextField) 295 { 296 writer.end(); // span.input-group-btn 297 } 298 299 writer.end(); // outer div 300 } 301 302 private void writeDisabled(MarkupWriter writer) 303 { 304 if (isDisabled()) 305 writer.attributes("disabled", "disabled"); 306 } 307 308 private String formatCurrentValue() 309 { 310 if (value == null) 311 return ""; 312 313 return format.format(value); 314 } 315 316 @Override 317 protected void processSubmission(String controlName) 318 { 319 String value = request.getParameter(controlName); 320 321 validationTracker.recordInput(this, value); 322 323 Date parsedValue = null; 324 325 try 326 { 327 if (InternalUtils.isNonBlank(value)) 328 parsedValue = format.parse(value); 329 } catch (ParseException ex) 330 { 331 validationTracker.recordError(this, messages.format("core-date-value-not-parseable", value)); 332 return; 333 } 334 335 putPropertyNameIntoBeanValidationContext("value"); 336 try 337 { 338 fieldValidationSupport.validate(parsedValue, resources, validate); 339 340 this.value = parsedValue; 341 } catch (ValidationException ex) 342 { 343 validationTracker.recordError(this, ex.getMessage()); 344 } 345 346 removePropertyNameFromBeanValidationContext(); 347 } 348 349 void injectResources(ComponentResources resources) 350 { 351 this.resources = resources; 352 } 353 354 @Override 355 public boolean isRequired() 356 { 357 return validate.isRequired(); 358 } 359}