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