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