001 package hirondelle.web4j.request; 002 003 import hirondelle.web4j.BuildImpl; 004 import hirondelle.web4j.action.Action; 005 import hirondelle.web4j.model.AppException; 006 import hirondelle.web4j.model.BadRequestException; 007 import hirondelle.web4j.model.ConvertParam; 008 import hirondelle.web4j.model.ConvertParamError; 009 import hirondelle.web4j.model.Id; 010 import hirondelle.web4j.model.ModelCtorException; 011 import hirondelle.web4j.security.SafeText; 012 import hirondelle.web4j.util.Util; 013 014 import java.math.BigDecimal; 015 import java.util.ArrayList; 016 import java.util.Collection; 017 import java.util.Collections; 018 import java.util.Date; 019 import java.util.List; 020 import java.util.Locale; 021 import java.util.TimeZone; 022 023 import javax.servlet.http.HttpServletRequest; 024 import javax.servlet.http.HttpServletResponse; 025 026 /** 027 Abstract Base Class (ABC) for mapping a request to an {@link Action}. 028 029 <P>See the {@link hirondelle.web4j.BuildImpl} for important information on how this item is configured. 030 031 <P><span class="highlight">Almost all concrete implementations of this Abstract Base Class will need to 032 implement only a single method</span> - {@link #getWebAction()}. WEB4J provides a default implementation 033 {@link RequestParserImpl}. 034 035 <P>The role of this class is to view the request at a higher level than the underlying 036 Servlet API. In particular, its services include : 037 <ul> 038 <li>mapping a request to an {@link Action} 039 <li>parsing request parameters into common "building block" objects (and Collections thereof), 040 such as {@link Date}, {@link BigDecimal} and so on, using the configured implementation of 041 {@link hirondelle.web4j.model.ConvertParam}. (The application programmer will usually use 042 {@link hirondelle.web4j.model.ModelFromRequest} to build Model Objects.) 043 </ul> 044 045 <P><a href="RequestParameter.html#FileUpload">File upload</a> parameters are not returned 046 by this class. Such parameters must be examined in an {@link Action}. The Servlet API 047 before version 3.0 of the specification has poor support for file upload parameters, 048 and use of a third party tool is recommended. 049 050 <P>The various <tt>toXXX</tt> methods are offered as a convenience for accessing <tt>String</tt> 051 and <tt>String</tt>-like data. All such <tt>toXXX</tt> methods apply the filtering (and possible 052 preprocessing) performed by {@link hirondelle.web4j.model.ConvertParam}. 053 */ 054 public abstract class RequestParser { 055 056 /** 057 Return the configured concrete instance of this Abstract Base Class. 058 <P>See the {@link hirondelle.web4j.BuildImpl} for important information on how 059 this item is configured. 060 */ 061 public static RequestParser getInstance(HttpServletRequest aRequest, HttpServletResponse aResponse){ 062 List<Object> args = new ArrayList<Object>(); 063 args.add(aRequest); 064 args.add(aResponse); 065 RequestParser result = (RequestParser)BuildImpl.forAbstractionPassCtorArgs( 066 RequestParser.class.getName(), 067 args 068 ); 069 return result; 070 } 071 072 /** Constructor called by subclasses. */ 073 public RequestParser(HttpServletRequest aRequest, HttpServletResponse aResponse){ 074 fRequest = aRequest; 075 fResponse = aResponse; 076 fLocale = BuildImpl.forLocaleSource().get(aRequest); 077 fTimeZone = BuildImpl.forTimeZoneSource().get(aRequest); 078 fConvertUserInput = BuildImpl.forConvertParam(); 079 fConversionError = BuildImpl.forConvertParamError(); 080 } 081 082 /** 083 Map a given request to a corresponding {@link Action}. 084 085 <P>The mapping is determined entirely by concrete subclasses, and must 086 be implemented by the application programmer. {@link RequestParserImpl} is 087 provided as a default implementation, and is very likely adequate for most 088 applications. 089 090 <P>If the incoming request does not map to a known {@link Action}, then throw 091 a {@link BadRequestException}. <span class="highlight">Such requests 092 are expected only for bugs and for malicious attacks, and never as part of the normal operation 093 of the program.</span> 094 */ 095 abstract public Action getWebAction() throws BadRequestException; 096 097 /** 098 Return the parameter value exactly as it appears in the request. 099 100 <P>Can return <tt>null</tt> values, empty values, values containing 101 only whitespace, and values equal to the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>. 102 */ 103 public final String getRawParamValue(RequestParameter aReqParam){ 104 String result = fRequest.getParameter(aReqParam.getName()); 105 return result; 106 } 107 108 /** 109 Return a multi-valued parameter's values exactly as they appear in the request. 110 111 <P>Can return <tt>null</tt> values, empty values, values containing 112 only whitespace, and values equal to the <tt>IgnorableParamValue</tt> configured in <tt>web.xml</tt>. 113 */ 114 public final String[] getRawParamValues(RequestParameter aReqParam){ 115 String[] result = fRequest.getParameterValues(aReqParam.getName()); 116 return result; 117 } 118 119 /** 120 Return a building block object. 121 122 <P>Uses all methods of the configured implementation of {@link ConvertParam}. 123 @param aReqParam underlying request parameter 124 @param aSupportedTargetClass must be supported - see {@link ConvertParam#isSupported(Class)} 125 */ 126 public <T> T toSupportedObject(RequestParameter aReqParam, Class<T> aSupportedTargetClass) throws ModelCtorException { 127 T result = null; 128 if( ! fConvertUserInput.isSupported(aSupportedTargetClass) ){ 129 throw new AssertionError("This class is not supported by ConvertParam: " + Util.quote(aSupportedTargetClass)); 130 } 131 String filteredValue = fConvertUserInput.filter(getRawParamValue(aReqParam)); 132 if( Util.textHasContent(filteredValue) ){ 133 try { 134 result = fConvertUserInput.convert(filteredValue, aSupportedTargetClass, fLocale, fTimeZone); 135 } 136 catch (ModelCtorException ex){ 137 ModelCtorException conversionEx = fConversionError.get(aSupportedTargetClass, filteredValue, aReqParam); 138 throw conversionEx; 139 } 140 } 141 return result; 142 } 143 144 /** 145 Return an ummodifiable <tt>List</tt> of building block objects. 146 147 <P>Uses all methods of the configured implementation of {@link ConvertParam}. 148 <P> 149 <em>Design Note</em><br> 150 <tt>List</tt> is returned here since HTML specs state that browsers submit param values 151 in the order of appearance of the corresponding controls in the web page. 152 @param aReqParam underlying request parameter 153 @param aSupportedTargetClass must be supported - see {@link ConvertParam#isSupported(Class)} 154 */ 155 public <T> List<T> toSupportedObjects(RequestParameter aReqParam, Class<T> aSupportedTargetClass) throws ModelCtorException { 156 List<T> result = new ArrayList<T>(); 157 ModelCtorException conversionExceptions = new ModelCtorException(); 158 if( ! fConvertUserInput.isSupported(aSupportedTargetClass) ){ 159 throw new AssertionError("This class is not supported by ConvertParam: " + Util.quote(aSupportedTargetClass)); 160 } 161 String[] rawValues = getRawParamValues(aReqParam); 162 if(rawValues != null){ 163 for(String rawValue: rawValues){ 164 String filteredValue = fConvertUserInput.filter(rawValue); //possibly null 165 //is it possible to have a multi-valued boolean param??? 166 if ( Util.textHasContent(filteredValue) || Boolean.class == aSupportedTargetClass){ 167 try { 168 T convertedItem = fConvertUserInput.convert(filteredValue, aSupportedTargetClass, fLocale, fTimeZone); 169 result.add(convertedItem); 170 } 171 catch (ModelCtorException ex){ 172 AppException conversionEx = fConversionError.get(aSupportedTargetClass, filteredValue, aReqParam); 173 conversionExceptions.add(conversionEx); 174 } 175 } 176 else { 177 result.add(null); 178 } 179 } 180 if (conversionExceptions.isNotEmpty()) throw conversionExceptions; 181 } 182 return Collections.unmodifiableList(result); 183 } 184 185 /** Return a single-valued request parameter as {@link SafeText}. */ 186 public final SafeText toSafeText(RequestParameter aReqParam) { 187 SafeText result = null; 188 try { 189 result = toSupportedObject(aReqParam, SafeText.class); 190 } 191 catch (ModelCtorException ex){ 192 changeToRuntimeException(ex); 193 } 194 return result; 195 } 196 197 /** Return a multi-valued request parameter as a {@code Collection<SafeText>}. */ 198 public final Collection<SafeText> toSafeTexts(RequestParameter aReqParam) { 199 Collection<SafeText> result = null; 200 try { 201 result = toSupportedObjects(aReqParam, SafeText.class); 202 } 203 catch (ModelCtorException ex){ 204 changeToRuntimeException(ex); 205 } 206 return result; 207 } 208 209 /** Return a single-valued request parameter as an {@link Id}. */ 210 public final Id toId(RequestParameter aReqParam) { 211 Id result = null; 212 try { 213 result = toSupportedObject(aReqParam, Id.class); 214 } 215 catch(ModelCtorException ex){ 216 changeToRuntimeException(ex); 217 } 218 return result; 219 } 220 221 /** Return a multi-valued request parameter as a {@code Collection<Id>}. */ 222 public final Collection<Id> toIds(RequestParameter aReqParam) { 223 Collection<Id> result = null; 224 try { 225 result = toSupportedObjects(aReqParam, Id.class); 226 } 227 catch (ModelCtorException ex){ 228 changeToRuntimeException(ex); 229 } 230 return result; 231 } 232 233 /** Return the underlying request. */ 234 public final HttpServletRequest getRequest(){ 235 return fRequest; 236 } 237 238 /** Return the response associated with the underlying request. */ 239 public final HttpServletResponse getResponse(){ 240 return fResponse; 241 } 242 243 /** 244 Return <tt>true</tt> only if the request is a <tt>POST</tt>, and has 245 content type starting with <tt>multipart/form-data</tt>. 246 */ 247 public final boolean isFileUploadRequest(){ 248 return 249 fRequest.getMethod().equalsIgnoreCase("POST") && 250 fRequest.getContentType().startsWith("multipart/form-data") 251 ; 252 } 253 254 // PRIVATE // 255 private final HttpServletRequest fRequest; 256 private final HttpServletResponse fResponse; 257 private final Locale fLocale; 258 private final TimeZone fTimeZone; 259 private final ConvertParam fConvertUserInput; 260 private final ConvertParamError fConversionError; 261 262 /** 263 Change from a checked to an unchecked ex. 264 265 <P>This is unusual, and a bit ugly. For stringy data, there isn't any possibility of a 266 parse error. Requiring Action constructors to catch or throw a ModelCtorEx is distasteful 267 (this would happen for items that have an Operation built in the constructor.) 268 */ 269 private void changeToRuntimeException(ModelCtorException ex){ 270 throw new IllegalArgumentException(ex); 271 } 272 }