package hirondelle.web4j.ui.tag;
import java.util.*;
import java.util.logging.*;
import javax.servlet.jsp.JspException;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.action.Operation;
import hirondelle.web4j.request.Formats;
import hirondelle.web4j.util.Util;
import static hirondelle.web4j.util.Consts.EMPTY_STRING;
/**
Custom tag which populates form controls in a simple, elegant way.
<P>From the point of view of this tag, there are 3 sources of data for a form control:
<ul>
<li>the HTML defined in your JSP can define an initial default value
<li>Request paramter values
<li>a Model Object
</ul>
<P>For reference, here is the logic that defines which data source is used, and related
naming conventions :
<PRE>
if a Model Object of the given name is in any scope {
override the default HTML for each control
use the Model Object
(match control names to getXXX methods of the Model Object)
}
else if the request is a POST {
override the default HTML for each control
must populate <i>every</i> control using request parameter values
(match control names to request param names)
}
else if the request is a GET {
if control name has a matching req param name {
override the default HTML for each control
populate control using request parameter values
(match control names to request param names)
}
else {
use the default HTML for that control
}
}
</PRE>
<P><span class='highlight'>This tag simply wraps static HTML forms</span>.
This is very economical since it does not force the page author to completely
replace well-known static HTML with a large set of custom tags.
<h3>Example use case</h3>
This use case corresponds to either an 'add' or a 'change' of a Model Object. The <tt>using</tt>
attribute signifies that a 'change' case is possible. (This example works with
an {@link hirondelle.web4j.action.ActionTemplateListAndEdit} action.)
<PRE>
<c:url value="RestoAction.do" var="baseURL"/>
<b><w:populate using="itemForEdit"></b>
<form action='${baseURL}' method="post" class="user-input">
<input name="Id" type="hidden">
<table align="center">
<tr>
<td><label>Name</label> *</td>
<td><input name="Name" type="text"></td>
</tr>
<tr>
<td><label>Location</label></td>
<td><input name="Location" type="text"></td>
</tr>
<tr>
<td><label>Price</label></td>
<td><input name="Price" type="text"></td>
</tr>
<tr>
<td><label>Comment</label></td>
<td><input name="Comment" type="text"></td>
</tr>
<b></w:populate></b>
<tr>
<td align="center" colspan=2>
<input type=submit value="Edit">
</td>
</tr>
</table>
<tags:hiddenOperationParam/>
</form>
</PRE>
Here, the <tt>itemForEdit</tt> Model Object has the following methods, corresponding to
the above populated controls :
<PRE>
public Id getId() {...}
public SafeText getName() {...}
public SafeText getLocation() {...}
public BigDecimal getPrice() {...}
public SafeText getComment() {...}
</PRE>
<h3>Example without <tt>using</tt> attribute</h3>
No <tt>using</tt> attribute is specified when :
<ul>
<li>only an 'add' operation is performed, and not a 'change' operation.
<li>or, only a <tt>Search</tt> {@link Operation} is performed. In this case, a form with <tt>method="GET"</tt> is used
to specify parameters to a <tt>SELECT</tt> statement.
</ul>
<P>Here is an example of a form used only for 'add' operations :
<PRE>
<b><w:populate></b>
<c:url value="AddMessageAction.do?Operation=Apply" var="baseURL"/>
<form action='${baseURL}' method=post class="user-input">
<table align="center">
<tr>
<td>
<label>Message</label> *
</td>
</tr>
<tr>
<td>
<textarea name="Message Body">
</textarea>
</td>
</tr>
<b></w:populate></b>
<tr>
<td colspan=2>
<label>Preview First ?</label> <input type="radio" name="Preview" value="true"> Yes
</td>
</tr>
<tr>
<td align="center" colspan=2>
<input type="submit" value="Add Message">
</td>
</tr>
</table>
</form>
</PRE>
<h3>Supported Controls</h3>
<P>The following form input items are called <em>supported controls</em> here, and
include all items which undergo population by this class :
<ul>
<li><tt>INPUT</tt> tags with type=<tt>text</tt>, <tt>password</tt>, <tt>radio</tt>,
<tt>checkbox</tt>, or <tt>hidden</tt>
<li><tt>SELECT</tt> tags
<li><tt>TEXTAREA</tt> tags
</ul>
<P>All supported controls must include a <tt>name</tt> attribute.
<P>Population is implemented by editing these supported control attributes :
<ul>
<li>the <tt>value</tt> attribute (only for INPUT tags of type <tt>text</tt>,
<tt>password</tt>, and <tt>hidden</tt>)
<li>the <tt>checked</tt> attribute (only for INPUT tags of type <tt>radio</tt>
and <tt>checkbox</tt>)
<li>the <tt>selected</tt> attribute (only for OPTION tags appearing in a SELECT)
<li>the body of a TEXTAREA tag
</ul>
<P>The body of this tag is standard HTML, with the following minor restrictions :
<ul>
<li>all attributes must be quoted, using either single or double quotes. For example,
<tt><input type='text' ... ></tt> is allowed but
<tt><input type=text ... ></tt> is not
<li> for SELECT tags, the </option> end tag is not optional, and must be included.
<li> INPUT tags must explicitly state the type attribute (the W3C specification
actually allows the <tt>type</tt> attribute to default to <tt>"text"</tt>)
</ul>
<h3>Prepopulating only portions of a form</h3>
There is no requirement that the entire HTML form be wrapped by this tag. If
desired, only part of a form may be placed in the body of this tag. This is useful
when some form controls take a fixed, static value.
<h3>Convention Regarding Control Names</h3>
This tag depends on a specfic convention to allow automatic 'binding' between supported controls
and corresponding <tt>getXXX</tt> methods of the Model Object. This convention is explained in
{@link hirondelle.web4j.request.RequestParameter}.
<h3>Deriving values from <tt>getXXX()</tt> methods of the Model Object</h3>
The return value is found. Any primitives are converted into corresponding wrapper
objects. The {@link hirondelle.web4j.request.Formats#objectToText} method is then used to
translate the object into text. If the return value of the <tt>getXXX</tt> is a
<tt>Collection</tt>, then the above is applied to each element.
<h3>Escaping special characters</h3>
When this tag assigns a text value to the content of an <tt>INPUT</tt> or <tt>TEXTAREA</tt> tag, then the
value is always escaped for special characters using {@link hirondelle.web4j.util.EscapeChars#forHTML(String)}.
<h3><tt>GET</tt> versus <tt>POST</tt></h3>
This tag depends on the proper <tt>GET/POST</tt> behavior of forms : a <tt>POST</tt>
request must only be used when an edit to the database is being attempted. (This is the usual style,
and would not be regarded by most as being a restriction.)
*/
public final class Populate extends TagHelper {
/**
Key for the Model Object to be used for form population.
<P>This attribute is specified only if the form can be used to edit or change an
existing Model Object. If the Model Object is present, then it will be used by this tag to
populate supported controls.
<P>This tag searches for the Model Object in the same way as <tt>JspContext.findAttribute(String)</tt>,
by searching scopes in a specific order : page scope, request scope, session scope, and finally
application scope.
@param aModelObjectKey satisfies {@link Util#textHasContent(String)}.
*/
public void setUsing(String aModelObjectKey){
checkForContent("Using", aModelObjectKey);
fModel = getPageContext().findAttribute(aModelObjectKey);
}
/**
Emit the possibly-changed body of this tag, by possibly editing supported form controls
contained in the body of this tag.
*/
@Override protected String getEmittedText(String aOriginalBody) throws JspException {
String result = null;
setUseCaseStyle();
if ( Style.ECHO == fStyle ){
result = aOriginalBody;
}
else {
result = getEditedBody(aOriginalBody, fStyle);
}
return result;
}
// PRIVATE //
/**
The ModelObject which is to be used to populate supported controls in the "edit" use case.
Is identified by the value of the 'using' attribute
*/
private Object fModel;
/** Use case style */
private Style fStyle;
enum Style {ECHO, USE_MODEL_OBJECT, MUST_RECYCLE_PARAMS, RECYCLE_PARAM_IF_PRESENT}
private static final String GET = "GET";
private static final String POST = "POST";
private static final Logger fLogger = Util.getLogger(Populate.class);
private void setUseCaseStyle(){
String PREAMBLE = "Form population use case: ";
if( fModel != null ) {
fLogger.fine(PREAMBLE +"'Using' object is specified and present. All controls will be populated using getXXX methods of the 'using' object.");
fStyle = Style.USE_MODEL_OBJECT;
}
else if( isRequest(POST) ){
/* Minor Problem: delete ids infecting ADD operations. */
fLogger.fine(PREAMBLE + "POST. All controls will be populated using request parameter values.");
fStyle = Style.MUST_RECYCLE_PARAMS;
}
else if( isRequest(GET) ) {
if ( hasNoRequestParameters() ) {
fLogger.fine(PREAMBLE + "GET, with no request parameters present. Echoing the HTML of entire form as is.");
fStyle = Style.ECHO;
}
else {
fLogger.fine(PREAMBLE + "GET. Any request parameter whose name matches a form control will be used to populate that control.");
fStyle = Style.RECYCLE_PARAM_IF_PRESENT;
}
}
else {
throw new AssertionError("Unexpected use case.");
}
}
private boolean isRequest(String aRequestStyle){
return getRequest().getMethod().equalsIgnoreCase(aRequestStyle);
}
private String getEditedBody(String aOriginalBody, Style aUseCaseStyle) throws JspException {
PopulateHelper populator = new PopulateHelper(new Wrapper(), aOriginalBody, aUseCaseStyle);
return populator.getEditedBody();
}
/** This exists solely to provide a particular 'view' of this object that does not leak into the public API. */
private class Wrapper implements PopulateHelper.Context {
public String getReqParamValue(String aParamName){
String value = getRequest().getParameter(aParamName);
return value == null ? EMPTY_STRING : value;
}
public Collection<String> getReqParamValues(String aParamName){
Collection<String> result = Collections.emptyList(); //default return value
String[] values = getRequest().getParameterValues(aParamName);
if ( values != null ) {
result = Collections.unmodifiableCollection( Arrays.asList(values) );
}
return result;
}
public boolean hasRequestParamNamed(String aParamName) {
boolean result = false;
Enumeration allParamNames = getRequest().getParameterNames();
while (allParamNames.hasMoreElements()){
if (allParamNames.nextElement().equals(aParamName)) {
result = true;
break;
}
}
return result;
}
public boolean isModelObjectPresent(){
return fModel != null;
}
public Object getModelObject(){
return fModel;
}
public Formats getFormats(){
Locale locale = BuildImpl.forLocaleSource().get(getRequest());
TimeZone timeZone = BuildImpl.forTimeZoneSource().get(getRequest());
return new Formats(locale, timeZone);
}
}
private boolean hasNoRequestParameters(){
Enumeration namesEnum = getRequest().getParameterNames();
int numParams = 0;
while ( namesEnum.hasMoreElements() ){
++numParams;
namesEnum.nextElement();
}
return numParams == 0;
}
}