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