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><security-constraint></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 }