package hirondelle.web4j.model;

import java.util.*;
import java.util.logging.*;
import java.lang.reflect.Constructor;

import hirondelle.web4j.request.RequestParameter;
import hirondelle.web4j.request.RequestParser;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.action.Action;
import hirondelle.web4j.model.ModelCtorException;

/**
 <span class="highlight">Parse a set of request parameters into a Model Object.</span>
 
 <P>Since HTTP is entirely textual, the problem always arises in a web application of 
 building Model Objects (whose constructors may take arguments of any type) out of 
 the text taken from HTTP request parameters. (See the <tt>hirondelle.web4j.database</tt> 
 package for the similar problem of translating rows of a <tt>ResultSet</tt> into a Model Object.)

 <P>Somewhat surprisingly, some web application frameworks do not assist the programmer
 in this regard. That is, they leave the programmer to always translate raw HTTP request 
 parameters (<tt>String</tt>s) into target types (<tt>Integer</tt>, <tt>Boolean</tt>,
 etc), and then to in turn build complete Model Objects. This usually results in 
 much code repetition.

 <P>This class, along with implementations of {@link ConvertParam} and {@link RequestParser}, 
 help an {@link Action} build a Model Object by defining such "type translation" 
 policies in one place. 

 <P>Example use case of building a <tt>'Visit'</tt> Model Object out of four
 {@link hirondelle.web4j.request.RequestParameter} objects (ID, RESTAURANT, etc.):
 <PRE>
protected void validateUserInput() {
  try {
    ModelFromRequest builder = new ModelFromRequest(getRequestParser());
    //pass RequestParameters (or any object) using a sequence (varargs)
    fVisit = builder.build(Visit.class, ID, RESTAURANT, LUNCH_DATE, MESSAGE);
  }
  catch (ModelCtorException ex){
    addError(ex);
  }    
}
 </PRE>

 <P><span class="highlight">The order of the sequence params passed to {@link #build(Class, Object...)} 
 must match the order of arguments passed to the Model Object constructor</span>. 
 This mechanism is quite effective and compact.

 <P>The sequence parameters passed to {@link #build(Class, Object...)} need not be a {@link RequestParameter}. 
 They can be any object whatsoever. Before calling the Model Object constructor, the sequence 
 parameters are examined and treated as follows :
 <PRE>
 if the item is not an instance of RequestParameter
    - do not alter it in any way
    - it will be passed to the MO ctor 'as is'
 else 
   - fetch the corresponding param value from the request
   - attempt to translate its text to the target type required
     by the corresponding MO ctor argument, using policies 
     defined by RequestParser and ConvertParam
     if the translation attempt fails
      - create a ModelCtorException 
 </PRE>

 <P> If no {@link ModelCtorException} has been constructed, then the MO constructor is 
 called using reflection. Note that the MO constructor may itself in turn throw 
 a <tt>ModelCtorException</tt>.
 In fact, in order for this class to be well-behaved, <span class="highlight">the MO 
 constructor cannot throw anything other than a <tt>ModelCtorException</tt> as part of
 its contract. This includes
 <tt>RuntimeException</tt>s</span>. For example, if a <tt>null</tt> is not permitted 
 by a MO constructor, it should not throw a <tt>NullPointerException</tt> (unchecked). 
 Rather, it should throw a <tt>ModelCtorException</tt> (checked). This allows the caller to 
 be notified of all faulty user input in a uniform manner. It also makes MO constructors
 simpler, since all irregular input will result in a <tt>ModelCtorException</tt>, instead 
 of a mixture of checked and unchecked exceptions.

 <P>This unusual policy is related to the unusual character of Model Objects, 
 which attempt to build an object out of arbitrary user input. 
 Unchecked exceptions should be thrown only if a bug is present. 
 <em>However, irregular user input is not a bug</em>. 

 <P>When converting from a {@link hirondelle.web4j.request.RequestParameter} into a building block class, 
 this class supports only the types supported by the implementation of {@link ConvertParam}.

<P>In summary, to work with this class, a Model Object must :
<ul>
 <li>be <tt>public</tt>
 <li>have a <tt>public</tt> constructor, whose number of arguments matches the number of <tt>Object[]</tt> params 
 passed to {@link #build(Class, Object...)}
 <li>the constructor is allowed to throw only {@link hirondelle.web4j.model.ModelCtorException} - no 
 unchecked exceptions should be (knowingly) permitted
</ul>
*/
public final class ModelFromRequest {

