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