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    &lt;c:url value="RestoAction.do" var="baseURL"/&gt;
058    <b>&lt;w:populate using="itemForEdit"&gt;</b>  
059    &lt;form action='${baseURL}' method="post" class="user-input"&gt; 
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    <b>&lt;/w:populate&gt;</b>
079    &lt;tr&gt;
080     &lt;td align="center" colspan=2&gt;
081      &lt;input type=submit value="Edit"&gt;
082     &lt;/td&gt;
083    &lt;/tr&gt;
084    &lt;/table&gt;
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    <b>&lt;/w:populate&gt;</b>
125    &lt;tr&gt;
126     &lt;td colspan=2&gt;
127      &lt;label&gt;Preview First ?&lt;/label&gt; &lt;input type="radio" name="Preview" value="true"&gt; Yes
128     &lt;/td&gt;
129    &lt;/tr&gt;
130    &lt;tr&gt;
131     &lt;td align="center" colspan=2&gt;
132      &lt;input type="submit" value="Add Message"&gt; 
133     &lt;/td&gt;
134    &lt;/tr&gt;
135    &lt;/table&gt;
136    &lt;/form&gt;
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>&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 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    }