/* | |
* Licensed to the Apache Software Foundation (ASF) under one | |
* or more contributor license agreements. See the NOTICE file | |
* distributed with this work for additional information | |
* regarding copyright ownership. The ASF licenses this file | |
* to you under the Apache License, Version 2.0 (the | |
* "License"); you may not use this file except in compliance | |
* with the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, | |
* software distributed under the License is distributed on an | |
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
* KIND, either express or implied. See the License for the | |
* specific language governing permissions and limitations | |
* under the License. | |
*/ | |
package org.apache.empire.jsf2.app; | |
import java.sql.Connection; | |
import java.sql.SQLException; | |
import java.util.HashMap; | |
import java.util.Iterator; | |
import java.util.Locale; | |
import java.util.Map; | |
import java.util.ResourceBundle; | |
import javax.faces.FactoryFinder; | |
import javax.faces.application.Application; | |
import javax.faces.application.ApplicationFactory; | |
import javax.faces.application.FacesMessage; | |
import javax.faces.application.FacesMessage.Severity; | |
import javax.faces.component.NamingContainer; | |
import javax.faces.component.UIComponent; | |
import javax.faces.component.UIViewRoot; | |
import javax.faces.context.FacesContext; | |
import javax.servlet.ServletContext; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.sql.DataSource; | |
import org.apache.empire.commons.StringUtils; | |
import org.apache.empire.data.DataType; | |
import org.apache.empire.db.DBDatabase; | |
import org.apache.empire.exceptions.InternalException; | |
import org.apache.empire.exceptions.InvalidArgumentException; | |
import org.apache.empire.exceptions.NotSupportedException; | |
import org.apache.empire.jsf2.controls.TextAreaInputControl; | |
import org.apache.empire.jsf2.controls.TextInputControl; | |
import org.apache.empire.jsf2.impl.FacesImplementation; | |
import org.apache.empire.jsf2.impl.ResourceTextResolver; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
public abstract class WebApplication | |
{ | |
private static final Logger log = LoggerFactory.getLogger(WebApplication.class); | |
private static final String CONNECTION_ATTRIBUTE = "dbConnections"; | |
public static String APPLICATION_BEAN_NAME = "webApplication"; | |
protected TextResolver[] textResolvers = null; | |
private String webRoot = null; | |
private FacesImplementation facesImpl = null; | |
private static WebApplication appInstance = null; | |
public static WebApplication getInstance() | |
{ | |
if (appInstance==null) | |
log.warn("No WebApplication instance available. Please add a PostConstructApplicationEvent using WebAppStartupListener in your faces-config.xml to create the WebApplication object."); | |
// return instance | |
return appInstance; | |
} | |
protected abstract void init(ServletContext servletContext); | |
protected abstract DataSource getAppDataSource(DBDatabase db); | |
protected WebApplication() | |
{ // subscribe | |
log.info("WebApplication {} created", getClass().getName()); | |
// Must be a singleton | |
if (appInstance!=null) { | |
throw new RuntimeException("An attempt was made to create second instance of WebApplication. WebApplication must be a singleton!"); | |
} | |
// set Instance | |
appInstance = this; | |
} | |
/** | |
* Init the Application | |
* @param servletContext | |
*/ | |
public final void init(FacesImplementation facesImpl, FacesContext startupContext) | |
{ | |
// Only call once! | |
if (this.facesImpl!=null || this.webRoot!=null) | |
{ // already initialized | |
log.warn("WARNING: WebApplication has already been initialized! Continuing without init..."); | |
return; | |
} | |
// set imppl | |
this.facesImpl = facesImpl; | |
// webRoot | |
ServletContext servletContext = (ServletContext) startupContext.getExternalContext().getContext(); | |
webRoot = servletContext.getContextPath(); | |
servletContext.setAttribute("webRoot", webRoot); | |
servletContext.setAttribute("app", this); | |
// Init | |
init(servletContext); | |
// text resolvers | |
log.info("*** initTextResolvers() ***"); | |
ApplicationFactory appFactory = (ApplicationFactory) FactoryFinder.getFactory(FactoryFinder.APPLICATION_FACTORY); | |
Application app = appFactory.getApplication(); | |
initTextResolvers(app); | |
// Log info | |
log.info("*** WebApplication initialization complete ***"); | |
log.info("JSF-Implementation is '{}'", facesImpl.getClass().getName()); | |
log.info("WebRoot is '{}'", webRoot); | |
} | |
public void destroy() | |
{ | |
// Override if needed | |
} | |
/* Context handling */ | |
/** | |
* handle request cleanup | |
* @param ctx | |
*/ | |
public void onRequestComplete(final FacesContext ctx) | |
{ | |
releaseAllConnections(ctx); | |
} | |
/** | |
* handle view not found | |
* @param fc | |
* @param HttpServletRequest | |
*/ | |
public void onViewNotFound(final FacesContext fc, final HttpServletRequest req) | |
{ // View not Found Error | |
log.warn("No view found for request to '{}'. Use FacesUtils.redirectDirectly() to redirect to valid view.", req.getRequestURI()); | |
} | |
/** | |
* handle view change | |
* @param fc | |
* @param viewId | |
*/ | |
public void onChangeView(final FacesContext fc, String viewId) | |
{ // allow custom view change logic | |
// clear page resources | |
Map<String, Object> sm = FacesUtils.getSessionMap(fc); | |
if (sm!=null) | |
sm.remove(FacesUtils.PAGE_RESOURCE_MAP_ATTRIBUTE); | |
} | |
public void addJavascriptCall(final FacesContext fc, String function) | |
{ | |
throw new NotSupportedException(this, "addJavascriptCall"); | |
} | |
/** | |
* return the interface for Implementation specific features | |
* that are specific for Mojarra or MyFaces | |
*/ | |
public FacesImplementation getFacesImplementation() | |
{ | |
return facesImpl; | |
} | |
/** | |
* returns the web context path as returned from ServletContext.getContextPath() | |
*/ | |
public String getWebRoot() | |
{ | |
return webRoot; | |
} | |
/** | |
* returns the active locale for a given FacesContext | |
*/ | |
public Locale getContextLocale(final FacesContext ctx) | |
{ | |
UIViewRoot root; | |
// Start out with the default locale | |
Locale locale; | |
Locale defaultLocale = Locale.getDefault(); | |
locale = defaultLocale; | |
// See if this FacesContext has a ViewRoot | |
if (null != (root = ctx.getViewRoot())) | |
{ | |
// If so, ask it for its Locale | |
if (null == (locale = root.getLocale())) | |
{ | |
// If the ViewRoot has no Locale, fall back to the default. | |
locale = defaultLocale; | |
} | |
} | |
return locale; | |
} | |
public TextResolver getTextResolver(Locale locale) | |
{ | |
// No text Resolvers provided | |
if (textResolvers == null || textResolvers.length == 0) | |
{ | |
throw new NotSupportedException(this, "getTextResolver"); | |
} | |
// Lookup resolver for locale | |
for (int i = 0; i < textResolvers.length; i++) | |
if (locale.equals(textResolvers[i].getLocale())) | |
return textResolvers[i]; | |
// locale not found: return default | |
return textResolvers[0]; | |
} | |
public TextResolver getTextResolver(FacesContext ctx) | |
{ | |
return getTextResolver(getContextLocale(ctx)); | |
} | |
/** | |
* checks if the current context contains an error | |
* @param fc the FacesContext | |
* @return true if the context has an error set or false otherwise | |
*/ | |
public boolean hasError(final FacesContext fc) | |
{ | |
Iterator<FacesMessage> msgIterator = fc.getMessages(); | |
if (msgIterator != null) | |
{ // Check Messages | |
while (msgIterator.hasNext()) | |
{ // Check Severity | |
Severity fms = msgIterator.next().getSeverity(); | |
if (fms == FacesMessage.SEVERITY_ERROR || fms == FacesMessage.SEVERITY_FATAL) | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* returns true if a form input element has been partially submitted | |
* @param fc the Faces Context | |
* @return the componentId or null if no partial submit was been performed | |
*/ | |
public boolean isPartialSubmit(final FacesContext fc) | |
{ | |
// Override for your JSF component Framework. e.g. for IceFaces | |
// Map<String,String> parameterMap = fc.getExternalContext().getRequestParameterMap(); | |
// return ObjectUtils.getBoolean(parameterMap.get("ice.submit.partial")); | |
return false; | |
} | |
/** | |
* returns the componentId for which a partial submit has been performed. | |
* @param fc the Faces Context | |
* @return the componentId or null if no partial submit was been performed | |
*/ | |
public String getPartialSubmitComponentId(final FacesContext fc) | |
{ | |
// Override for your JSF component Framework. e.g. for IceFaces | |
// Map<String,String> parameterMap = fc.getExternalContext().getRequestParameterMap(); | |
// return parameterMap.get("ice.event.captured"); | |
return null; | |
} | |
/** | |
* finds the component with the given id that is located in the same NamingContainer as a given component | |
* @param fc the FacesContext | |
* @param componentId the component id | |
* @param nearComponent a component within the same naming container from which to start the search (optional) | |
* @return the component or null if no component was found | |
*/ | |
public UIComponent findComponent(FacesContext fc, String componentId, UIComponent nearComponent) | |
{ | |
if (StringUtils.isEmpty(componentId)) | |
throw new InvalidArgumentException("componentId", componentId); | |
// Begin search near given component (if any) | |
UIComponent component = null; | |
if (nearComponent != null) | |
{ // Search below the nearest naming container | |
component = nearComponent.findComponent(componentId); | |
if (component == null) | |
{ // Recurse upwards | |
UIComponent nextParent = nearComponent; | |
while (true) | |
{ | |
nextParent = nextParent.getParent(); | |
// search NamingContainers only | |
while (nextParent != null && !(nextParent instanceof NamingContainer)) | |
{ | |
nextParent = nextParent.getParent(); | |
} | |
if (nextParent == null) | |
{ | |
break; | |
} | |
else | |
{ | |
component = nextParent.findComponent(componentId); | |
} | |
if (component != null) | |
{ | |
break; | |
} | |
} | |
} | |
} | |
// Not found. Search the entire tree | |
if (component == null) | |
component = findChildComponent(fc.getViewRoot(), componentId); | |
// done | |
return component; | |
} | |
/** | |
* finds a child component with the given id that is located below the given parent component | |
* @param parent the parent | |
* @param componentId the component id | |
* @return the component or null if no component was found | |
*/ | |
public static UIComponent findChildComponent(UIComponent parent, String componentId) | |
{ | |
UIComponent component = null; | |
if (parent.getChildCount() == 0) | |
return null; | |
Iterator<UIComponent> children = parent.getChildren().iterator(); | |
while (children.hasNext()) | |
{ | |
UIComponent nextChild = children.next(); | |
if (nextChild instanceof NamingContainer) | |
{ | |
component = nextChild.findComponent(componentId); | |
} | |
if (component == null) | |
{ | |
component = findChildComponent(nextChild, componentId); | |
} | |
if (component != null) | |
{ | |
break; | |
} | |
} | |
return component; | |
} | |
/** | |
* returns the default input control type for a given data Type | |
* @see org.apache.empire.jsf2.controls.InputControlManager | |
* @param dataType | |
* @return an Input Cnotrol type | |
*/ | |
public String getDefaultControlType(DataType dataType) | |
{ | |
switch (dataType) | |
{ | |
case CLOB: | |
return TextAreaInputControl.NAME; | |
default: | |
return TextInputControl.NAME; | |
} | |
} | |
/* Message handling */ | |
protected void initTextResolvers(Application app) | |
{ | |
int count = 0; | |
Iterator<Locale> locales = app.getSupportedLocales(); | |
for (count = 0; locales.hasNext(); count++) | |
{ | |
locales.next(); | |
} | |
// get message bundles | |
String messageBundle = app.getMessageBundle(); | |
textResolvers = new TextResolver[count]; | |
locales = app.getSupportedLocales(); | |
for (int i = 0; locales.hasNext(); i++) | |
{ | |
Locale locale = locales.next(); | |
textResolvers[i] = new ResourceTextResolver(ResourceBundle.getBundle(messageBundle, locale)); | |
log.info("added TextResolver for {} bundle='{}'", locale.getLanguage(), messageBundle); | |
} | |
} | |
/** | |
* returns a connection from the connection pool | |
* | |
* @return | |
*/ | |
protected Connection getConnection(DBDatabase db) | |
{ | |
// Get From Pool | |
try | |
{ // Obtain a connection | |
Connection conn = getAppDataSource(db).getConnection(); | |
conn.setAutoCommit(false); | |
return conn; | |
} | |
catch (SQLException e) | |
{ | |
log.error("Failed to get connection from pool.", e); | |
throw new InternalException(e); | |
} | |
} | |
/** | |
* releases a connection from the connection pool | |
*/ | |
protected void releaseConnection(DBDatabase db, Connection conn, boolean commit) | |
{ | |
try | |
{ // release connection | |
if (conn == null) | |
{ | |
return; | |
} | |
// Commit or rollback connection depending on the exit code | |
if (commit) | |
{ // success: commit all changes | |
db.commit(conn); | |
log.debug("REQUEST commited."); | |
} | |
else | |
{ // failure: rollback all changes | |
db.rollback(conn); | |
log.debug("REQUEST rolled back."); | |
} | |
// Release Connection | |
conn.close(); | |
// done | |
if (log.isDebugEnabled()) | |
log.debug("REQUEST returned connection to pool."); | |
} | |
catch (SQLException e) | |
{ | |
log.error("Error releasing connection", e); | |
e.printStackTrace(); | |
} | |
} | |
/** | |
* returns a connection for the current Request | |
*/ | |
public Connection getConnectionForRequest(FacesContext fc, DBDatabase db) | |
{ | |
if (fc == null) | |
throw new InvalidArgumentException("FacesContext", fc); | |
if (db == null) | |
throw new InvalidArgumentException("DBDatabase", db); | |
// Get Conneciton map | |
@SuppressWarnings("unchecked") | |
Map<DBDatabase, Connection> connMap = (Map<DBDatabase, Connection>) FacesUtils.getRequestAttribute(fc, CONNECTION_ATTRIBUTE); | |
if (connMap != null && connMap.containsKey(db)) | |
return connMap.get(db); | |
// Pooled Connection | |
Connection conn = getConnection(db); | |
if (conn == null) | |
return null; | |
// Add to map | |
if (connMap == null) | |
{ | |
connMap = new HashMap<DBDatabase, Connection>(); | |
FacesUtils.setRequestAttribute(fc, CONNECTION_ATTRIBUTE, connMap); | |
} | |
connMap.put(db, conn); | |
return conn; | |
} | |
/** | |
* releases the current request connection | |
* @param fc the FacesContext | |
* @param commit when true changes are committed otherwise they are rolled back | |
*/ | |
public void releaseAllConnections(final FacesContext fc, boolean commit) | |
{ | |
@SuppressWarnings("unchecked") | |
Map<DBDatabase, Connection> connMap = (Map<DBDatabase, Connection>) FacesUtils.getRequestAttribute(fc, CONNECTION_ATTRIBUTE); | |
if (connMap != null) | |
{ // Walk the connection map | |
for (Map.Entry<DBDatabase, Connection> e : connMap.entrySet()) | |
{ | |
releaseConnection(e.getKey(), e.getValue(), commit); | |
} | |
// remove from request map | |
FacesUtils.setRequestAttribute(fc, CONNECTION_ATTRIBUTE, null); | |
} | |
} | |
public void releaseAllConnections(final FacesContext fc) | |
{ | |
releaseAllConnections(fc, !hasError(fc)); | |
} | |
public void releaseConnection(final FacesContext fc, DBDatabase db, boolean commit) | |
{ | |
@SuppressWarnings("unchecked") | |
Map<DBDatabase, Connection> connMap = (Map<DBDatabase, Connection>) FacesUtils.getRequestAttribute(fc, CONNECTION_ATTRIBUTE); | |
if (connMap != null && connMap.containsKey(db)) | |
{ // Walk the connection map | |
releaseConnection(db, connMap.get(db), commit); | |
connMap.remove(db); | |
if (connMap.size() == 0) | |
FacesUtils.setRequestAttribute(fc, CONNECTION_ATTRIBUTE, null); | |
} | |
} | |
public void releaseConnection(final FacesContext fc, DBDatabase db) | |
{ | |
releaseConnection(fc, db, !hasError(fc)); | |
} | |
} |