Package hirondelle.web4j.request

Source Code of hirondelle.web4j.request.RequestParserImpl

package hirondelle.web4j.request;

import java.util.logging.*;
import java.util.*;
import java.lang.reflect.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletConfig;

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

/**
<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 is 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(ServletConfig aConfig){
    fImplicitMappingRemoveBasePackage = fIMPLICIT_MAPPING_REMOVE_BASE_PACKAGE.fetch(aConfig).getValue();
    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(){
    return fImplicitMappingRemoveBasePackage;
  }
  
  // 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 String fImplicitMappingRemoveBasePackage;
  private static final InitParam fIMPLICIT_MAPPING_REMOVE_BASE_PACKAGE = new InitParam("ImplicitMappingRemoveBasePackage");
 
  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);
  }
}
TOP

Related Classes of hirondelle.web4j.request.RequestParserImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.