001 package hirondelle.web4j.ui.tag; 002 003 import java.util.*; 004 import java.util.logging.*; 005 import javax.servlet.jsp.JspException; 006 import hirondelle.web4j.BuildImpl; 007 import hirondelle.web4j.action.Operation; 008 import hirondelle.web4j.request.Formats; 009 import hirondelle.web4j.util.Util; 010 import static hirondelle.web4j.util.Consts.EMPTY_STRING; 011 012 /** 013 Custom tag which populates form controls in a simple, elegant way. 014 015 <P>From the point of view of this tag, there are 3 sources of data for a form control: 016 <ul> 017 <li>the HTML defined in your JSP can define an initial default value 018 <li>Request parameter values 019 <li>a Model Object 020 </ul> 021 022 <P>For reference, here is the logic that defines which data source is used, and related 023 naming conventions : 024 <PRE> 025 if a Model Object of the given name is in any scope { 026 override the default HTML for each control 027 use the Model Object 028 (match control names to getXXX methods of the Model Object) 029 } 030 else if the request is a POST { 031 override the default HTML for each control 032 must populate <i>every</i> control using request parameter values 033 (match control names to request param names) 034 } 035 else if the request is a GET { 036 if control name has a matching req param name { 037 override the default HTML for each control 038 populate control using request parameter values 039 (match control names to request param names) 040 } 041 else { 042 use the default HTML for that control 043 } 044 } 045 </PRE> 046 047 <P><span class='highlight'>This tag simply wraps static HTML forms</span>. 048 This is very economical since it does not force the page author to completely 049 replace well-known static HTML with a large set of custom tags. 050 051 <h3>Example use case</h3> 052 This use case corresponds to either an 'add' or a 'change' of a Model Object. The <tt>using</tt> 053 attribute signifies that a 'change' case is possible. (This example works with 054 an {@link hirondelle.web4j.action.ActionTemplateListAndEdit} action.) 055 056 <PRE> 057 <c:url value="RestoAction.do" var="baseURL"/> 058 <form action='${baseURL}' method="post" class="user-input"> 059 <b><w:populate using="itemForEdit"></b> 060 <input name="Id" type="hidden"> 061 <table align="center"> 062 <tr> 063 <td><label>Name</label> *</td> 064 <td><input name="Name" type="text"></td> 065 </tr> 066 <tr> 067 <td><label>Location</label></td> 068 <td><input name="Location" type="text"></td> 069 </tr> 070 <tr> 071 <td><label>Price</label></td> 072 <td><input name="Price" type="text"></td> 073 </tr> 074 <tr> 075 <td><label>Comment</label></td> 076 <td><input name="Comment" type="text"></td> 077 </tr> 078 <tr> 079 <td align="center" colspan=2> 080 <input type='submit' value="Edit"> 081 </td> 082 </tr> 083 </table> 084 <b></w:populate></b> 085 <tags:hiddenOperationParam/> 086 </form> 087 </PRE> 088 089 Here, the <tt>itemForEdit</tt> Model Object has the following methods, corresponding to 090 the above populated controls : 091 <PRE> 092 public Id getId() {...} 093 public SafeText getName() {...} 094 public SafeText getLocation() {...} 095 public BigDecimal getPrice() {...} 096 public SafeText getComment() {...} 097 </PRE> 098 099 <h3>Example without <tt>using</tt> attribute</h3> 100 No <tt>using</tt> attribute is specified when : 101 <ul> 102 <li>only an 'add' operation is performed, and not a 'change' operation. 103 <li>or, only a <tt>Search</tt> {@link Operation} is performed. In this case, a form with <tt>method="GET"</tt> is used 104 to specify parameters to a <tt>SELECT</tt> statement. 105 </ul> 106 107 <P>Here is an example of a form used only for 'add' operations : 108 <PRE> 109 <b><w:populate></b> 110 <c:url value="AddMessageAction.do?Operation=Apply" var="baseURL"/> 111 <form action='${baseURL}' method=post class="user-input"> 112 <table align="center"> 113 <tr> 114 <td> 115 <label>Message</label> * 116 </td> 117 </tr> 118 <tr> 119 <td> 120 <textarea name="Message Body"> 121 </textarea> 122 </td> 123 </tr> 124 <tr> 125 <td colspan=2> 126 <label>Preview First ?</label> <input type="radio" name="Preview" value="true"> Yes 127 </td> 128 </tr> 129 <tr> 130 <td align="center" colspan=2> 131 <input type="submit" value="Add Message"> 132 </td> 133 </tr> 134 </table> 135 </form> 136 <b></w:populate></b> 137 </PRE> 138 139 <h3>Supported Controls</h3> 140 <P>The following form input items are called <em>supported controls</em> here, and 141 include all items which undergo population by this class : 142 <ul> 143 <li><tt>INPUT</tt> tags with type=<tt>text</tt>, <tt>password</tt>, <tt>radio</tt>, 144 <tt>checkbox</tt>, <tt>hidden</tt> 145 <li>HTML5 input tags with type=<tt>search</tt>, <tt>email</tt>, <tt>url</tt>, <tt>number</tt>, <tt>tel</tt>, <tt>color</tt>, <tt>range</tt> 146 <li><tt>SELECT</tt> tags 147 <li><tt>TEXTAREA</tt> tags 148 </ul> 149 150 <P>Population is implemented by editing these supported control attributes : 151 <ul> 152 <li>the <tt>checked</tt> attribute for INPUT tags of type <tt>radio</tt> 153 and <tt>checkbox</tt> 154 <li>the <tt>value</tt> attribute for the remaining INPUT tags (of the different types listed above) 155 <li>the <tt>selected</tt> attribute for OPTION tags appearing in a SELECT 156 <li>the body of a TEXTAREA tag 157 </ul> 158 159 <P>The body of this tag is HTML, with the following minor restrictions: 160 <ul> 161 <li>all supported controls must include a <tt>name</tt> attribute 162 <li>all supported INPUT controls must include a <tt>type</tt> attribute 163 <li>all attributes must be quoted, using either single or double quotes. For example, 164 <tt><input type='text' ... ></tt> is allowed but 165 <tt><input type=text ... ></tt> is not 166 <li> for SELECT tags, the </option> end tag is not optional, and must be included. 167 <li>INPUT tags with <tt>type='email'</tt> are treated as always being single-valued 168 <li>the repetitive form of <tt>selected='selected'</tt> can't be used 169 </ul> 170 171 HTML often allows alternate ways of expressing the exact same thing. 172 For example, the <tt>selected</tt> attribute can be expressed as <tt>selected='selected'</tt>, or simply as 173 the single word <tt>selected</tt> - this tag only accepts the second form, not the first. 174 These sorts of restrictions are a nuisance, and result from the way the framework parses HTML internally. 175 Sometimes you will need to tweak your form hypertext in order to let this tag parse the form correctly. Sorry about that. 176 177 <P><b>Warning: unfortunately, INPUT controls of type color and range can't represent nullable items in a database.</b> 178 This is because the (draft) HTML5 specification doesn't allow such controls to POST non-empty 179 values when forms are submitted. 180 The only workaround for this defect of the specification is to define magic values which map to null. 181 Use such controls with caution. 182 183 <h3>Prepopulating only portions of a form</h3> 184 There is no requirement that the entire HTML form be wrapped by this tag. If 185 desired, only part of a form may be placed in the body of this tag. This is useful 186 when some form controls take a fixed, static value. 187 188 <h3>Convention Regarding Control Names</h3> 189 This tag depends on a specfic convention to allow automatic 'binding' between supported controls 190 and corresponding <tt>getXXX</tt> methods of the Model Object. This convention is explained in 191 {@link hirondelle.web4j.request.RequestParameter}. 192 193 <h3>Deriving values from <tt>getXXX()</tt> methods of the Model Object</h3> 194 The return value is found. Any primitives are converted into corresponding wrapper 195 objects. The {@link hirondelle.web4j.request.Formats#objectToText} method is then used to 196 translate the object into text. If the return value of the <tt>getXXX</tt> is a 197 <tt>Collection</tt>, then the above is applied to each element. 198 199 <h3>Escaping special characters</h3> 200 When this tag assigns a text value to the content of an <tt>INPUT</tt> or <tt>TEXTAREA</tt> tag, then the 201 value is always escaped for special characters using {@link hirondelle.web4j.util.EscapeChars#forHTML(String)}. 202 203 <h3><tt>GET</tt> versus <tt>POST</tt></h3> 204 This tag depends on the proper <tt>GET/POST</tt> behavior of forms : a <tt>POST</tt> 205 request must only be used when an edit to the database is being attempted. (This is the usual style, 206 and would not be regarded by most as being a restriction.) 207 */ 208 public final class Populate extends TagHelper { 209 210 /** 211 Key for the Model Object to be used for form population. 212 213 <P>This attribute is specified only if the form can be used to edit or change an 214 existing Model Object. If the Model Object is present, then it will be used by this tag to 215 populate supported controls. 216 217 <P>This tag searches for the Model Object in the same way as <tt>JspContext.findAttribute(String)</tt>, 218 by searching scopes in a specific order : page scope, request scope, session scope, and finally 219 application scope. 220 221 @param aModelObjectKey satisfies {@link Util#textHasContent(String)}. 222 */ 223 public void setUsing(String aModelObjectKey){ 224 checkForContent("Using", aModelObjectKey); 225 fModel = getPageContext().findAttribute(aModelObjectKey); 226 } 227 228 /** 229 Emit the possibly-changed body of this tag, by possibly editing supported form controls 230 contained in the body of this tag. 231 */ 232 @Override protected String getEmittedText(String aOriginalBody) throws JspException { 233 String result = null; 234 setUseCaseStyle(); 235 if ( Style.ECHO == fStyle ){ 236 result = aOriginalBody; 237 } 238 else { 239 result = getEditedBody(aOriginalBody, fStyle); 240 } 241 return result; 242 } 243 244 // PRIVATE 245 246 /** 247 The ModelObject which is to be used to populate supported controls in the "edit" use case. 248 Is identified by the value of the 'using' attribute 249 */ 250 private Object fModel; 251 252 /** Use case style */ 253 private Style fStyle; 254 255 enum Style {ECHO, USE_MODEL_OBJECT, MUST_RECYCLE_PARAMS, RECYCLE_PARAM_IF_PRESENT} 256 257 private static final String GET = "GET"; 258 private static final String POST = "POST"; 259 private static final Logger fLogger = Util.getLogger(Populate.class); 260 261 private void setUseCaseStyle(){ 262 String PREAMBLE = "Form population use case: "; 263 if( fModel != null ) { 264 fLogger.fine(PREAMBLE +"'Using' object is specified and present. All controls will be populated using getXXX methods of the 'using' object."); 265 fStyle = Style.USE_MODEL_OBJECT; 266 } 267 else if( isRequest(POST) ){ 268 /* Minor Problem: delete ids infecting ADD operations. */ 269 fLogger.fine(PREAMBLE + "POST. All controls will be populated using request parameter values."); 270 fStyle = Style.MUST_RECYCLE_PARAMS; 271 } 272 else if( isRequest(GET) ) { 273 if ( hasNoRequestParameters() ) { 274 fLogger.fine(PREAMBLE + "GET, with no request parameters present. Echoing the HTML of entire form as is."); 275 fStyle = Style.ECHO; 276 } 277 else { 278 fLogger.fine(PREAMBLE + "GET. Any request parameter whose name matches a form control will be used to populate that control."); 279 fStyle = Style.RECYCLE_PARAM_IF_PRESENT; 280 } 281 } 282 else { 283 throw new AssertionError("Unexpected use case."); 284 } 285 } 286 287 private boolean isRequest(String aRequestStyle){ 288 return getRequest().getMethod().equalsIgnoreCase(aRequestStyle); 289 } 290 291 private String getEditedBody(String aOriginalBody, Style aUseCaseStyle) throws JspException { 292 PopulateHelper populator = new PopulateHelper(new Wrapper(), aOriginalBody, aUseCaseStyle); 293 return populator.getEditedBody(); 294 } 295 296 /** This exists solely to provide a particular 'view' of this object that does not leak into the public API. */ 297 private class Wrapper implements PopulateHelper.Context { 298 public String getReqParamValue(String aParamName){ 299 String value = getRequest().getParameter(aParamName); 300 return value == null ? EMPTY_STRING : value; 301 } 302 public Collection<String> getReqParamValues(String aParamName){ 303 Collection<String> result = Collections.emptyList(); //default return value 304 String[] values = getRequest().getParameterValues(aParamName); 305 if ( values != null ) { 306 result = Collections.unmodifiableCollection( Arrays.asList(values) ); 307 } 308 return result; 309 } 310 public boolean hasRequestParamNamed(String aParamName) { 311 boolean result = false; 312 Enumeration allParamNames = getRequest().getParameterNames(); 313 while (allParamNames.hasMoreElements()){ 314 if (allParamNames.nextElement().equals(aParamName)) { 315 result = true; 316 break; 317 } 318 } 319 return result; 320 } 321 public boolean isModelObjectPresent(){ 322 return fModel != null; 323 } 324 public Object getModelObject(){ 325 return fModel; 326 } 327 public Formats getFormats(){ 328 Locale locale = BuildImpl.forLocaleSource().get(getRequest()); 329 TimeZone timeZone = BuildImpl.forTimeZoneSource().get(getRequest()); 330 return new Formats(locale, timeZone); 331 } 332 } 333 334 private boolean hasNoRequestParameters(){ 335 Enumeration namesEnum = getRequest().getParameterNames(); 336 int numParams = 0; 337 while ( namesEnum.hasMoreElements() ){ 338 ++numParams; 339 namesEnum.nextElement(); 340 } 341 return numParams == 0; 342 } 343 }