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    }