classes/hirondelle/web4j/request/RequestParserImpl.java
author Tomas Zeman <tzeman@volny.cz>
Wed, 04 Dec 2013 17:00:31 +0100
changeset 0 3060119b1292
permissions -rw-r--r--
Imported web4j 4.10.0

package hirondelle.web4j.request;

import static hirondelle.web4j.util.Consts.NOT_FOUND;
import hirondelle.web4j.action.Action;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.readconfig.Config;
import hirondelle.web4j.readconfig.ConfigReader;
import hirondelle.web4j.util.Util;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 <span class="highlight">Maps each HTTP request to a concrete {@link Action}.</span>
 
 <P> Default implementation of {@link RequestParser}.
 
 <P>This implementation extracts the <a  href="#URIMappingString">URI Mapping String</a> from the 
 underlying request, and maps it to a specific {@link Action} class, and calls its constructor by passing 
 a {@link RequestParser}. (Here, each {@link Action} must have a <tt>public</tt> constructor 
 which takes a {@link RequestParser} as its single parameter.)
 
 <P>There are two kinds of mapping available :
<ul>
 <li><a href="#ImplicitMapping">implicit mapping</a> - simple, and recommended
 <li><a href="#ExplicitMapping">explicit mapping</a> - requires an extra step, and overrides the implicit mapping