  /*<em>Design Note (for background only)</em> :
    The design of this mechanism is a result of the following issues :
    <ul>
     <li>model objects (MO's) need to be constructed out of a textual source
     <li>that textual source (the HTTP request) is not necessarily the <em>sole</em> 
     source of data; that is, a MO may be constructed entirely out of the parameters in 
     a request, or may also be constructed out of an arbitrary combination of both 
     request params and java objects. For example, a time-stamp may be passed to a 
     MO constructor alongside other information extracted from the request.
     <li>the HTTP request may lack explicit data needed to create a MO. For example, an
     unchecked checkbox will not cause a request param to be sent to the server.
     <li>users do not always have to make an explicit selection for every field in a form.
     This corresponds to a MO constructor having optional arguments, and to absent or empty 
     request parameters.
     <li>error messages should use names meaningful to the user; for example 
     <tt>'Number of planetoids is not an integer'</tt> is preferred over the more 
     generic <tt>'Item is not an integer'</tt>.
     <li>since construction of MOs is tedious and repetitive, this class should make
     the caller's task as simple as possible. This class should not force the caller to 
     select particular methods based on the target type of a constructor argument.
     <li>error messages should be gathered for all erroneous fields, and presented to the 
     user in a single listing. This gives the user the chance to make all corrections at once, 
     instead of in sequence. This class is not completely successful in this regard, since 
     it is possible, in a few cases, to not see all possible error messages after the first 
     submission : a <tt>ModelCtorException</tt> can be thrown first by this class after 
     a failure to translate into a target type, and then subsequently by the MO 
     constructor itself. Thus, there are thus two flavours of error message : 
     'bad translation from text to type x', and 'bad call to a MO constructor'.
    </ul>
    */
  
  /**
   Constructor.
    
   @param aRequestParser translates parameter values into <tt>Integer</tt>, 
   <tt>Date</tt>, and so on, using the implementation of {@link ConvertParam}.
  */
  public ModelFromRequest(RequestParser aRequestParser){
    fRequestParser = aRequestParser;
    fModelCtorException = new ModelCtorException();
  }
  
  /**
   Return a Model Object constructed out of request parameters (and possibly 
   other Java objects).
  
   @param aMOClass class of the target Model Object to be built.
   @param aCandidateArgs represents the <em>ordered</em> list of items to be passed 
   to the Model Object's constructor, and can contain <tt>null</tt> elements. Usually contains {@link RequestParameter} 
   objects, but may contain objects of any type, as long as they are expected by the target Model Object constructor.
   @throws ModelCtorException if either an element of <tt>aCandidateArgs</tt> 
   cannot be translated into the target type, or if all such translations succeed,  
   but the call to the MO constructor itself fails.
  */
  public <T> T build(Class<T> aMOClass, Object... aCandidateArgs) throws ModelCtorException {
    fLogger.finest("Constructing a Model Object using request param values.");
    Constructor<T> ctor = ModelCtorUtil.getConstructor(aMOClass, aCandidateArgs.length);
    Class<?>[] targetClasses = ctor.getParameterTypes();
    
    List<Object> argValues = new ArrayList<Object>(); //may contain nulls!
    int argIdx = 0;
    for( Class<?> targetClass : targetClasses ){
      argValues.add( convertCandidateArg(aCandidateArgs[argIdx], targetClass) );
      ++argIdx;
    }
    fLogger.finest("Candidate args: " + argValues); 
    if ( fModelCtorException.isNotEmpty() ) {
      fLogger.finest("Failed to convert request param(s) into types expected by ctor.");
      throw fModelCtorException;
    }
    return ModelCtorUtil.buildModelObject(ctor, argValues);
  }
  
  // PRIVATE //

  /** Provides access to the underlying request.  */
  private final RequestParser fRequestParser;
  
  /**
   Holds all error messages, for either failed translation of a param into an Object, 
   or for a failed call to a constructor.
  */
  private final ModelCtorException fModelCtorException;
  private static final Logger fLogger = Util.getLogger(ModelFromRequest.class); 
  
  private Object convertCandidateArg(Object aCandidateArg, Class<?> aTargetClass){
    Object result = null;
    if ( ! (aCandidateArg instanceof RequestParameter) ) {
      result = aCandidateArg;
    }
    else {
      RequestParameter reqParam = (RequestParameter)aCandidateArg;
      result = translateParam(reqParam, aTargetClass);
    }
    return result;
  }
  
  private Object translateParam(RequestParameter aReqParam, Class<?> aTargetClass){
    Object result = null;
    try {
      result = fRequestParser.toSupportedObject(aReqParam, aTargetClass);  
    }
    catch (ModelCtorException ex){
      fModelCtorException.add(ex);
    }
    return result;
  }
}
