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    &lt;c:url value="RestoAction.do" var="baseURL"/&gt;
058    &lt;form action='${baseURL}' method="post" class="user-input"&gt; 
059    <b>&lt;w:populate using="itemForEdit"&gt;</b>  
060    &lt;input name="Id" type="hidden"&gt;
061    &lt;table align="center"&gt;
062    &lt;tr&gt;
063     &lt;td&gt;&lt;label&gt;Name&lt;/label&gt; *&lt;/td&gt;
064     &lt;td&gt;&lt;input name="Name" type="text"&gt;&lt;/td&gt;
065    &lt;/tr&gt;
066    &lt;tr&gt;
067     &lt;td&gt;&lt;label&gt;Location&lt;/label&gt;&lt;/td&gt;
068     &lt;td&gt;&lt;input name="Location" type="text"&gt;&lt;/td&gt;
069    &lt;/tr&gt;
070    &lt;tr&gt;
071     &lt;td&gt;&lt;label&gt;Price&lt;/label&gt;&lt;/td&gt;
072     &lt;td&gt;&lt;input name="Price" type="text"&gt;&lt;/td&gt;
073    &lt;/tr&gt;
074    &lt;tr&gt;
075     &lt;td&gt;&lt;label&gt;Comment&lt;/label&gt;&lt;/td&gt;
076     &lt;td&gt;&lt;input name="Comment" type="text"&gt;&lt;/td&gt;
077    &lt;/tr&gt;
078    &lt;tr&gt;
079     &lt;td align="center" colspan=2&gt;
080      &lt;input type='submit' value="Edit"&gt;
081     &lt;/td&gt;
082    &lt;/tr&gt;
083    &lt;/table&gt;
084    <b>&lt;/w:populate&gt;</b>
085     &lt;tags:hiddenOperationParam/&gt;
086    &lt;/form&gt;
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>&lt;w:populate&gt;</b>
110    &lt;c:url value="AddMessageAction.do?Operation=Apply" var="baseURL"/&gt; 
111    &lt;form action='${baseURL}' method=post class="user-input"&gt;
112    &lt;table align="center"&gt;
113    &lt;tr&gt;
114     &lt;td&gt;
115      &lt;label&gt;Message&lt;/label&gt; *
116     &lt;/td&gt;
117    &lt;/tr&gt;
118    &lt;tr&gt;
119     &lt;td&gt;
120      &lt;textarea name="Message Body"&gt;
121      &lt;/textarea&gt;
122     &lt;/td&gt;
123    &lt;/tr&gt;
124    &lt;tr&gt;
125     &lt;td colspan=2&gt;
126      &lt;label&gt;Preview First ?&lt;/label&gt; &lt;input type="radio" name="Preview" value="true"&gt; Yes
127     &lt;/td&gt;
128    &lt;/tr&gt;
129    &lt;tr&gt;
130     &lt;td align="center" colspan=2&gt;
131      &lt;input type="submit" value="Add Message"&gt; 
132     &lt;/td&gt;
133    &lt;/tr&gt;
134    &lt;/table&gt;
135    &lt;/form&gt;
136    <b>&lt;/w:populate&gt;</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>&lt;input type='text' ... &gt;</tt> is allowed but 
165     <tt>&lt;input type=text ... &gt;</tt> is not
166     <li> for SELECT tags, the &lt;/option&gt; 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    }