package hirondelle.web4j.request;
import java.util.*;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import hirondelle.web4j.BuildImpl;
import hirondelle.web4j.request.Formats;
import hirondelle.web4j.request.RequestParameter;
import hirondelle.web4j.security.ApplicationFirewallImpl;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.action.Action;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.BadRequestException;
import hirondelle.web4j.model.ConvertParamError;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.model.ConvertParam;
import hirondelle.web4j.security.SafeText;
/**
Abstract Base Class (ABC) for mapping a request to an {@link Action}.
<P>See the {@link hirondelle.web4j.BuildImpl} for important information on how this item is configured.
<P><span class="highlight">Almost all concrete implementations of this Abstract Base Class will need to
implement only a single method</span> - {@link #getWebAction()}. WEB4J provides a default implementation
{@link RequestParserImpl}.
<P>The role of this class is to view the request at a higher level than the underlying
Servlet API. In particular, its services include :
<ul>
<li>mapping a request to an {@link Action}
<li>parsing request parameters into common "building block" objects (and Collections thereof),
such as {@link Date}, {@link BigDecimal} and so on, using the configured implementation of
{@link hirondelle.web4j.model.ConvertParam}. (The application programmer will usually use
{@link hirondelle.web4j.model.ModelFromRequest} to build Model Objects.)
</ul>
<P><a href="RequestParameter.html#FileUpload">File upload</a> parameters are not returned
by this class. Such parameters must be examined in an {@link Action}. The Servlet API has poor
support for file upload parameters, and use of a third party tool is recommended.
<P>The various <tt>toXXX</tt> methods are offered as a convenience for accessing <tt>String</tt>
and <tt>String</tt>-like data. All such <tt>toXXX</tt> methods apply the filtering (and possible
preprocessing) performed by {@link hirondelle.web4j.model.ConvertParam}.
*/
public abstract class RequestParser {
/**
Called by the framework upon startup.
<P>Initialize both this class and this package, using settings in <tt>web.xml</tt>.
<P>This method will always call {@link ApplicationFirewallImpl#init(ServletConfig)}) and
{@link RequestParserImpl#initWebActionMappings(ServletConfig)}, regardless of whether or not they are
actually the configured implementations. This lets the application programmer forget about
calling these methods in their application's {@link hirondelle.web4j.StartupTasks}.
*/
public static void initUiLayer(ServletConfig aConfig){
Formats.init(aConfig);
RequestParameter.init(aConfig);
}
/**
Return the configured concrete instance of this Abstract Base Class.
<P>See the {@link hirondelle.web4j.BuildImpl} for important information on how
this item is configured.
*/
public static RequestParser getInstance(HttpServletRequest aRequest, HttpServletResponse aResponse){
List<Object> args = new ArrayList<Object>();
args.add(aRequest);
args.add(aResponse);
RequestParser result = (RequestParser)BuildImpl.forAbstractionPassCtorArgs(
RequestParser.class.getName(),
args
);
return result;
}
/** Constructor called by subclasses. */
public RequestParser(HttpServletRequest aRequest, HttpServletResponse aResponse){
fRequest = aRequest;
fResponse = aResponse;
fLocale = BuildImpl.forLocaleSource().get(aRequest);
fTimeZone = BuildImpl.forTimeZoneSource().get(aRequest);
fConvertUserInput = BuildImpl.forConvertParam();
fConversionError = BuildImpl.forConvertParamError();
}
/**
Map a given request to a corresponding {@link Action}.
<P>The mapping is determined entirely by concrete subclasses, and must
be implemented by the application programmer. {@link RequestParserImpl} is
provided as a default implementation, and is very likely adequate for most
applications.
<P>If the incoming request does not map to a known {@link Action}, then throw
a {@link BadRequestException}. <span class="highlight">Such requests
are expected only for bugs and for malicious attacks, and never as part of the normal operation
of the program.</span>
*/
abstract public Action getWebAction() throws BadRequestException;
/**
Return the parameter value exactly as it appears in the request.
<P>Can return <tt>null</tt> values, empty values, values containing
only whitespace, and values equal to the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>.
*/
public final String getRawParamValue(RequestParameter aReqParam){
String result = fRequest.getParameter(aReqParam.getName());
return result;
}
/**
Return a multi-valued parameter's values exactly as they appear in the request.
<P>Can return <tt>null</tt> values, empty values, values containing
only whitespace, and values equal to the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>.
*/
public final String[] getRawParamValues(RequestParameter aReqParam){
String[] result = fRequest.getParameterValues(aReqParam.getName());
return result;
}
/**
Return a building block object.
<P>Uses all methods of the configured implementation of {@link ConvertParam}.
@param aReqParam underlying request parameter
@param aSupportedTargetClass must be supported - see {@link ConvertParam#isSupported(Class)}
*/
public <T> T toSupportedObject(RequestParameter aReqParam, Class<T> aSupportedTargetClass) throws ModelCtorException {
T result = null;
if( ! fConvertUserInput.isSupported(aSupportedTargetClass) ){
throw new AssertionError("This class is not supported by ConvertParam: " + Util.quote(aSupportedTargetClass));
}
String filteredValue = fConvertUserInput.filter(getRawParamValue(aReqParam));
if( Util.textHasContent(filteredValue) ){
try {
result = fConvertUserInput.convert(filteredValue, aSupportedTargetClass, fLocale, fTimeZone);
}
catch (ModelCtorException ex){
ModelCtorException conversionEx = fConversionError.get(aSupportedTargetClass, filteredValue, aReqParam);
throw conversionEx;
}
}
return result;
}
/**
Return an ummodifiable <tt>List</tt> of building block objects.
<P>Uses all methods of the configured implementation of {@link ConvertParam}.
<P>
<em>Design Note</em><br>
<tt>List</tt> is returned here since HTML specs state that browsers submit param values
in the order of appearance of the corresponding controls in the web page.
@param aReqParam underlying request parameter
@param aSupportedTargetClass must be supported - see {@link ConvertParam#isSupported(Class)}
*/
public <T> List<T> toSupportedObjects(RequestParameter aReqParam, Class<T> aSupportedTargetClass) throws ModelCtorException {
List<T> result = new ArrayList<T>();
ModelCtorException conversionExceptions = new ModelCtorException();
if( ! fConvertUserInput.isSupported(aSupportedTargetClass) ){
throw new AssertionError("This class is not supported by ConvertParam: " + Util.quote(aSupportedTargetClass));
}
String[] rawValues = getRawParamValues(aReqParam);
if(rawValues != null){
for(String rawValue: rawValues){
String filteredValue = fConvertUserInput.filter(rawValue); //possibly null
//is it possible to have a multi-valued boolean param???
if ( Util.textHasContent(filteredValue) || Boolean.class == aSupportedTargetClass){
try {
T convertedItem = fConvertUserInput.convert(filteredValue, aSupportedTargetClass, fLocale, fTimeZone);
result.add(convertedItem);
}
catch (ModelCtorException ex){
AppException conversionEx = fConversionError.get(aSupportedTargetClass, filteredValue, aReqParam);
conversionExceptions.add(conversionEx);
}
}
else {
result.add(null);
}
}
if (conversionExceptions.isNotEmpty()) throw conversionExceptions;
}
return Collections.unmodifiableList(result);
}
/** Return a single-valued request parameter as {@link SafeText}. */
public final SafeText toSafeText(RequestParameter aReqParam) {
SafeText result = null;
try {
result = toSupportedObject(aReqParam, SafeText.class);
}
catch (ModelCtorException ex){
changeToRuntimeException(ex);
}
return result;
}
/** Return a multi-valued request parameter as a {@code Collection<SafeText>}. */
public final Collection<SafeText> toSafeTexts(RequestParameter aReqParam) {
Collection<SafeText> result = null;
try {
result = toSupportedObjects(aReqParam, SafeText.class);
}
catch (ModelCtorException ex){
changeToRuntimeException(ex);
}
return result;
}
/** Return a single-valued request parameter as an {@link Id}. */
public final Id toId(RequestParameter aReqParam) {
Id result = null;
try {
result = toSupportedObject(aReqParam, Id.class);
}
catch(ModelCtorException ex){
changeToRuntimeException(ex);
}
return result;
}
/** Return a multi-valued request parameter as a {@code Collection<Id>}. */
public final Collection<Id> toIds(RequestParameter aReqParam) {
Collection<Id> result = null;
try {
result = toSupportedObjects(aReqParam, Id.class);
}
catch (ModelCtorException ex){
changeToRuntimeException(ex);
}
return result;
}
/** Return the underlying request. */
public final HttpServletRequest getRequest(){
return fRequest;
}
/** Return the response associated with the underlying request. */
public final HttpServletResponse getResponse(){
return fResponse;
}
/**
Return <tt>true</tt> only if the request is a <tt>POST</tt>, and has
content type starting with <tt>multipart/form-data</tt>.
*/
public final boolean isFileUploadRequest(){
return
fRequest.getMethod().equalsIgnoreCase("POST") &&
fRequest.getContentType().startsWith("multipart/form-data")
;
}
// PRIVATE //
private final HttpServletRequest fRequest;
private final HttpServletResponse fResponse;
private final Locale fLocale;
private final TimeZone fTimeZone;
private final ConvertParam fConvertUserInput;
private final ConvertParamError fConversionError;
/**
Change from a checked to an unchecked ex.
<P>This is unusual, and a bit ugly. For stringy data, there isn't any possibility of a
parse error. Requiring Action constructors to catch or throw a ModelCtorEx is distasteful
(this would happen for items that have an Operation built in the constructor.)
*/
private void changeToRuntimeException(ModelCtorException ex){
throw new IllegalArgumentException(ex);
}
}