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 }