001 // Copyright 2007, 2008, 2009, 2010, 2011 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 015 package org.apache.tapestry5.corelib.components; 016 017 import org.apache.tapestry5.*; 018 import org.apache.tapestry5.annotations.*; 019 import org.apache.tapestry5.corelib.base.AbstractField; 020 import org.apache.tapestry5.ioc.Messages; 021 import org.apache.tapestry5.ioc.annotations.Inject; 022 import org.apache.tapestry5.ioc.internal.util.InternalUtils; 023 import org.apache.tapestry5.json.JSONObject; 024 import org.apache.tapestry5.services.ComponentDefaultProvider; 025 import org.apache.tapestry5.services.Request; 026 import org.apache.tapestry5.services.javascript.JavaScriptSupport; 027 028 import java.text.DateFormat; 029 import java.text.ParseException; 030 import java.text.SimpleDateFormat; 031 import java.util.Date; 032 import java.util.Locale; 033 034 /** 035 * A component used to collect a provided date from the user using a client-side JavaScript calendar. Non-JavaScript 036 * clients can simply type into a text field. 037 * <p/> 038 * One wierd aspect here is that, because client-side JavaScript formatting and parsing is so limited, we (currently) 039 * use Ajax to send the user's input to the server for parsing (before raising the popup) and formatting (after closing 040 * the popup). Weird and inefficient, but easier than writing client-side JavaScript for that purpose. 041 * <p/> 042 * Tapestry's DateField component is a wrapper around <a 043 * href="http://webfx.eae.net/dhtml/datepicker/datepicker.html">WebFX DatePicker</a>. 044 * 045 * @tapestrydoc 046 * @see Form 047 * @see TextField 048 */ 049 // TODO: More testing; see https://issues.apache.org/jira/browse/TAPESTRY-1844 050 @Import(stack = "core-datefield") 051 @Events(EventConstants.VALIDATE) 052 public class DateField extends AbstractField 053 { 054 /** 055 * The value parameter of a DateField must be a {@link java.util.Date}. 056 */ 057 @Parameter(required = true, principal = true, autoconnect = true) 058 private Date value; 059 060 /** 061 * The format used to format <em>and parse</em> dates. This is typically specified as a string which is coerced to a 062 * DateFormat. You should be aware that using a date format with a two digit year is problematic: Java (not 063 * Tapestry) may get confused about the century. 064 */ 065 @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL) 066 private DateFormat format; 067 068 /** 069 * If true, then the text field will be hidden, and only the icon for the date picker will be visible. The default 070 * is false. 071 */ 072 @Parameter 073 private boolean hideTextField; 074 075 /** 076 * The object that will perform input validation (which occurs after translation). The translate binding prefix is 077 * generally used to provide this object in a declarative fashion. 078 */ 079 @Parameter(defaultPrefix = BindingConstants.VALIDATE) 080 @SuppressWarnings("unchecked") 081 private FieldValidator<Object> validate; 082 083 @Parameter(defaultPrefix = BindingConstants.ASSET, value = "datefield.gif") 084 private Asset icon; 085 086 /** 087 * Used to override the component's message catalog. 088 * 089 * @since 5.2.0.0 090 */ 091 @Parameter("componentResources.messages") 092 private Messages messages; 093 094 @Environmental 095 private JavaScriptSupport support; 096 097 @Environmental 098 private ValidationTracker tracker; 099 100 @Inject 101 private ComponentResources resources; 102 103 @Inject 104 private Request request; 105 106 @Inject 107 private Locale locale; 108 109 @Inject 110 private ComponentDefaultProvider defaultProvider; 111 112 @Inject 113 private FieldValidationSupport fieldValidationSupport; 114 115 private static final String RESULT = "result"; 116 117 private static final String ERROR = "error"; 118 private static final String INPUT_PARAMETER = "input"; 119 120 DateFormat defaultFormat() 121 { 122 DateFormat shortDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale); 123 124 if (shortDateFormat instanceof SimpleDateFormat) 125 { 126 SimpleDateFormat simpleDateFormat = (SimpleDateFormat) shortDateFormat; 127 128 String pattern = simpleDateFormat.toPattern(); 129 130 String revised = pattern.replaceAll("([^y])yy$", "$1yyyy"); 131 132 return new SimpleDateFormat(revised); 133 } 134 135 return shortDateFormat; 136 } 137 138 /** 139 * Computes a default value for the "validate" parameter using {@link ComponentDefaultProvider}. 140 */ 141 final Binding defaultValidate() 142 { 143 return defaultProvider.defaultValidatorBinding("value", resources); 144 } 145 146 /** 147 * Ajax event handler, used when initiating the popup. The client sends the input value form the field to the server 148 * to parse it according to the server-side format. The response contains a "result" key of the formatted date in a 149 * format acceptable to the JavaScript Date() constructor. Alternately, an "error" key indicates the the input was 150 * not formatted correct. 151 */ 152 JSONObject onParse(@RequestParameter(INPUT_PARAMETER) 153 String input) 154 { 155 JSONObject response = new JSONObject(); 156 157 try 158 { 159 Date date = format.parse(input); 160 161 response.put(RESULT, date.getTime()); 162 } catch (ParseException ex) 163 { 164 response.put(ERROR, ex.getMessage()); 165 } 166 167 return response; 168 } 169 170 /** 171 * Ajax event handler, used after the client-side popup completes. The client sends the date, formatted as 172 * milliseconds since the epoch, to the server, which reformats it according to the server side format and returns 173 * the result. 174 */ 175 JSONObject onFormat(@RequestParameter(INPUT_PARAMETER) 176 String input) 177 { 178 JSONObject response = new JSONObject(); 179 180 try 181 { 182 long millis = Long.parseLong(input); 183 184 Date date = new Date(millis); 185 186 response.put(RESULT, format.format(date)); 187 } catch (NumberFormatException ex) 188 { 189 response.put(ERROR, ex.getMessage()); 190 } 191 192 return response; 193 } 194 195 void beginRender(MarkupWriter writer) 196 { 197 String value = tracker.getInput(this); 198 199 if (value == null) 200 value = formatCurrentValue(); 201 202 String clientId = getClientId(); 203 String triggerId = clientId + "-trigger"; 204 205 writer.element("input", 206 207 "type", hideTextField ? "hidden" : "text", 208 209 "name", getControlName(), 210 211 "id", clientId, 212 213 "value", value); 214 215 writeDisabled(writer); 216 217 putPropertyNameIntoBeanValidationContext("value"); 218 219 validate.render(writer); 220 221 removePropertyNameFromBeanValidationContext(); 222 223 resources.renderInformalParameters(writer); 224 225 decorateInsideField(); 226 227 writer.end(); 228 229 // Now the trigger icon. 230 231 writer.element("img", 232 233 "id", triggerId, 234 235 "class", "t-calendar-trigger", 236 237 "src", icon.toClientURL(), 238 239 "alt", "[Show]"); 240 writer.end(); // img 241 242 JSONObject spec = new JSONObject(); 243 244 spec.put("field", clientId); 245 spec.put("parseURL", resources.createEventLink("parse").toURI()); 246 spec.put("formatURL", resources.createEventLink("format").toURI()); 247 248 support.addInitializerCall("dateField", spec); 249 } 250 251 private void writeDisabled(MarkupWriter writer) 252 { 253 if (isDisabled()) 254 writer.attributes("disabled", "disabled"); 255 } 256 257 private String formatCurrentValue() 258 { 259 if (value == null) 260 return ""; 261 262 return format.format(value); 263 } 264 265 @Override 266 protected void processSubmission(String controlName) 267 { 268 String value = request.getParameter(controlName); 269 270 tracker.recordInput(this, value); 271 272 Date parsedValue = null; 273 274 try 275 { 276 if (InternalUtils.isNonBlank(value)) 277 parsedValue = format.parse(value); 278 } catch (ParseException ex) 279 { 280 tracker.recordError(this, messages.format("date-value-not-parseable", value)); 281 return; 282 } 283 284 putPropertyNameIntoBeanValidationContext("value"); 285 try 286 { 287 fieldValidationSupport.validate(parsedValue, resources, validate); 288 289 this.value = parsedValue; 290 } catch (ValidationException ex) 291 { 292 tracker.recordError(this, ex.getMessage()); 293 } 294 295 removePropertyNameFromBeanValidationContext(); 296 } 297 298 void injectResources(ComponentResources resources) 299 { 300 this.resources = resources; 301 } 302 303 @Override 304 public boolean isRequired() 305 { 306 return validate.isRequired(); 307 } 308 }