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 standard 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 </ul>
169
170 <P><b>Warning: unfortunately, INPUT controls of type color and range can't represent nullable items in a database.</b>
171 This is because the (draft) HTML5 specification doesn't allow such controls to POST non-empty
172 values when forms are submitted.
173 The only workaround for this defect of the specification is to define magic values which map to null.
174 Use such controls with caution.
175
176 <h3>Prepopulating only portions of a form</h3>
177 There is no requirement that the entire HTML form be wrapped by this tag. If
178 desired, only part of a form may be placed in the body of this tag. This is useful
179 when some form controls take a fixed, static value.
180
181 <h3>Convention Regarding Control Names</h3>
182 This tag depends on a specfic convention to allow automatic 'binding' between supported controls
183 and corresponding <tt>getXXX</tt> methods of the Model Object. This convention is explained in
184 {@link hirondelle.web4j.request.RequestParameter}.
185
186 <h3>Deriving values from <tt>getXXX()</tt> methods of the Model Object</h3>
187 The return value is found. Any primitives are converted into corresponding wrapper
188 objects. The {@link hirondelle.web4j.request.Formats#objectToText} method is then used to
189 translate the object into text. If the return value of the <tt>getXXX</tt> is a
190 <tt>Collection</tt>, then the above is applied to each element.
191
192 <h3>Escaping special characters</h3>
193 When this tag assigns a text value to the content of an <tt>INPUT</tt> or <tt>TEXTAREA</tt> tag, then the
194 value is always escaped for special characters using {@link hirondelle.web4j.util.EscapeChars#forHTML(String)}.
195
196 <h3><tt>GET</tt> versus <tt>POST</tt></h3>
197 This tag depends on the proper <tt>GET/POST</tt> behavior of forms : a <tt>POST</tt>
198 request must only be used when an edit to the database is being attempted. (This is the usual style,
199 and would not be regarded by most as being a restriction.)
200 */
201 public final class Populate extends TagHelper {
202
203 /**
204 Key for the Model Object to be used for form population.
205
206 <P>This attribute is specified only if the form can be used to edit or change an
207 existing Model Object. If the Model Object is present, then it will be used by this tag to
208 populate supported controls.
209
210 <P>This tag searches for the Model Object in the same way as <tt>JspContext.findAttribute(String)</tt>,
211 by searching scopes in a specific order : page scope, request scope, session scope, and finally
212 application scope.
213
214 @param aModelObjectKey satisfies {@link Util#textHasContent(String)}.
215 */
216 public void setUsing(String aModelObjectKey){
217 checkForContent("Using", aModelObjectKey);
218 fModel = getPageContext().findAttribute(aModelObjectKey);
219 }
220
221 /**
222 Emit the possibly-changed body of this tag, by possibly editing supported form controls
223 contained in the body of this tag.
224 */
225 @Override protected String getEmittedText(String aOriginalBody) throws JspException {
226 String result = null;
227 setUseCaseStyle();
228 if ( Style.ECHO == fStyle ){
229 result = aOriginalBody;
230 }
231 else {
232 result = getEditedBody(aOriginalBody, fStyle);
233 }
234 return result;
235 }
236
237 // PRIVATE
238
239 /**
240 The ModelObject which is to be used to populate supported controls in the "edit" use case.
241 Is identified by the value of the 'using' attribute
242 */
243 private Object fModel;
244
245 /** Use case style */
246 private Style fStyle;
247
248 enum Style {ECHO, USE_MODEL_OBJECT, MUST_RECYCLE_PARAMS, RECYCLE_PARAM_IF_PRESENT}
249
250 private static final String GET = "GET";
251 private static final String POST = "POST";
252 private static final Logger fLogger = Util.getLogger(Populate.class);
253
254 private void setUseCaseStyle(){
255 String PREAMBLE = "Form population use case: ";
256 if( fModel != null ) {
257 fLogger.fine(PREAMBLE +"'Using' object is specified and present. All controls will be populated using getXXX methods of the 'using' object.");
258 fStyle = Style.USE_MODEL_OBJECT;
259 }
260 else if( isRequest(POST) ){
261 /* Minor Problem: delete ids infecting ADD operations. */
262 fLogger.fine(PREAMBLE + "POST. All controls will be populated using request parameter values.");
263 fStyle = Style.MUST_RECYCLE_PARAMS;
264 }
265 else if( isRequest(GET) ) {
266 if ( hasNoRequestParameters() ) {
267 fLogger.fine(PREAMBLE + "GET, with no request parameters present. Echoing the HTML of entire form as is.");
268 fStyle = Style.ECHO;
269 }
270 else {
271 fLogger.fine(PREAMBLE + "GET. Any request parameter whose name matches a form control will be used to populate that control.");
272 fStyle = Style.RECYCLE_PARAM_IF_PRESENT;
273 }
274 }
275 else {
276 throw new AssertionError("Unexpected use case.");
277 }
278 }
279
280 private boolean isRequest(String aRequestStyle){
281 return getRequest().getMethod().equalsIgnoreCase(aRequestStyle);
282 }
283
284 private String getEditedBody(String aOriginalBody, Style aUseCaseStyle) throws JspException {
285 PopulateHelper populator = new PopulateHelper(new Wrapper(), aOriginalBody, aUseCaseStyle);
286 return populator.getEditedBody();
287 }
288
289 /** This exists solely to provide a particular 'view' of this object that does not leak into the public API. */
290 private class Wrapper implements PopulateHelper.Context {
291 public String getReqParamValue(String aParamName){
292 String value = getRequest().getParameter(aParamName);
293 return value == null ? EMPTY_STRING : value;
294 }
295 public Collection<String> getReqParamValues(String aParamName){
296 Collection<String> result = Collections.emptyList(); //default return value
297 String[] values = getRequest().getParameterValues(aParamName);
298 if ( values != null ) {
299 result = Collections.unmodifiableCollection( Arrays.asList(values) );
300 }
301 return result;
302 }
303 public boolean hasRequestParamNamed(String aParamName) {
304 boolean result = false;
305 Enumeration allParamNames = getRequest().getParameterNames();
306 while (allParamNames.hasMoreElements()){
307 if (allParamNames.nextElement().equals(aParamName)) {
308 result = true;
309 break;
310 }
311 }
312 return result;
313 }
314 public boolean isModelObjectPresent(){
315 return fModel != null;
316 }
317 public Object getModelObject(){
318 return fModel;
319 }
320 public Formats getFormats(){
321 Locale locale = BuildImpl.forLocaleSource().get(getRequest());
322 TimeZone timeZone = BuildImpl.forTimeZoneSource().get(getRequest());
323 return new Formats(locale, timeZone);
324 }
325 }
326
327 private boolean hasNoRequestParameters(){
328 Enumeration namesEnum = getRequest().getParameterNames();
329 int numParams = 0;
330 while ( namesEnum.hasMoreElements() ){
331 ++numParams;
332 namesEnum.nextElement();
333 }
334 return numParams == 0;
335 }
336 }