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 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>&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    </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    }