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