001    package hirondelle.web4j.model;
003    import java.util.*;
004    import java.util.logging.*;
005    import java.lang.reflect.Constructor;
007    import hirondelle.web4j.request.RequestParameter;
008    import hirondelle.web4j.request.RequestParser;
009    import hirondelle.web4j.util.Util;
010    import hirondelle.web4j.action.Action;
011    import hirondelle.web4j.model.ModelCtorException;
013    /**
014     <span class="highlight">Parse a set of request parameters into a Model Object.</span>
016     <P>Since HTTP is entirely textual, the problem always arises in a web application of 
017     building Model Objects (whose constructors may take arguments of any type) out of 
018     the text taken from HTTP request parameters. (See the <tt>hirondelle.web4j.database</tt> 
019     package for the similar problem of translating rows of a <tt>ResultSet</tt> into a Model Object.)
021     <P>Somewhat surprisingly, some web application frameworks do not assist the programmer
022     in this regard. That is, they leave the programmer to always translate raw HTTP request 
023     parameters (<tt>String</tt>s) into target types (<tt>Integer</tt>, <tt>Boolean</tt>,
024     etc), and then to in turn build complete Model Objects. This usually results in 
025     much code repetition.
027     <P>This class, along with implementations of {@link ConvertParam} and {@link RequestParser}, 
028     help an {@link Action} build a Model Object by defining such "type translation" 
029     policies in one place. 
031     <P>Example use case of building a <tt>'Visit'</tt> Model Object out of four
032     {@link hirondelle.web4j.request.RequestParameter} objects (ID, RESTAURANT, etc.):
033     <PRE>
034    protected void validateUserInput() {
035      try {
036        ModelFromRequest builder = new ModelFromRequest(getRequestParser());
037        //pass RequestParameters (or any object) using a sequence (varargs)
038        fVisit = builder.build(Visit.class, ID, RESTAURANT, LUNCH_DATE, MESSAGE);
039      }
040      catch (ModelCtorException ex){
041        addError(ex);
042      }    
043    }
044     </PRE>
046     <P><span class="highlight">The order of the sequence params passed to {@link #build(Class, Object...)} 
047     must match the order of arguments passed to the Model Object constructor</span>. 
048     This mechanism is quite effective and compact.
050     <P>The sequence parameters passed to {@link #build(Class, Object...)} need not be a {@link RequestParameter}. 
051     They can be any object whatsoever. Before calling the Model Object constructor, the sequence 
052     parameters are examined and treated as follows :
053     <PRE>
054     if the item is not an instance of RequestParameter
055        - do not alter it in any way
056        - it will be passed to the MO ctor 'as is'
057     else 
058       - fetch the corresponding param value from the request
059       - attempt to translate its text to the target type required
060         by the corresponding MO ctor argument, using policies 
061         defined by RequestParser and ConvertParam
062         if the translation attempt fails
063          - create a ModelCtorException 
064     </PRE>
066     <P> If no {@link ModelCtorException} has been constructed, then the MO constructor is 
067     called using reflection. Note that the MO constructor may itself in turn throw 
068     a <tt>ModelCtorException</tt>.
069     In fact, in order for this class to be well-behaved, <span class="highlight">the MO 
070     constructor cannot throw anything other than a <tt>ModelCtorException</tt> as part of
071     its contract. This includes
072     <tt>RuntimeException</tt>s</span>. For example, if a <tt>null</tt> is not permitted 
073     by a MO constructor, it should not throw a <tt>NullPointerException</tt> (unchecked). 
074     Rather, it should throw a <tt>ModelCtorException</tt> (checked). This allows the caller to 
075     be notified of all faulty user input in a uniform manner. It also makes MO constructors
076     simpler, since all irregular input will result in a <tt>ModelCtorException</tt>, instead 
077     of a mixture of checked and unchecked exceptions.
079     <P>This unusual policy is related to the unusual character of Model Objects, 
080     which attempt to build an object out of arbitrary user input. 
081     Unchecked exceptions should be thrown only if a bug is present. 
082     <em>However, irregular user input is not a bug</em>. 
084     <P>When converting from a {@link hirondelle.web4j.request.RequestParameter} into a building block class, 
085     this class supports only the types supported by the implementation of {@link ConvertParam}.
087    <P>In summary, to work with this class, a Model Object must :
088    <ul>
089     <li>be <tt>public</tt>
090     <li>have a <tt>public</tt> constructor, whose number of arguments matches the number of <tt>Object[]</tt> params 
091     passed to {@link #build(Class, Object...)}
092     <li>the constructor is allowed to throw only {@link hirondelle.web4j.model.ModelCtorException} - no 
093     unchecked exceptions should be (knowingly) permitted
094    </ul>
095    */
096    public final class ModelFromRequest {
098      /*<em>Design Note (for background only)</em> :
099        The design of this mechanism is a result of the following issues :
100        <ul>
101         <li>model objects (MO's) need to be constructed out of a textual source
102         <li>that textual source (the HTTP request) is not necessarily the <em>sole</em> 
103         source of data; that is, a MO may be constructed entirely out of the parameters in 
104         a request, or may also be constructed out of an arbitrary combination of both 
105         request params and java objects. For example, a time-stamp may be passed to a 
106         MO constructor alongside other information extracted from the request.
107         <li>the HTTP request may lack explicit data needed to create a MO. For example, an
108         unchecked checkbox will not cause a request param to be sent to the server.
109         <li>users do not always have to make an explicit selection for every field in a form.
110         This corresponds to a MO constructor having optional arguments, and to absent or empty 
111         request parameters.
112         <li>error messages should use names meaningful to the user; for example 
113         <tt>'Number of planetoids is not an integer'</tt> is preferred over the more 
114         generic <tt>'Item is not an integer'</tt>.
115         <li>since construction of MOs is tedious and repetitive, this class should make
116         the caller's task as simple as possible. This class should not force the caller to 
117         select particular methods based on the target type of a constructor argument.
118         <li>error messages should be gathered for all erroneous fields, and presented to the 
119         user in a single listing. This gives the user the chance to make all corrections at once, 
120         instead of in sequence. This class is not completely successful in this regard, since 
121         it is possible, in a few cases, to not see all possible error messages after the first 
122         submission : a <tt>ModelCtorException</tt> can be thrown first by this class after 
123         a failure to translate into a target type, and then subsequently by the MO 
124         constructor itself. Thus, there are thus two flavours of error message : 
125         'bad translation from text to type x', and 'bad call to a MO constructor'.
126        </ul>
127        */
129      /**
130       Constructor.
132       @param aRequestParser translates parameter values into <tt>Integer</tt>, 
133       <tt>Date</tt>, and so on, using the implementation of {@link ConvertParam}.
134      */
135      public ModelFromRequest(RequestParser aRequestParser){
136        fRequestParser = aRequestParser;
137        fModelCtorException = new ModelCtorException();
138      }
140      /**
141       Return a Model Object constructed out of request parameters (and possibly 
142       other Java objects).
144       @param aMOClass class of the target Model Object to be built.
145       @param aCandidateArgs represents the <em>ordered</em> list of items to be passed 
146       to the Model Object's constructor, and can contain <tt>null</tt> elements. Usually contains {@link RequestParameter} 
147       objects, but may contain objects of any type, as long as they are expected by the target Model Object constructor.
148       @throws ModelCtorException if either an element of <tt>aCandidateArgs</tt> 
149       cannot be translated into the target type, or if all such translations succeed,  
150       but the call to the MO constructor itself fails.
151      */
152      public <T> T build(Class<T> aMOClass, Object... aCandidateArgs) throws ModelCtorException {
153        fLogger.finest("Constructing a Model Object using request param values.");
154        Constructor<T> ctor = ModelCtorUtil.getConstructor(aMOClass, aCandidateArgs.length);
155        Class<?>[] targetClasses = ctor.getParameterTypes();
157        List<Object> argValues = new ArrayList<Object>(); //may contain nulls!
158        int argIdx = 0;
159        for( Class<?> targetClass : targetClasses ){
160          argValues.add( convertCandidateArg(aCandidateArgs[argIdx], targetClass) );
161          ++argIdx;
162        }
163        fLogger.finest("Candidate args: " + argValues); 
164        if ( fModelCtorException.isNotEmpty() ) {
165          fLogger.finest("Failed to convert request param(s) into types expected by ctor.");
166          throw fModelCtorException;
167        }
168        return ModelCtorUtil.buildModelObject(ctor, argValues);
169      }
171      // PRIVATE //
173      /** Provides access to the underlying request.  */
174      private final RequestParser fRequestParser;
176      /**
177       Holds all error messages, for either failed translation of a param into an Object, 
178       or for a failed call to a constructor.
179      */
180      private final ModelCtorException fModelCtorException;
181      private static final Logger fLogger = Util.getLogger(ModelFromRequest.class); 
183      private Object convertCandidateArg(Object aCandidateArg, Class<?> aTargetClass){
184        Object result = null;
185        if ( ! (aCandidateArg instanceof RequestParameter) ) {
186          result = aCandidateArg;
187        }
188        else {
189          RequestParameter reqParam = (RequestParameter)aCandidateArg;
190          result = translateParam(reqParam, aTargetClass);
191        }
192        return result;
193      }
195      private Object translateParam(RequestParameter aReqParam, Class<?> aTargetClass){
196        Object result = null;
197        try {
198          result = fRequestParser.toSupportedObject(aReqParam, aTargetClass);  
199        }
200        catch (ModelCtorException ex){
201          fModelCtorException.add(ex);
202        }
203        return result;
204      }
205    }