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    }