package hirondelle.web4j;
import static hirondelle.web4j.util.Consts.NEW_LINE;
import hirondelle.web4j.action.Action;
import hirondelle.web4j.action.ResponsePage;
import hirondelle.web4j.database.ConnectionSource;
import hirondelle.web4j.database.DAOException;
import hirondelle.web4j.database.DbConfig;
import hirondelle.web4j.model.AppException;
import hirondelle.web4j.model.BadRequestException;
import hirondelle.web4j.model.ConvertParamImpl;
import hirondelle.web4j.model.DateTime;
import hirondelle.web4j.model.Decimal;
import hirondelle.web4j.model.Id;
import hirondelle.web4j.readconfig.ConfigReader;
import hirondelle.web4j.readconfig.InitParam;
import hirondelle.web4j.request.RequestParser;
import hirondelle.web4j.request.RequestParserImpl;
import hirondelle.web4j.security.ApplicationFirewall;
import hirondelle.web4j.security.ApplicationFirewallImpl;
import hirondelle.web4j.security.FetchIdentifierOwner;
import hirondelle.web4j.security.UntrustedProxyForUserId;
import hirondelle.web4j.security.UntrustedProxyForUserIdImpl;
import hirondelle.web4j.util.Stopwatch;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.WebUtil;
import hirondelle.web4j.webmaster.EmailerImpl;
import hirondelle.web4j.webmaster.TroubleTicket;
import java.io.IOException;
import java.math.RoundingMode;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.jsp.JspFactory;
/**
Single point of entry for serving dynamic pages.
<P>The application can serve content both directly (by simple, direct reference to
a JSP's URL), and indirectly, through this <tt>Controller</tt>.
<P>Like almost all servlets, this class is safe for multi-threaded environments.
<P>Validates user input and request parameters, interacts with a datastore,
and places problem domain model objects in scope for eventual rendering by a JSP.
Performs either a forward or a redirect, according to the instructions of the
{@link Action}.
<P>Emails are sent to the webmaster when :
<ul>
<li>an unexpected problem occurs (the email will include extensive diagnostic
information, including a stack trace)
<li>servlet response times degrade to below a configured level
</ul>
<P>This class is in a distinct package for two reasons :
<ul>
<li>to make it easier to find, since it is at the very top of the hierarchy
<li>to force the <tt>Controller</tt> to use only the public aspects of
the <tt>ui</tt> package. This ensures it remains at a high level of abstraction.
</ul>
<P>There are key-names defined in this class (see below). Their names need to be
long-winded (<tt>web4j_key_for_...</tt>), unfortunately, in order to
avoid conflict with other tools, including your application.
*/
public class Controller extends HttpServlet {
/**
Name and version number of the WEB4J API.
<P>Value: {@value}.
<P>Upon startup, this item is logged at <tt>CONFIG</tt> level. (This item is
is simply a hard-coded field in this class. It is not configured in <tt>web.xml</tt>.)
*/
public static final String WEB4J_VERSION = "WEB4J/4.6.2";
/**
Key name for the application's character encoding, placed in application scope
as a <tt>String</tt> upon startup. This character encoding (charset) is set
as an HTTP header for every reponse.
<P>Key name: {@value}.
<P>Configured in <tt>web.xml</tt>. The value <tt>UTF-8</tt> is highly recommended.
*/
public static final String CHARACTER_ENCODING = "web4j_key_for_character_encoding";
/**
Key name for the webmaster email address, placed in application scope
as a <tt>String</tt> upon startup.
<P>Key name: {@value}.
<P>Configured in <tt>web.xml</tt>.
*/
public static final String WEBMASTER = "web4j_key_for_webmaster";
/**
Key name for the default {@link Locale}, placed in application scope
as a <tt>Locale</tt> upon startup.
<P>Key name: {@value}.
<P>The application programmer is encouraged to use this key for any
<tt>Locale</tt> stored in <em>session</em> scope : the <em>default</em> implementation
of {@link hirondelle.web4j.request.LocaleSource} will always search for this
key in increasingly larges scopes. Thus, the default mechanism will
automatically use the user-specific <tt>Locale</tt> as an override to
the default one.
<P>Configured in <tt>web.xml</tt>.
*/
public static final String LOCALE = "web4j_key_for_locale";
/**
Key name for the default {@link TimeZone}, placed in application scope
as a <tt>TimeZone</tt> upon startup.
<P>Key name: {@value}.
<P>The application programmer is encouraged to use this key for any
<tt>TimeZone</tt> stored in <em>session</em> scope : the <em>default</em> implementation
of {@link hirondelle.web4j.request.TimeZoneSource} will always search for this
key in increasingly larges scopes. Thus, the default mechanism will
automatically use the user-specific <tt>TimeZone</tt> as an override to
the default one.
<P>Configured in <tt>web.xml</tt>.
*/
public static final String TIME_ZONE = "web4j_key_for_time_zone";
/**
Key name for the most recent {@link TroubleTicket}, placed in application scope when a
problem occurs.
<P>Key name: {@value}.
*/
public static final String MOST_RECENT_TROUBLE_TICKET = "web4j_key_for_most_recent_trouble_ticket";
/**
Key name for the startup time, placed in application scope as a {@link DateTime} upon startup.
<P>Key name: {@value}.
*/
public static final String START_TIME = "web4j_key_for_start_time";
/**
Key name for the URI for the current request, placed in request scope as a <tt>String</tt>.
<P>Key name: {@value}.
<P>Somewhat bizarrely, the servlet API does not allow direct access to this item.
*/
public static final String CURRENT_URI = "web4j_key_for_current_uri";
/**
Perform operations to be executed only upon startup of
this application, and not during its regular operation.
<P>Operations include :
<ul>
<li>log version and configuration information
<li>distribute configuration information in <tt>web.xml</tt> to the various
parts of WEB4J
<li>place an {@link ApplicationInfo} object into application scope
<li>place the configured character encoding into application scope, for use in JSPs
<li>call {@link StartupTasks#startApplication(ServletConfig)}, to
allow the application to perform its own startup tasks
<li>perform various validations
</ul>
<P>One or more of the application's databases may not be running when
the web application starts. Upon startup, this Controller first queries each database
for simple name and version information. If that query fails, then the database is
assumed to be "down", and the app's implementation of {@link StartupTasks}
(which usually fetches code tables from the database) is not called.
<P>The web app, however, will not terminate. Instead, this Controller will keep
attempting to connect for each incoming request. When all databases are
determined to be healthy, the Controller will perform the database initialization
tasks it usually performs upon startup, and the app will then function normally.
<P>If the database subsequently goes down again, then this Controller will not take
any special action. Instead, the container's connection pool should be configured to
attempt to reconnect automatically on the application's behalf.
*/
@Override public final void init(ServletConfig aConfig) throws ServletException {
super.init(aConfig);
fConfig = aConfig;
Stopwatch stopwatch = new Stopwatch();
stopwatch.start();
BuildImpl.init(aConfig); //first load of application-specific classes; configures and begins logging as well
displaySystemProperties();
displayConfigInfo(aConfig);
setPoorPerformanceThreshold(aConfig);
setCharacterEncodingAndPutIntoAppScope(aConfig);
putWebmasterEmailAddressIntoAppScope(aConfig);
putDefaultLocaleIntoAppScope(aConfig);
putDefaultTimeZoneIntoAppScope(aConfig);
putStartTimeIntoAppScope(aConfig);
fLogger.fine("System properties and first app scope items completed " + stopwatch + " after start.");
/*
Implementation Note
There are strong order dependencies here: ConfigReader is used later in the
init of SqlStatement, for example
*/
ConfigReader.init(aConfig);
RequestParser.initUiLayer(aConfig); //inits other classes in that layer as well
WebUtil.init(aConfig);
//This will be the first loading of application-specific classes.
//This will cause static fields to be initialized.
ApplicationInfo appInfo = BuildImpl.forApplicationInfo();
displayVersionInfo(aConfig, appInfo);
placeAppInfoIntoAppScope(aConfig, appInfo);
TroubleTicket.init(aConfig, appInfo);
initMoney(aConfig);
fLogger.config("Calling ConnectionSource.init(ServletConfig).");
ConnectionSource connSource = BuildImpl.forConnectionSource();
connSource.init(aConfig);
fLogger.fine("Init of internal classes, ConnectionSource completed " + stopwatch + " after start.");
tryDatabaseInitAndStartupTasks();
fLogger.fine("Database init and startup tasks completed " + stopwatch + " after start.");
CheckModelObjects checkModelObjects = new CheckModelObjects();
checkModelObjects.performChecks();
stopwatch.stop();
fLogger.fine("Cross-Site Scripting scan completed " + stopwatch + " after start.");
fLogger.config("*** SUCCESS : STARTUP COMPLETED SUCCESSFULLY. *** Total startup time : " + stopwatch );
}
/** Log the name and version of the application. */
@Override public void destroy() {
ApplicationInfo appInfo = BuildImpl.forApplicationInfo();
fLogger.config("Shutting Down Controller for " + appInfo.getName() + "/" + appInfo.getVersion());
}
/** Call {@link #processRequest}. */
@Override public final void doGet(HttpServletRequest aRequest, HttpServletResponse aResponse) throws ServletException, IOException {
logClasses(aRequest, aResponse);
processRequest(aRequest, aResponse);
}
/** Call {@link #processRequest}. */
@Override public final void doPost(HttpServletRequest aRequest, HttpServletResponse aResponse) throws ServletException, IOException {
logClasses(aRequest, aResponse);
processRequest(aRequest, aResponse);
}
/**
Handle all HTTP <tt>GET</tt> and <tt>POST</tt> requests.
<P>This method can be overridden, if desired. The great majority of applications will not need
to override this method.
<P>Operations include :
<ul>
<li>set the request character encoding (using the value configured in <tt>web.xml</tt>)
<li>set the <tt>charset</tt> HTTP header for the response (using the value configured in <tt>web.xml</tt>)
<li>react to a successful user login, using the configured implementation of {@link hirondelle.web4j.security.LoginTasks}
<li>get an instance of {@link RequestParser}
<li>get its {@link Action}, and execute it
<li>check for an ownership constraint (see {@link UntrustedProxyForUserId})
<li>perform either a forward or a redirect to the Action's {@link hirondelle.web4j.action.ResponsePage}
<li>if an unexpected problem occurs, create a {@link TroubleTicket}, log it, and
email it to the webmaster email address configured in <tt>web.xml</tt>
<li>if the response time exceeds a configured threshold, build a
{@link TroubleTicket}, log it, and email it to the webmaster address configured in <tt>web.xml</tt>
</ul>
*/
protected void processRequest(HttpServletRequest aRequest, HttpServletResponse aResponse) throws ServletException, IOException {
Stopwatch stopwatch = new Stopwatch();
stopwatch.start();
aRequest.setCharacterEncoding(fCHARACTER_ENCODING);
aResponse.setCharacterEncoding(fCHARACTER_ENCODING);
addCurrentUriToRequest(aRequest, aResponse);
RequestParser requestParser = RequestParser.getInstance(aRequest, aResponse);
try {
LoginTasksHelper loginHelper = new LoginTasksHelper();
loginHelper.reactToNewLogins(aRequest);
Action action = requestParser.getWebAction();
ApplicationFirewall appFirewall = BuildImpl.forApplicationFirewall();
appFirewall.doHardValidation(action, requestParser);
logAttributesForAllScopes(aRequest);
ensureDatabasesOk();
ResponsePage responsePage = checkOwnershipThenExecuteAction(action, requestParser);
if ( responsePage.hasBinaryData() ) {
fLogger.fine("Serving binary data. Controller not performing a forward or redirect.");
}
else {
if ( responsePage.getIsRedirect() ) {
redirect(responsePage, aResponse);
}
else {
forward(responsePage, aRequest, aResponse);
}
}
}
catch (BadRequestException ex){
//NOTE : sendError() commits the response.
if( Util.textHasContent(ex.getErrorMessage()) ){
aResponse.sendError(ex.getStatusCode(), ex.getErrorMessage());
}
else {
aResponse.sendError(ex.getStatusCode());
}
}
catch (Throwable ex) {
//Bugs OR rare conditions, for example datastore failure
logAndEmailSeriousProblem(ex, aRequest);
aResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
stopwatch.stop();
if ( stopwatch.toValue() >= fPOOR_PERFORMANCE_THRESHOLD ) {
logAndEmailPerformanceProblem(stopwatch.toValue(), aRequest);
}
}
/**
Change the {@link ResponsePage} according to {@link Locale}.
<P>This overridable default implementation does nothing, and returns <tt>null</tt>.
If the return value of this method is <tt>null</tt>, then the nominal <tt>ResponsePage</tt>
will be used without alteration. If the return value of this method is not <tt>null</tt>,
then it will be used to override the nominal <tt>ResponsePage</tt>.
<P>This method is intended for applications that use different JSPs for different Locales.
For example, if the nominal response is a forward to <tt>Blah_en.jsp</tt>, and the "real"
response should be <tt>Blah_fr.jsp</tt>, then this method can be overridden to return the
appropriate {@link ResponsePage}. <span class="highlight">This method is called only for
forward operations. If it is overridden, then its return value must also correspond to a forward
operation.</span>
<P><span class="highlight">This style of implementing translation is not recommended.</span>
Instead, please use the services of the <tt>hirondelle.web4j.ui.translate</tt> package.
*/
protected ResponsePage swapResponsePage(ResponsePage aResponsePage, Locale aLocale){
return null; //does nothing
}
/**
Inform the webmaster of an unexpected problem with the deployed application.
<P>Typically called when an unexpected <tt>Exception</tt> occurs in
{@link #processRequest}. Uses {@link TroubleTicket#mailToWebmaster}.
<P>Also, stores the trouble ticket in application scope, for possible
later examination.
*/
protected final void logAndEmailSeriousProblem (Throwable ex, HttpServletRequest aRequest) throws AppException {
TroubleTicket troubleTicket = new TroubleTicket(ex, aRequest);
fLogger.severe("TOP LEVEL CATCHING Throwable");
fLogger.severe( troubleTicket.toString() );
log("SERIOUS PROBLEM OCCURRED.");
log( troubleTicket.toString() );
fConfig.getServletContext().setAttribute(MOST_RECENT_TROUBLE_TICKET, troubleTicket);
troubleTicket.mailToWebmaster();
}
/**
Inform the webmaster of a performance problem.
<P>Called only when the response time of a request is above the threshold
value configured in <tt>web.xml</tt>.
<P>Builds a <tt>Throwable</tt> with a description of the problem, then creates and
emails a {@link TroubleTicket} to the webmaster.
@param aMilliseconds response time of a request in milliseconds
*/
protected final void logAndEmailPerformanceProblem(long aMilliseconds, HttpServletRequest aRequest) throws AppException {
String message =
"Response time of web application exceeds configured performance threshold." + NEW_LINE +
"Time : " + aMilliseconds + " milliseconds."
;
Throwable ex = new Throwable(message);
TroubleTicket troubleTicket = new TroubleTicket(ex, aRequest);
fLogger.severe("Poor response time : " + aMilliseconds + " milliseconds");
fLogger.severe( troubleTicket.toString() );
log("Poor response time : " + aMilliseconds + " milliseconds");
log( troubleTicket.toString() );
troubleTicket.mailToWebmaster();
}
// PRIVATE
/** Mutable field. Must be accessed in thread-safe way. */
private static boolean fDbStartupSuccess;
/** Mutable field. Must be accessed in thread-safe way. */
private static boolean fHasInitedDefaultImpls;
/**
The config must be saved. It is not accessible from a request, or from the context.
It may be needed after startup, should no db connections be initially available.
*/
private static ServletConfig fConfig;
/** Item configured in web.xml. */
private static final InitParam fPoorPerformanceThreshold = new InitParam(
"PoorPerformanceThreshold", "20"
);
/**
If any request takes longer than this many milliseconds to be processed, then
an email is sent to the webmaster. The web.xml states this configured time in
seconds, but milliseconds is used by this class to perform the comparison.
*/
private static long fPOOR_PERFORMANCE_THRESHOLD;
/** Item configured in web.xml. */
private static final InitParam fCharacterEncoding = new InitParam(
"CharacterEncoding", "UTF-8"
);
/**
Character encoding for this application.
<P>The Controller will assume that every request will have this
character encoding. In addition, this value will be placed in an
application scope attribute named {@link Controller#CHARACTER_ENCODING};
*/
private static String fCHARACTER_ENCODING;
/** Item configured in web.xml. */
private static final InitParam fDefaultLocale = new InitParam(
"DefaultLocale", "en"
);
/**
Default Locale for this application.
<P>Placed in an app scope attribute named {@link Controller#LOCALE} (as a
Locale object, not as a String).
*/
private static String fDEFAULT_LOCALE;
private static final InitParam fDefaultTimeZone = new InitParam(
"DefaultUserTimeZone", "GMT"
);
/**
Default TimeZone for this application.
<P>Placed in an app scope attribute named {@link Controller#TIME_ZONE} (as a
TimeZone object, not as a String).
*/
private static String fDEFAULT_TIME_ZONE;
/** Item configured in web.xml. */
private static final InitParam fWebmaster = new InitParam("Webmaster");
/** Item configured in web.xml. Default currency and rounding style. See {@link Decimal}. */
private static final InitParam fDecimalStyle = new InitParam("DecimalStyle", "HALF_EVEN,2");
/**
Webmaster email address for this application.
<P>This value will be placed in an application scope attribute
named {@link Controller#WEBMASTER};
*/
private static String fWEBMASTER;
private static final boolean DO_NOT_CREATE_SESSION = false;
private static final String OWNERSHIP_NO_SESSION =
"According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " +
"However, this request has no session, and ownership constraints work only when the user has logged in."
;
private static final String OWNERSHIP_NO_LOGIN =
"According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " +
"A session exists, but there is no valid login, and ownership constraints work only when the user has logged in."
;
private static final Logger fLogger = Util.getLogger(Controller.class);
private void logClasses(HttpServletRequest aRequest, HttpServletResponse aResponse) {
fLogger.finest("Request class :" + aRequest.getClass());
fLogger.finest("Response class :" + aResponse.getClass());
}
private void redirect (
ResponsePage aDestinationPage, HttpServletResponse aResponse
) throws IOException {
String urlWithSessionID = aResponse.encodeRedirectURL(aDestinationPage.toString());
fLogger.fine("REDIRECT: " + Util.quote(urlWithSessionID));
aResponse.sendRedirect( urlWithSessionID );
}
private void forward (
ResponsePage aResponsePage, HttpServletRequest aRequest, HttpServletResponse aResponse
) throws ServletException, IOException {
ResponsePage responsePage = possiblyAlterForLocale(aResponsePage, aRequest);
RequestDispatcher dispatcher = aRequest.getRequestDispatcher(responsePage.toString());
fLogger.fine("Forward : " + responsePage);
dispatcher.forward(aRequest, aResponse);
}
private ResponsePage possiblyAlterForLocale(ResponsePage aNominalForward, HttpServletRequest aRequest){
Locale locale = BuildImpl.forLocaleSource().get(aRequest);
ResponsePage langSpecificForward = swapResponsePage(aNominalForward, locale);
if ( langSpecificForward != null && langSpecificForward.getIsRedirect() ){
throw new RuntimeException(
"A 'forward' ResponsePage has been altered for Locale, but is no longer a forward : " + langSpecificForward
);
}
return (langSpecificForward != null) ? langSpecificForward : aNominalForward;
}
private void displaySystemProperties(){
String sysProps = Util.logOnePerLine(System.getProperties());
fLogger.config("System Properties " + sysProps);
}
private void displayVersionInfo(ServletConfig aConfig, ApplicationInfo aAppInfo){
ServletContext context = aConfig.getServletContext();
Map<String, String> info = new LinkedHashMap<String, String>();
info.put("Application", aAppInfo.getName() + "/" + aAppInfo.getVersion());
info.put("Server", context.getServerInfo());
info.put("Servlet API Version", context.getMajorVersion() + "." + context.getMinorVersion() );
if( JspFactory.getDefaultFactory() != null) {
//this item is null when outside the normal runtime environment.
info.put("Java Server Page API Version", JspFactory.getDefaultFactory().getEngineInfo().getSpecificationVersion());
}
info.put("Java Runtime Environment (JRE)", System.getProperty("java.version"));
info.put("Operating System", System.getProperty("os.name") + "/" + System.getProperty("os.version") );
info.put("WEB4J Version", WEB4J_VERSION);
fLogger.config("Versions" + Util.logOnePerLine(info));
}
private void displayConfigInfo(ServletConfig aConfig){
fLogger.config(
"Context Name : " + Util.quote(aConfig.getServletContext().getServletContextName())
);
Enumeration ctxParamNames = aConfig.getServletContext().getInitParameterNames();
Map<String, String> ctxParams = new LinkedHashMap<String, String>();
while ( ctxParamNames.hasMoreElements() ){
String name = (String)ctxParamNames.nextElement();
String value = aConfig.getServletContext().getInitParameter(name);
ctxParams.put(name, value);
}
fLogger.config( "Context Params : " + Util.logOnePerLine(ctxParams));
Enumeration initParamNames = aConfig.getInitParameterNames();
Map<String, String> initParams = new LinkedHashMap<String, String>();
while ( initParamNames.hasMoreElements() ){
String name = (String)initParamNames.nextElement();
String value = aConfig.getInitParameter(name);
initParams.put(name, value);
}
fLogger.config( "Servlet Params : " + Util.logOnePerLine(initParams));
}
private void initMoney(ServletConfig aConfig){
String moneyStyle = fDecimalStyle.fetch(aConfig).getValue();
String DELIMITER = ",";
StringTokenizer parser = new StringTokenizer(moneyStyle, DELIMITER);
RoundingMode rounding = RoundingMode.valueOf(parser.nextToken());
Integer numDecimals = Integer.valueOf(parser.nextToken());
Decimal.init(rounding, numDecimals);
}
private void setPoorPerformanceThreshold(ServletConfig aConfig){
int MILLISECONDS_PER_SECOND = 1000;
Integer seconds = Integer.valueOf(
fPoorPerformanceThreshold.fetch(aConfig).getValue()
);
fPOOR_PERFORMANCE_THRESHOLD = seconds.intValue() * MILLISECONDS_PER_SECOND;
}
private void setCharacterEncodingAndPutIntoAppScope(ServletConfig aConfig){
fCHARACTER_ENCODING = fCharacterEncoding.fetch(aConfig).getValue();
aConfig.getServletContext().setAttribute(
CHARACTER_ENCODING, fCHARACTER_ENCODING
);
}
private void putWebmasterEmailAddressIntoAppScope(ServletConfig aConfig){
fWEBMASTER = fWebmaster.fetch(aConfig).getValue();
aConfig.getServletContext().setAttribute(WEBMASTER, fWEBMASTER);
}
private void putDefaultLocaleIntoAppScope(ServletConfig aConfig){
fDEFAULT_LOCALE = fDefaultLocale.fetch(aConfig).getValue();
Locale defaultLocale = Util.buildLocale(fDEFAULT_LOCALE);
aConfig.getServletContext().setAttribute(
LOCALE, defaultLocale
);
}
private void putDefaultTimeZoneIntoAppScope(ServletConfig aConfig){
fDEFAULT_TIME_ZONE = fDefaultTimeZone.fetch(aConfig).getValue();
TimeZone defaultTimeZone = TimeZone.getTimeZone(fDEFAULT_TIME_ZONE);
aConfig.getServletContext().setAttribute(
TIME_ZONE, defaultTimeZone
);
}
private void putStartTimeIntoAppScope(ServletConfig aConfig){
aConfig.getServletContext().setAttribute(START_TIME, DateTime.now(getTimeZone()));
}
private void placeAppInfoIntoAppScope(ServletConfig aConfig, ApplicationInfo aAppInfo){
aConfig.getServletContext().setAttribute(
ApplicationInfo.KEY, aAppInfo
);
}
/**
Log attributes stored in the various scopes.
*/
private void logAttributesForAllScopes(HttpServletRequest aRequest){
//the following style is conservative, and is meant to avoid calls which may be expensive
//remember that the level of the HANDLER affects whether the item is emitted as well.
if( fLogger.getLevel() != null && fLogger.getLevel().equals(Level.FINEST) ) {
fLogger.finest("Application Scope Items " + Util.logOnePerLine(getApplicationScopeObjectsForLogging(aRequest)));
fLogger.finest("Session Scope Items " + Util.logOnePerLine(getSessionScopeObjectsForLogging(aRequest)));
fLogger.finest("Request Parameter Names " + Util.logOnePerLine(getRequestParamNamesForLogging(aRequest)));
}
}
/**
Return Map of name-value pairs of items in application scope.
<P>In many cases, the actual data will be quite lengthy. For instance, translation data is often
sizeable. Thus, this should be called only when logging at the highest level.
Logging should only be performed after the {@link ApplicationFirewall} has executed.
*/
private Map<String, Object> getApplicationScopeObjectsForLogging(HttpServletRequest aRequest){
Map<String, Object> result = new LinkedHashMap<String, Object>();
HttpSession session = aRequest.getSession(DO_NOT_CREATE_SESSION);
if ( session != null ){
ServletContext appScope = session.getServletContext();
Enumeration objNames = appScope.getAttributeNames();
while ( objNames.hasMoreElements() ){
String name = (String)objNames.nextElement();
result.put(name, appScope.getAttribute(name));
}
}
return result;
}
/**
Return a Map of keys and objects for each session attribute.
Logging should only be performed after the {@link ApplicationFirewall} has executed.
*/
private Map<String, Object> getSessionScopeObjectsForLogging(HttpServletRequest aRequest){
Map<String, Object> result = new LinkedHashMap<String, Object>();
HttpSession session = aRequest.getSession(DO_NOT_CREATE_SESSION);
if ( session != null ){
result.put( "(Session Created) : ", DateTime.forInstant(session.getCreationTime(), getTimeZone()));
result.put( "(Session Timeout - seconds) : ", new Integer(session.getMaxInactiveInterval()) );
Enumeration objNames = session.getAttributeNames();
while ( objNames.hasMoreElements() ){
String name = (String)objNames.nextElement();
result.put(name, session.getAttribute(name));
}
}
return result;
}
private TimeZone getTimeZone(){
return TimeZone.getTimeZone(fDEFAULT_TIME_ZONE);
}
/**
Return a Map of key names, objects for each request scope attribute.
Logging should only be performed after the {@link ApplicationFirewall} has executed.
*/
private Map<String, Object> getRequestParamNamesForLogging(HttpServletRequest aRequest) {
Map<String, Object> result = new LinkedHashMap<String, Object>();
Map input = aRequest.getParameterMap();
Iterator iter = input.keySet().iterator();
while( iter.hasNext() ) {
String key = (String)iter.next();
result.put(key, aRequest.getAttribute(key));
}
return result;
}
private void addCurrentUriToRequest(HttpServletRequest aRequest, HttpServletResponse aResponse){
String currentURI = WebUtil.getOriginalRequestURL(aRequest, aResponse);
aRequest.setAttribute(CURRENT_URI, currentURI);
}
/**
This method is synchronized! The cost is usually only that of a quick boolean check, which is acceptable.
However, if the databases are down, then the app will be noticeably slower, since these
init operations are a bit sluggish.
*/
private synchronized void ensureDatabasesOk() throws DAOException, AppException {
if ( ! fDbStartupSuccess ) {
tryDatabaseInitAndStartupTasks();
}
if( ! fDbStartupSuccess ) {
throw new RuntimeException("Cannot connect to one or more databases!! Cannot execute request.");
}
}
private void tryDatabaseInitAndStartupTasks() throws DAOException, AppException {
ConnectionSource connSrc = BuildImpl.forConnectionSource();
if ( connSrc.getDatabaseNames().isEmpty() ) {
fLogger.config("No databases in this application, since ConnectionSource returns an empty Set for database names.");
fDbStartupSuccess = true; //since no db at all
}
else {
fLogger.config("Attempting data layer startup tasks.");
fDbStartupSuccess = DbConfig.initDataLayer(getServletConfig(), DbConfig.UseInformalConfig.NO);
}
initDefaultImplementations(fConfig);
if ( fDbStartupSuccess ) {
fLogger.config("Performing app startup tasks specific to this web application, using its implementation of the StartupTasks interface.");
StartupTasks startupTasks = BuildImpl.forStartupTasks();
startupTasks.startApplication(fConfig);
}
else {
fLogger.severe("Failure: detected that at least one database is down.");
fLogger.severe("Will attempt to reconnect to databases for each request. When all databases known to be OK, then will run StartupTasks.");
}
}
/**
Must call just before {@link StartupTasks}.
<P>This ensures WEB4J does not mistakenly perform such initialization at a time other than
that available to {@link StartupTasks}. If custom impl's are used, the only place to init them
is in StartupTasks. It is prudent to do the init of default impls at the same place, to ensure
the defaults don't 'cheat', or have any unfair advantage over custom impls.
*/
private void initDefaultImplementations(ServletConfig aConfig){
if( ! fHasInitedDefaultImpls ) {
fLogger.config("Initializing web4j default implementations.");
ConvertParamImpl.init(aConfig);
EmailerImpl.init(aConfig);
ApplicationFirewallImpl.init(aConfig);
UntrustedProxyForUserIdImpl.init(aConfig);
RequestParserImpl.initWebActionMappings(aConfig);
fHasInitedDefaultImpls = true;
}
else {
fLogger.config("Web4j default implementations already initialized.");
}
}
private ResponsePage checkOwnershipThenExecuteAction(Action aAction, RequestParser aRequestParser) throws AppException, BadRequestException {
UntrustedProxyForUserId ownershipFirewall = BuildImpl.forOwnershipFirewall();
if ( ownershipFirewall.usesUntrustedIdentifier(aRequestParser) ) {
fLogger.fine("This request has an ownership constraint.");
enforceOwnershipConstraint(aAction, aRequestParser);
}
else {
fLogger.fine("No ownership constraint detected.");
if(aAction instanceof FetchIdentifierOwner) {
fLogger.warning("Action implements FetchIdentifierOwner, but no ownership constraint is defined in web.xml for this specific operation.");
}
}
return aAction.execute();
}
private void enforceOwnershipConstraint(Action aAction, RequestParser aRequestParser) throws AppException, BadRequestException {
if (aAction instanceof FetchIdentifierOwner ) {
FetchIdentifierOwner constraint = (FetchIdentifierOwner)aAction;
Id owner = constraint.fetchOwner();
String ownerText = (owner == null ? null : owner.getRawString());
HttpSession session = aRequestParser.getRequest().getSession(DO_NOT_CREATE_SESSION);
if( session == null ) {
ownershipConstraintNotImplementedCorrectly(OWNERSHIP_NO_SESSION);
}
if( aRequestParser.getRequest().getUserPrincipal() == null ) {
ownershipConstraintNotImplementedCorrectly(OWNERSHIP_NO_LOGIN);
}
String loggedInUserName = aRequestParser.getRequest().getUserPrincipal().getName();
if ( ! loggedInUserName.equals(ownerText) ) {
fLogger.severe(
"Violation of an ownership constraint! " +
"The currently logged in user-name ('" + loggedInUserName + "') does not match the name of the data-owner ('" + ownerText + "')."
);
throw new BadRequestException(HttpServletResponse.SC_BAD_REQUEST, "Ownership Constraint has been violated.");
}
}
else {
ownershipConstraintNotImplementedCorrectly(
"According to the configured UntrustedProxyForUserId implementation, the requested operation has an ownership constraint. " +
"Such constraints require the Action to implement the FetchIdentifierOwner interface, but this Action doesn't implement that interface."
);
}
fLogger.fine("Ownership constraint has been validated.");
}
private void ownershipConstraintNotImplementedCorrectly(String aMessage){
fLogger.severe(aMessage + " Please see the User Guide for more information on Ownership Constraints.");
throw new RuntimeException("Ownership Constraint not implemented correctly.");
}
}