</ul> 
  
 <P><a name="URIMappingString"><h3>URI Mapping String</h3>
 The 'URI Mapping String' is extracted from the underlying request. It is simply the concatention of 
 {@link HttpServletRequest#getServletPath()} and {@link HttpServletRequest#getPathInfo()} 
 (minus the extension - <tt>.do</tt>, for example).  
 
 <P>(The servlet path is the part of the URI which has been mapped to a servlet by the <tt>servlet-mapping</tt> 
 entries in the <tt>web.xml</tt>.)

 <P><a name="ImplicitMapping"><h3>Implicit Mapping</h3>
 If no <a href="#ExplicitMapping">explicit mapping</a> exists in an <tt>Action</tt>, then it will <em>implicitly</em> 
 map to the <a href="#URIMappingString">URI Mapping String</a> that corresponds to a <em>modified</em> version of its 
 package-qualified name : 
<ul>
 <li>take the package-qualified class name
 <li>change '.' characters to '/'
 <li><em>remove</em> the base package prefix, configured in <tt>web.xml</tt> as <tt>ImplicitMappingRemoveBasePackage</tt>
</ul>

 <P>Example of an implicit mapping :
 <table cellpadding="3" cellspacing="0" border="1">
  <tr><td>Class Name:</td><td>hirondelle.fish.main.member.MemberEdit</th></tr>
  <tr><td><tt>ImplicitMappingRemoveBasePackage</tt> (web.xml):</td><td>hirondelle.fish</th></tr>
  <tr><td>Implicit Mapping calculated as:</td><td>/main/member/MemberEdit</th></tr>
 </table>
 
 <P>Which maps to the following requests:
 
 <P><table cellpadding="3" cellspacing="0" border="1">
  <tr><td>Request 1:</td><td>http://www.blah.com/fish/main/member/MemberEdit.list</th></tr>
  <tr><td>Request 2:</td><td>http://www.blah.com/fish/main/member/MemberEdit.do?Operation=List</th></tr>
  <tr><td>URI Mapping String calculated as:</td><td>/main/member/MemberEdit</th></tr>
 </table>
 
 <P><a name="ExplicitMapping"><h3>Explicit Mapping</h3>
 An <tt>Action</tt> may declare an explicit mapping to a <a href="#URIMappingString">URI Mapping String</a> 
 simply by declaring a field of the form (for example) : 
 <PRE>
  public static final String EXPLICIT_URI_MAPPING = "/translate/basetext/BaseTextEdit";
 </PRE>
 Explicit mappings override implicit mappings. 

<P><h3>Fine-Grained Security</h3>
 Fine-grained security allows <tt>&lt;security-constraint&gt;</tt> items to be specifed for various extensions, 
 where the extensions represent various action verbs, such as <tt>.list</tt>, <tt>.change</tt>, and so on. 
 In that case, the conventional <tt>.do</tt> is replaced with several different extensions.
 See the User Guide for more information on fine-grained security.

 <P><h3>Looking Up Action, Given URI</h3>
 It's a common requirement to look up an action class, given a URI. Various sources 
 can be used to perform that task:
<ul>
 <li>the application's javadoc listing of Constant Field Values can be 
 quickly searched for an explicit <tt>EXPLICIT_URI_MAPPING</tt> 
 <li>all mappings are logged upon startup at <tt>CONFIG</tt> level
 <li>the source code itself can be searched, if necessary
</ul>
*/
public class RequestParserImpl extends RequestParser {

  /**
   Scan for {@link Action} mappings. Called by the framework upon startup. Scans for all classes 
   that implement {@link Action}. Stores either an <a href="ImplicitMapping">implicit</a>  
   or an <a href="#ExplicitMapping">explicit</a> mapping. Implicit mappings are the recommended style. 
   
   <P>If a problem with mapping is detected, then a {@link RuntimeException} is thrown, and 
   the application will not load. This protects the application, by forcing some important 
   errors to occur during startup, instead of during normal operation. Possible errors include :
   <ul>
   <li>the <tt>EXPLICIT_URI_MAPPING</tt> field is not a <tt>public static final String</tt>
   <li>the same mapping is used for more than one {@link Action}
   </ul>
  */
  public static void initWebActionMappings(){
    scanMappings();
    fLogger.config("URI Mappings : " + Util.logOnePerLine(fUriToActionMapping));
  }

  /**
   Constructor.
    
   @param aRequest passed to the super class.
   @param aResponse passed to the super class.
  */
  public RequestParserImpl(HttpServletRequest aRequest, HttpServletResponse aResponse) {
    super(aRequest, aResponse);
    if (aRequest.getPathInfo() != null){
      fURIMappingString = aRequest.getServletPath() + aRequest.getPathInfo();
    }
    else {
      fURIMappingString = aRequest.getServletPath();
    }
    fLogger.fine("*** ________________________ NEW REQUEST _________________");
    fURIMappingString = removeExtension(fURIMappingString);
    fLogger.fine("URL Mapping String: " + fURIMappingString);
  }
  
  /**
   Map an HTTP request to a concrete implementation of {@link Action}.
  
   <P>Extract the <a href="#URIMappingString">URI Mapping String</a> from the underlying request, and 
   map it to an {@link Action}.
  */
  @Override public final Action getWebAction() {
    Action result = null;
    AppException problem = new AppException();
    Class webAction = fUriToActionMapping.get(fURIMappingString);
    if ( webAction == null ) {
      throw new RuntimeException("Cannot map URI to an Action class : " + Util.quote(fURIMappingString));
    }
    
    Class[] ctorArgs = {RequestParser.class};
    try {
      Constructor ctor = webAction.getConstructor(ctorArgs);
      result = (Action)ctor.newInstance(new Object[]{this});
    }
    catch(NoSuchMethodException ex){
      problem.add("Action does not have public constructor having single argument of type 'RequestParser'.");
    }
    catch(InstantiationException ex){
      problem.add("Cannot call Action constructor using reflection (class is abstract). " + ex);
    }
    catch(IllegalAccessException ex){
      problem.add("Cannot call Action constructor using reflection (constructor not public). " + ex);
    }
    catch(IllegalArgumentException ex){
      problem.add("Cannot call Action constructor using reflection. " + ex);
    }
    catch(InvocationTargetException ex){
      String message = ex.getCause() == null ? ex.toString() : ex.getCause().getMessage();
      problem.add("Cannot call Action constructor using reflection (constructor threw exception). " + message);
    }
    
    if( problem.isNotEmpty() ){
      throw new RuntimeException("Problem constructing Action for URI " + Util.quote(fURIMappingString) + " " + Util.logOnePerLine(problem.getMessages()));
    }
    fLogger.info("URI " + Util.quote(fURIMappingString) + " successfully mapped to an instance of " + webAction);
    
    return result;
  }
  
  /**
   Return the <tt>String</tt> configured in <tt>web.xml</tt> as being the 
   base or root package that is to be ignored by the default Action mapping mechanism. 
   
   See <tt>web.xml</tt> for more information.
  */
  public static final String getImplicitMappingRemoveBasePackage(){
    //why is this method public?
    return new Config().getImplicitMappingRemoveBasePackage();
  }
   
  // PRIVATE
  
  /**
   Portion of the complete URL, which contains sufficient information to 
   to decide which {@link Action} is to be returned. 
  */
  private String fURIMappingString;
  
  /**
   Conventional field name used in {@link Action} classes. 
  */
  private static final String EXPLICIT_URI_MAPPING = "EXPLICIT_URI_MAPPING";
  
  /**
   Maps URIs to implementations of {@link Action}.
   
   <P>Key - String, taken from public static final field named {@link #EXPLICIT_URI_MAPPING}.
   <br>Value - Class for the {@link Action} having a <tt>EXPLICIT_URI_MAPPING</tt> field 
   of that given value.
   
   <P>At runtime, the request is inspected, and the corresponding {@link Action} is 
   created, using a constructor of a specific signature.
  */
  private static final Map<String, Class<Action>> fUriToActionMapping = new LinkedHashMap<String, Class<Action>>();
  
  private static final Logger fLogger = Util.getLogger(RequestParserImpl.class);
  
  private static void scanMappings(){
    fUriToActionMapping.clear(); //needed for reloading application : reloading app does not reload this class.
    Set<Class<Action>> actionClasses = ConfigReader.fetchConcreteClassesThatImplement(Action.class);
    AppException problems = new AppException();
    for(Class<Action> actionClass: actionClasses){
      Field explicitMappingField = null;
      try {
        explicitMappingField = actionClass.getField(EXPLICIT_URI_MAPPING);
      }
      catch (NoSuchFieldException ex){
        addMapping(actionClass,  getImplicitURI(actionClass), problems);
        continue;
      }
      addExplicitMapping(actionClass, explicitMappingField, problems);
    }
    //ensure that any problems will cause a failure to startup
    //thus, runtime exception are replaced with startup time exceptions
    if ( problems.isNotEmpty() ) {
      throw new RuntimeException("Problem(s) occurred while creating mapping of URIs to WebActions. " + Util.logOnePerLine(problems.getMessages()));
    }
  }

  private static void addExplicitMapping(Class<Action> aActionClass, Field aExplicitMappingField, AppException aProblems) {
    int modifiers = aExplicitMappingField.getModifiers();
    if (  Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) ) {
      try {
        Object fieldValue = aExplicitMappingField.get(null);
        if ( ! (fieldValue instanceof String) ){
          aProblems.add("Value for for " + EXPLICIT_URI_MAPPING + " field is not a String.");
        }
        addMapping(aActionClass, fieldValue.toString(), aProblems);
      }
      catch(IllegalAccessException ex){
        aProblems.add("Action " + aActionClass + ": cannot get value of field " + aExplicitMappingField);
      }
    }
    else {
      aProblems.add("Action " + aActionClass + ": field is not public static final : " + aExplicitMappingField);
    }
  }

  private static void addMapping(Class<Action> aClass, String aURI, AppException aProblems) {
    if( ! fUriToActionMapping.containsKey(aURI) ){
      fUriToActionMapping.put(aURI, aClass);
    }
    else {
      aProblems.add("Action " + aClass + ": mapping for URI " + aURI + " already in use by  " + fUriToActionMapping.get(aURI));
    }
  }
  
  private static String getImplicitURI(Class<Action> aActionClass){
    String result = aActionClass.getName(); //eg: com.blah.module.Whatever
    
    String prefix = getImplicitMappingRemoveBasePackage(); //com.blah
    if( ! Util.textHasContent(prefix) ){
      throw new RuntimeException("Init-param ImplicitMappingRemoveBasePackage must have content. See web.xml.");      
    }
    if( prefix.endsWith(".")){
      throw new RuntimeException("Init-param ImplicitMappingRemoveBasePackage must not include a trailing dot : " + Util.quote(prefix) + ". See web.xml.");
    }
    if ( ! result.startsWith(prefix) ){
      throw new RuntimeException("Class named " + Util.quote(aActionClass.getName()) + " does not start with expected base package " + Util.quote(prefix) + " See ImplicitMappingRemoveBasePackage in web.xml.");
    }
    
    result = result.replace('.','/'); // com/blah/module/Whatever
    result = result.substring(prefix.length()); // /module/Whatever
    fLogger.finest("Implicit mapping for " + Util.quote(aActionClass) + " is : " + Util.quote(result));
    return result;
  }
  
  private String removeExtension(String aURI){
    int firstPeriod = aURI.indexOf(".");
    if ( firstPeriod == NOT_FOUND ) {
      fLogger.severe("Cannot find extension for " + Util.quote(aURI));
    }
    return aURI.substring(0,firstPeriod);
  }
}