001 package hirondelle.web4j.model;
002
003 import java.util.*;
004 import java.util.logging.*;
005 import java.lang.reflect.Constructor;
006
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;
012
013 /**
014 <span class="highlight">Parse a set of request parameters into a Model Object.</span>
015
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.)
020
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.
026
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.
030
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>
045
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.
049
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>
065
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.
078
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>.
083
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}.
086
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 {
097
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 */
128
129 /**
130 Constructor.
131
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 }
139
140 /**
141 Return a Model Object constructed out of request parameters (and possibly
142 other Java objects).
143
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();
156
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 }
170
171 // PRIVATE //
172
173 /** Provides access to the underlying request. */
174 private final RequestParser fRequestParser;
175
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);
182
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 }
194
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 }