001    package hirondelle.web4j.request;
002    
003    import java.util.logging.*;
004    import java.util.*;
005    import java.lang.reflect.*;
006    import javax.servlet.http.HttpServletRequest;
007    import javax.servlet.http.HttpServletResponse;
008    import javax.servlet.ServletConfig;
009    
010    import hirondelle.web4j.readconfig.ConfigReader;
011    import hirondelle.web4j.readconfig.InitParam;
012    import hirondelle.web4j.request.RequestParser;
013    import hirondelle.web4j.util.Util;
014    import hirondelle.web4j.action.Action;
015    import hirondelle.web4j.model.AppException;
016    import static hirondelle.web4j.util.Consts.NOT_FOUND;
017    
018    /**
019     <span class="highlight">Maps each HTTP request to a concrete {@link Action}.</span>
020     
021     <P> Default implementation of {@link RequestParser}.
022     
023     <P>This implementation extracts the <a  href="#URIMappingString">URI Mapping String</a> from the 
024     underlying request, and maps it to a specific {@link Action} class, and calls its constructor by passing 
025     a {@link RequestParser}. (Here, each {@link Action} must have a <tt>public</tt> constructor 
026     which takes a {@link RequestParser} as its single parameter.)
027     
028     <P>There are two kinds of mapping available :
029    <ul>
030     <li><a href="#ImplicitMapping">implicit mapping</a> - simple, and recommended
031     <li><a href="#ExplicitMapping">explicit mapping</a> - requires an extra step, and overrides the implicit mapping
032    </ul> 
033      
034     <P><a name="URIMappingString"><h3>URI Mapping String</h3>
035     The 'URI Mapping String' is extracted from the underlying request. It is simply the concatention of 
036     {@link HttpServletRequest#getServletPath()} and {@link HttpServletRequest#getPathInfo()} 
037     (minus the extension - <tt>.do</tt>, for example).  
038     
039     <P>(The servlet path is the part of the URI which has been mapped to a servlet by the <tt>servlet-mapping</tt> 
040     entries in the <tt>web.xml</tt>.)
041    
042     <P><a name="ImplicitMapping"><h3>Implicit Mapping</h3>
043     If no <a href="#ExplicitMapping">explicit mapping</a> exists in an <tt>Action</tt>, then it will <em>implicitly</em> 
044     map to the <a href="#URIMappingString">URI Mapping String</a> that corresponds to a <em>modified</em> version of its 
045     package-qualified name : 
046    <ul>
047     <li>take the package-qualified class name
048     <li>change '.' characters to '/'
049     <li><em>remove</em> the base package prefix, configured in <tt>web.xml</tt> as <tt>ImplicitMappingRemoveBasePackage</tt>
050    </ul>
051    
052     <P>Example of an implicit mapping :
053     <table cellpadding="3" cellspacing="0" border="1">
054      <tr><td>Class Name:</td><td>hirondelle.fish.main.member.MemberEdit</th></tr>
055      <tr><td><tt>ImplicitMappingRemoveBasePackage</tt> (web.xml):</td><td>hirondelle.fish</th></tr>
056      <tr><td>Implicit Mapping calculated as:</td><td>/main/member/MemberEdit</th></tr>
057     </table>
058     
059     <P>Which maps to the following requests :
060     
061     <P><table cellpadding="3" cellspacing="0" border="1">
062      <tr><td>Request 1:</td><td>http://www.blah.com/fish/main/member/MemberEdit.list</th></tr>
063      <tr><td>Request 2:</td><td>http://www.blah.com/fish/main/member/MemberEdit.do?Operation=List</th></tr>
064      <tr><td>URI Mapping String calculated as:</td><td>/main/member/MemberEdit</th></tr>
065     </table>
066     
067     <P><a name="ExplicitMapping"><h3>Explicit Mapping</h3>
068     An <tt>Action</tt> may declare an explicit mapping to a <a href="#URIMappingString">URI Mapping String</a> 
069     simply by declaring a field of the form (for example) : 
070     <PRE>
071      public static final String EXPLICIT_URI_MAPPING = "/translate/basetext/BaseTextEdit";
072     </PRE>
073     Explicit mappings override implicit mappings. 
074    
075    <P><h3>Fine-Grained Security</h3>
076     Fine-grained security allows <tt>&lt;security-constraint&gt;</tt> items to be specifed for various extensions, 
077     where the extensions represent various action verbs, such as <tt>.list</tt>, <tt>.change</tt>, and so on. 
078     In that case, the conventional <tt>.do</tt> is replaced with several different extensions.
079     See the User Guide for more information on fine-grained security.
080    
081     <P><h3>Looking Up Action, Given URI</h3>
082     It is a common requirement to look up an action class, given a URI. Various sources 
083     can be used to perform that task:
084    <ul>
085     <li>the application's javadoc listing of Constant Field Values can be 
086     quickly searched for an explicit <tt>EXPLICIT_URI_MAPPING</tt> 
087     <li>all mappings are logged upon startup at <tt>CONFIG</tt> level
088     <li>the source code itself can be searched, if necessary
089    </ul>
090    */
091    public class RequestParserImpl extends RequestParser {
092    
093      /**
094       Scan for {@link Action} mappings. Called by the framework upon startup. Scans for all classes 
095       that implement {@link Action}. Stores either an <a href="ImplicitMapping">implicit</a>  
096       or an <a href="#ExplicitMapping">explicit</a> mapping. Implicit mappings are the recommended style. 
097       
098       <P>If a problem with mapping is detected, then a {@link RuntimeException} is thrown, and 
099       the application will not load. This protects the application, by forcing some important 
100       errors to occur during startup, instead of during normal operation. Possible errors include :
101       <ul>
102       <li>the <tt>EXPLICIT_URI_MAPPING</tt> field is not a <tt>public static final String</tt>
103       <li>the same mapping is used for more than one {@link Action}
104       </ul>
105      */
106      public static void initWebActionMappings(ServletConfig aConfig){
107        fImplicitMappingRemoveBasePackage = fIMPLICIT_MAPPING_REMOVE_BASE_PACKAGE.fetch(aConfig).getValue();
108        scanMappings();
109        fLogger.config("URI Mappings : " + Util.logOnePerLine(fUriToActionMapping));
110      }
111    
112      /**
113       Constructor.
114        
115       @param aRequest passed to the super class.
116       @param aResponse passed to the super class.
117      */
118      public RequestParserImpl(HttpServletRequest aRequest, HttpServletResponse aResponse) {
119        super(aRequest, aResponse);
120        if (aRequest.getPathInfo() != null){
121          fURIMappingString = aRequest.getServletPath() + aRequest.getPathInfo();
122        }
123        else {
124          fURIMappingString = aRequest.getServletPath();
125        }
126        fLogger.fine("*** ________________________ NEW REQUEST _________________");
127        fURIMappingString = removeExtension(fURIMappingString);
128        fLogger.fine("URL Mapping String: " + fURIMappingString);
129      }
130      
131      /**
132       Map an HTTP request to a concrete implementation of {@link Action}.
133      
134       <P>Extract the <a href="#URIMappingString">URI Mapping String</a> from the underlying request, and 
135       map it to an {@link Action}.
136      */
137      @Override public final Action getWebAction() {
138        Action result = null;
139        AppException problem = new AppException();
140        Class webAction = fUriToActionMapping.get(fURIMappingString);
141        if ( webAction == null ) {
142          throw new RuntimeException("Cannot map URI to an Action class : " + Util.quote(fURIMappingString));
143        }
144        
145        Class[] ctorArgs = {RequestParser.class};
146        try {
147          Constructor ctor = webAction.getConstructor(ctorArgs);
148          result = (Action)ctor.newInstance(new Object[]{this});
149        }
150        catch(NoSuchMethodException ex){
151          problem.add("Action does not have public constructor having single argument of type 'RequestParser'.");
152        }
153        catch(InstantiationException ex){
154          problem.add("Cannot call Action constructor using reflection (class is abstract). " + ex);
155        }
156        catch(IllegalAccessException ex){
157          problem.add("Cannot call Action constructor using reflection (constructor not public). " + ex);
158        }
159        catch(IllegalArgumentException ex){
160          problem.add("Cannot call Action constructor using reflection. " + ex);
161        }
162        catch(InvocationTargetException ex){
163          String message = ex.getCause() == null ? ex.toString() : ex.getCause().getMessage();
164          problem.add("Cannot call Action constructor using reflection (constructor threw exception). " + message);
165        }
166        
167        if( problem.isNotEmpty() ){
168          throw new RuntimeException("Problem constructing Action for URI " + Util.quote(fURIMappingString) + " " + Util.logOnePerLine(problem.getMessages()));
169        }
170        fLogger.info("URI " + Util.quote(fURIMappingString) + " successfully mapped to an instance of " + webAction);
171        
172        return result;
173      }
174      
175      /**
176       Return the <tt>String</tt> configured in <tt>web.xml</tt> as being the 
177       base or root package that is to be ignored by the default Action mapping mechanism. 
178       
179       See <tt>web.xml</tt> for more information.
180      */
181      public static final String getImplicitMappingRemoveBasePackage(){
182        return fImplicitMappingRemoveBasePackage;
183      }
184       
185      // PRIVATE //
186      
187      /**
188       Portion of the complete URL, which contains sufficient information to 
189       to decide which {@link Action} is to be returned. 
190      */
191      private String fURIMappingString;
192      
193      /**
194       Conventional field name used in {@link Action} classes. 
195      */
196      private static final String EXPLICIT_URI_MAPPING = "EXPLICIT_URI_MAPPING";
197      
198      /**
199       Maps URIs to implementations of {@link Action}.
200       
201       <P>Key - String, taken from public static final field named {@link #EXPLICIT_URI_MAPPING}.
202       <br>Value - Class for the {@link Action} having a <tt>EXPLICIT_URI_MAPPING</tt> field 
203       of that given value.
204       
205       <P>At runtime, the request is inspected, and the corresponding {@link Action} is 
206       created, using a constructor of a specific signature.
207      */
208      private static final Map<String, Class<Action>> fUriToActionMapping = new LinkedHashMap<String, Class<Action>>();
209      
210      private static String fImplicitMappingRemoveBasePackage;
211      private static final InitParam fIMPLICIT_MAPPING_REMOVE_BASE_PACKAGE = new InitParam("ImplicitMappingRemoveBasePackage");
212      
213      private static final Logger fLogger = Util.getLogger(RequestParserImpl.class);
214      
215      private static void scanMappings(){
216        fUriToActionMapping.clear(); //needed for reloading application : reloading app does not reload this class.
217        Set<Class<Action>> actionClasses = ConfigReader.fetchConcreteClassesThatImplement(Action.class);
218        AppException problems = new AppException();
219        for(Class<Action> actionClass: actionClasses){
220          Field explicitMappingField = null;
221          try {
222            explicitMappingField = actionClass.getField(EXPLICIT_URI_MAPPING);
223          }
224          catch (NoSuchFieldException ex){
225            addMapping(actionClass,  getImplicitURI(actionClass), problems);
226            continue;
227          }
228          addExplicitMapping(actionClass, explicitMappingField, problems);
229        }
230        //ensure that any problems will cause a failure to startup
231        //thus, runtime exception are replaced with startup time exceptions
232        if ( problems.isNotEmpty() ) {
233          throw new RuntimeException("Problem(s) occurred while creating mapping of URIs to WebActions. " + Util.logOnePerLine(problems.getMessages()));
234        }
235      }
236    
237      private static void addExplicitMapping(Class<Action> aActionClass, Field aExplicitMappingField, AppException aProblems) {
238        int modifiers = aExplicitMappingField.getModifiers();
239        if (  Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) ) {
240          try {
241            Object fieldValue = aExplicitMappingField.get(null);
242            if ( ! (fieldValue instanceof String) ){
243              aProblems.add("Value for for " + EXPLICIT_URI_MAPPING + " field is not a String.");
244            }
245            addMapping(aActionClass, fieldValue.toString(), aProblems);
246          }
247          catch(IllegalAccessException ex){
248            aProblems.add("Action " + aActionClass + ": cannot get value of field " + aExplicitMappingField);
249          }
250        }
251        else {
252          aProblems.add("Action " + aActionClass + ": field is not public static final : " + aExplicitMappingField);
253        }
254      }
255    
256      private static void addMapping(Class<Action> aClass, String aURI, AppException aProblems) {
257        if( ! fUriToActionMapping.containsKey(aURI) ){
258          fUriToActionMapping.put(aURI, aClass);
259        }
260        else {
261          aProblems.add("Action " + aClass + ": mapping for URI " + aURI + " already in use by  " + fUriToActionMapping.get(aURI));
262        }
263      }
264      
265      private static String getImplicitURI(Class<Action> aActionClass){
266        String result = aActionClass.getName(); //eg: com.blah.module.Whatever
267        
268        String prefix = getImplicitMappingRemoveBasePackage(); //com.blah
269        if( ! Util.textHasContent(prefix) ){
270          throw new RuntimeException("Init-param ImplicitMappingRemoveBasePackage must have content. See web.xml.");      
271        }
272        if( prefix.endsWith(".")){
273          throw new RuntimeException("Init-param ImplicitMappingRemoveBasePackage must not include a trailing dot : " + Util.quote(prefix) + ". See web.xml.");
274        }
275        if ( ! result.startsWith(prefix) ){
276          throw new RuntimeException("Class named " + Util.quote(aActionClass.getName()) + " does not start with expected base package " + Util.quote(prefix) + " See ImplicitMappingRemoveBasePackage in web.xml.");
277        }
278        
279        result = result.replace('.','/'); // com/blah/module/Whatever
280        result = result.substring(prefix.length()); // /module/Whatever
281        fLogger.finest("Implicit mapping for " + Util.quote(aActionClass) + " is : " + Util.quote(result));
282        return result;
283      }
284      
285      private String removeExtension(String aURI){
286        int firstPeriod = aURI.indexOf(".");
287        if ( firstPeriod == NOT_FOUND ) {
288          fLogger.severe("Cannot find extension for " + Util.quote(aURI));
289        }
290        return aURI.substring(0,firstPeriod);
291      }
292    }