Some notes on the process of making this change to an application.
In addition to this other changes that have been included are:
- Java 6/7 to Java 12
- Usage of HTML5
- Removal of the Trinidad component library
- Use of Maven 3.*
- Move from JDeveloper to Eclipse or IntelliJ IDEA
A fundamental change is required in order to get a connection to a database. In order to get it to work with Tomcat some changes needed to be made to the good old bham.utils.backing.DBUtils
.
First configure Tomcat by adding the Oracle JDBC driver to the Tomcat libs folder. Then add the following Resource
and ResourceLink
elements:
<!-- server.xml -->
<GlobalNamingResources>
...
<Resource
accessToUnderlyingConnectionAllowed="true"
auth="Container"
driverClassName="oracle.jdbc.OracleDriver"
initialSize="25"
maxIdle="-1"
maxTotal="400"
maxWaitMillis="30"
name="jdbc/myapp"
password="..."
testOnBorrow="true"
type="javax.sql.DataSource"
url="..."
username="..."
validationQuery="select 1 from dual"/>
</GlobalNamingResources>
<!-- context.xml -->
<Context>
...
<ResourceLink
name="jdbc/myapp"
global="jdbc/myapp"
type="javax.sql.DataSource" />
</Context>
This will make the JNDI datasource available to any application running on the Tomcat instance.
We need to create a new class to load the JNDI datasource from Tomcat. At the same time it would also be useful to be able to create a datasource without a container like Tomcat when running tests with jUnit.
First things are basic interfaces to provide a datasource and connection objects:
import javax.sql.DataSource;
public interface DatabaseDataSource
{
DataSource getDataSource();
}
import java.sql.Connection;
import java.sql.SQLException;
public interface ConnectionFactory
{
Connection getConnection() throws SQLException;
}
We can then have multiple implementations of the datasource one for running in a container or running in jUnit:
// JndiDataSource is used when running in Tomcat
import lombok.extern.slf4j.Slf4j;
import javax.enterprise.inject.Default;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
@Slf4j
@Default
public class JndiDataSource implements DatabaseDataSource
{
private final String CONTEXT = "java:/comp/env";
private final String DATA_SOURCE = "jdbc/myapp";
private DataSource dataSource;
@Override
public DataSource getDataSource()
{
if (this.dataSource == null)
{
try
{
logger.debug("Get context");
Context ctx = new InitialContext();
Context envContext = (Context) ctx.lookup(this.CONTEXT);
this.dataSource = (DataSource) envContext.lookup(this.DATA_SOURCE);
}
catch (NamingException e)
{
e.printStackTrace();
}
}
return this.dataSource;
}
}
// JdbcDataSource is used when running in jUnit
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Alternative;
import javax.sql.DataSource;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Properties;
@ApplicationScoped
@Alternative
public class JdbcDataSource implements DatabaseDataSource
{
private OracleDataSource dataSource;
public DataSource getDataSource()
{
try
{
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
var databaseProperties = new File(
classLoader.getResource("conf/database.properties").getFile()
);
Properties p = new Properties();
p.load(new FileReader(databaseProperties));
this.dataSource = new OracleDataSource();
dataSource.setURL(p.getProperty("url"));
dataSource.setUser(p.getProperty("username"));
dataSource.setPassword(p.getProperty("password"));
}
catch (SQLException | IOException e)
{
e.printStackTrace();
}
return dataSource;
}
}
In order for the application to create connections to the datasource to execute queries we have a DatabaseConnectionFactory
object. This is a singleton that can be injected into any class that requires a database connection.
import lombok.NoArgsConstructor;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@NoArgsConstructor // applicationScope bean needs this to allow for proxy object creation
@ApplicationScoped
public class DatabaseConnectionFactory implements ConnectionFactory
{
private DataSource dataSource;
@Inject
public DatabaseConnectionFactory(DatabaseDataSource databaseDataSource)
{
this.dataSource = databaseDataSource.getDataSource();
}
@Override
public Connection getConnection() throws SQLException
{
DataSource dataSource = this.dataSource;
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
return conn;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="4.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">
<display-name>Admissions PTB</display-name>
<servlet>
<servlet-name>faces</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>faces</servlet-name>
<url-pattern>*.xhtml</url-pattern>
</servlet-mapping>
<context-param>
<param-name>facelets.SKIP_COMMENTS</param-name>
<param-value>true</param-value>
</context-param>
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<context-param>
<param-name>org.jboss.weld.development</param-name>
<param-value>true</param-value>
</context-param>
</web-app>
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_3.xsd"
version="2.3">
</faces-config>
This file is needed to trigger scanning of the project for beans.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2.xsd"
bean-discovery-mode="all">
</beans>
Rather than specificying beans in the faces-config.xml or beans.xml files we specify them via attributes.
For example:
@Named("backing_search")
@SessionScoped
public class Search extends BasePage
{
...
}
Will create a session scoped bean called backing_search
.
This is a massive change. Basically any class used in the application will need to be updated to work this way.
CSS and JavaScript files need to be in the 'resources' folder of the webapp directory. JSF will only look in here to resolve any page links.
To get jQuery UI images to load you need to edit the CSS file:
/* before */
.ui-icon{background-image:url("/images/ui-icons_444444_256x240.png")}
/* after */
.ui-icon{background-image:url(#{resource["css/images/ui-icons_444444_256x240.png"]})}
Web pages now have .xhtml
extensions instead of .jspx
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
<f:view encoding="UTF-8" contentType="text/html">
<h:head id="head">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<h:outputStylesheet name="css/..."/>
<title></title>
<f:attribute name="pageId" value="..."/>
<f:attribute name="pageName" value="..."/>
</h:head>
<h:body>
<p>Page content</p>
<!-- include standard js -->
<h:outputScript name="js/..."/>
</h:body>
</f:view>
</html>
Details for how to configure this are here.
Basically, add a class to the application like this:
import javax.enterprise.context.ApplicationScoped;
import javax.faces.annotation.FacesConfig;
@FacesConfig
@ApplicationScoped
public class Jsf23Activator {}
In JSF 1.2 you could have JSP and JSF expressions:
<!-- JSF 1.2 -->
<title>#{backing_search.shortMnemonic}</title>
<title>${backing_search.shortMnemonic}</title>
<!-- JSF 2.3 -->
<title>#{backing_search.shortMnemonic}</title>
In JSF 2.3 you can only have JSF epressions #{...}
// JSF 1.2
public String search_action() {
...
return "search";
}
<!-- JSF 1.2 faces-config.xml -->
<navigation-rule>
<from-view-id>/details.jspx</from-view-id>
<navigation-case>
<from-outcome>search</from-outcome>
<to-view-id>/search.jspx</to-view-id>
<redirect/>
</navigation-case>
</navigation-rule>
In JSF 2.3 the <navigation-rule>
is no longer needed. It will simply look for a page with the name that is returned:
// JSF 2.3
public String search_action() {
...
return "search"; // will load search.xhtml
}
By default this will not update the browser URL. It would display the same page path as the original page. To navigate to a new page and have the browser URL update like a normal GET request do the following:
<!-- JSF 2.3 -->
public String search_action() {
...
return "search?faces-redirect=true"; // will load search.xhtml and update the
// browser address bar to search.xhmtl without
// the `faces-redirect=true` bit
}
<!-- JSF 1.2 with Trinidad -->
xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:trh="http://myfaces.apache.org/trinidad/html"
xmlns:tr="http://myfaces.apache.org/trinidad"
<!-- JSF 2.3 -->
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
CSS Links:
<!-- JSF 1.2 -->
<link type="text/css" rel="stylesheet" href="css/..."/>
<!-- JSF 2.3 -->
<h:outputStylesheet name="css/..."/>
Script Links:
<!-- JSF 1.2 -->
<script type="text/javascript" language="JavaScript" src="js/...s"></script>
<!-- JSF 2.3 -->
<h:outputScript name="js/..."/>
These elements can now also be included in ui:composition
files and will be included in the calling page. You can specify a target parameter to say where in the calling page they appear:
<!-- JSF 2.3 -->
<h:outputStylesheet name="css/..." target="head"/>
<h:outputScript name="js/..." target="body" />
This will insert the CSS link in the calling page head element and the script tag at the end of the calling page body element.
The links are added in the order the ui:include
elements appear in the calling page.
<!-- JSF 1.2 with Trinidad -->
<tr:spacer width="10" height="1"/>
<!-- JSF 2.3 -->
<h:outputText value=" "/>
<!-- JSF 1.2 with Trinidad -->
<tr:outputText value="Some value" styleClass="CSS classes"/>
<!-- JSF 2.3 -->
<h:outputText value="Some value" styleClass="CSS classes"/>
There is no direct replacement for the Trinidad messages
tag so we need to make something simillar:
<!-- JSF 1.2 -->
<tr:panelBorderLayout styleClass="error">
<tr:messages globalOnly="true" text="Errors/Information:"/>
</tr:panelBorderLayout>
<!-- JSF 2.3 -->
<c:if test="#{not empty facesContext.getMessageList()}">
<div class="header-messages">
<h2>Errors/Information:</h2>
<h:messages layout="list" globalOnly="true" />
</div>
</c:if>
As we are making our own version of the Trinidad one we also need some CSS:
.header-messages {
border: solid 1px #999999;
color: #669966;
background: #f3f3f3;
margin-top: 5px;
}
.header-messages h2 {
border-bottom: solid 1px #cecece;
padding: 0 0 0 5px;
margin: 0;
font-size: 13pt;
}
.header-messages ul {
margin: 0;
padding: 5px 30px;
}
.header-messages li {
list-style-type: none;
}
<!-- JSF 1.2 -->
<c:if test="${...}">
</c:if>
<!-- JSF 2.3 -->
<c:if test="#{...}">
...
</c:if>
<!-- JSF 1.2 -->
<f:subview id="headerm">
<jsp:include page="header.jspx" flush="true"/>
</f:subview>
<!-- JSF 2.3 -->
<ui:include src="header.xhtml"/>
The filea you are including are defined as ui:composition
documents:
<ui:composition
xmlns="http://www.w3.org/1999/xhtml"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets">
...
</ui:composition>
tr:inputHidden
<!-- JSF 1.2 with Trinidad -->
<tr:inputHidden value="#{...}" id="..."/>
<!-- JSF 2.3 -->
<h:inputHidden value="#{...}" id="..."/>
There are a number of changes to how this element behaves.
<!-- JSF 1.2 with Trinidad -->
<tr:commandButton
text="View Selected"
styleClass="ui-button ui-corner-all ui-widget"
action="#{...}"
id="..." />
<!-- output -->
<button id="..." type="button" class="ui-button ui-corner-all ui-widget">View Selected</button>
<!-- JSF 2.3 -->
<h:commandButton
value="View Selected"
styleClass="ui-button ui-corner-all ui-widget"
action="#{...}"
id="..."/>
<!-- output -->
<input id="..." type="submit" value="View Selected" class="ui-button ui-corner-all ui-widget"/>
Because this is now rendered as an input element CSS styles for button elements may need to be revisited.
<!-- JSF 1.2 with Trinidad -->
<tr:forEach var="item" items="#{list}">
</tr:forEach>
<!-- JSF 2.3 -->
<c:forEach var="item" items="#{list}">
</c:forEach>
<!-- JSF 1.2 with Trinidad -->
<td>
<c:out value="${activity.year}"/>
</td>
<!-- JSF 2.3 -->
<td>
#{activity.year}
</td>
JSF 1.2 Page markup:
<!-- JSF 1.2 with Trinidad -->
<tr:panelGroupLayout id="ptbReviewLayout" partialTriggers="save">
...
</tr:panelGroupLayout>
<tr:commandButton text="Save"
action="#{backing.save_action}"
partialSubmit="true"
id="save" />
JSF 1.2 JavaScript events:
// JSF 1.2 with Trinidad
TrPage.getInstance().getRequestQueue().addStateChangeListener(function(state) {
...
});
TrPage.getInstance().addDomReplaceListener(function(oldDom, newDom) {
...
});
JSF 2.3 page markup:
<!-- JSF 2.3 -->
<h:panelGroup id="save">
...
</h:panelGroup>
<h:commandButton
value="Save"
action="#{backing.save_action}">
<f:ajax execute="@form" render="save"/>
</h:commandButton>
JSF 2.3 JavaScript event:
// JSF 2.3
jsf.ajax.addOnEvent(function(event){
...
});
<!-- JSF 1.2 with Trinidad -->
<tr:commandButton
text="View Document"
action="#{backing.view_document_action}">
<tr:setActionListener from="#{item.guid}" to="#{backing.selectedDocumentID}"/>
</tr:commandButton>
<!-- JSF 2.3 -->
<h:commandButton
value="View Document"
action="#{backing.view_document_action(item.guid)}">
</h:commandButton>
Now we are not using Trinidad the following lines can be removed when creating a client side multi select:
selectList.select2({
multiple: true,
placeholder: placeholder,
allowClear: true,
closeOnSelect: false
}).on('change', {
selectList: selectList,
valholder: valholder
}, this.getSelected);
// NO LONGER NEEDED! if left in will remove the first item in the list!
// .find("option")
// .eq(0)
// .remove();
A configuration object that is responsible for loading settings from various sources and then can be injected into other objects when needed is added, with a sub object for development specific config:
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
@Data
@Slf4j
public class AppConfig
{
private final Config conf;
private final String environment;
private final DevelopmentConfig development;
...
}
import lombok.Value;
@Value
public class DevelopmentConfig
{
private String userId;
}
In Servlets 3 Servlet Filters can be loaded dynamically at run time. This allows for different authentication process based on enviroment. In development we want to load a user ID based on an enviroment varialble. In test and production we want the normal token based authentication.
To set the filter at runtime we use a @WebListner
:
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.annotation.WebListener;
import java.util.EnumSet;
@WebListener
@Slf4j
public class FilterStartupListener implements ServletContextListener
{
@Inject
private AppConfig config;
@Override
public void contextInitialized(ServletContextEvent servletContextEvent)
{
ServletContext ctx = servletContextEvent.getServletContext();
if (config.getEnv().equals("development"))
{
FilterRegistration fr = ctx.addFilter("DevelopmentAuthenticationFilter", new DevelopmentAuthenticationFilter(config));
fr.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD), true, "/*");
} else
{
FilterRegistration fr = ctx.addFilter("AuthenticationFilter", AuthenticationFilter.class);
fr.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD), true, "/*");
}
}
}
The standard authentication filter is AuthenticationFilter
:
import lombok.extern.slf4j.Slf4j;
import uk.ac.bham.portal.utils.Encrypter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.GregorianCalendar;
@Slf4j
public class AuthenticationFilter implements Filter
{
private static final String[] ignoredURLs = { "/error" };
private static int TIMEDELAY = 1;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
...
}
}
And the development authentication filter is DevelopmentAuthenticationFilter
:
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigFactory;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Slf4j
public class DevelopmentAuthenticationFilter implements Filter
{
private static final String[] ignoredURLs = { "/error" };
private AppConfig config;
public DevelopmentAuthenticationFilter(AppConfig config)
{
this.config = config;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
{
...
String userId = this.config.getUserId();
...
}
}
Adding a hash or version number to the path of CSS or JavaScript files is something that web frameworks support. JSF works a bit differently as this is ability is not covered by the spec specifically and the expectation is you will version the entire resources folder. This is not practical so as a work around we append the application start time to the resource’s files. This means users should (it will not always work) get the latest copy of the application resources whenever it restarts.
First create an App
class:
import lombok.Value;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import java.util.Date;
@Named("app")
@ApplicationScoped
@Value
public class App
{
private Date startup;
public App()
{
this.startup = new Date();
}
}
This can then be referenced in pages:
<h:outputScript name="js/jquery.js?v=#{app.startup.time}"/>
Lombok is a useful Java library that saves you from writing a lot of boiler plate code. Some examples of frequently used annotations are:
- @Data - Classes annotated with @Data automatically get all of their getters and setters created automatically as well as toString, equals, hash and a required args constructor.
- @Slf4j - Classes annotated with @Slf4j automatically get static Log class property.
This file lives in the root of the project and allows you to set options for how lombok works. E.g:
lombok.log.fieldName=logger
will set the name of the log class property to logger
instead of log
.