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