Introduction
In this blog we will show how a WEB API created using Tapestry can be tested. Testing a web service is not straight forward as the PageTester, the default way of testing in Tapestry, doesn't allow for testing API. To overcome this difficulty we must start a Jetty server with custom/test web.xml. The test web.xml allows us to control the construction of objects. In this example we show how a mock service object can be inserted for testing, while using the full implementation for production.
This blog is part of a series of blogs on Tapestry that explains how to build a "simple" hello world example html page and expose the functionality of the page(s) via a WEB API. In a previous blog we showed how to setup a web API that responses in either XML or JSON. If you haven't read it yet, it's probably a good idea to have a look at this blog first before proceeding.
This blog picks things up from another previous blog in the series on testing page. The basic of testing a webpage generated with Tapestry are explained there. In this blog we will build on the code previously created.
Prerequisites
- This tutorial uses Eclipse with Maven
- Code hello_tapestry_pagetest
Setup
If you have been following the series or downloaded the code (see prerequisites) and you run the application, when entering the url localhost:8080/hello/api/xml/fred in your browser it should return the following
<HelloWorld xmlns="http://uglyduckling.nl/CreateXMLDOM"> <result>Hello fred</result> <version>0.0.1-SNAPSHOT-DEV</version> <timestamp>Mon May 02 09:17:40 CEST 2016</timestamp> </HelloWorld>
First we write a simple test to check if the Jetty server has started properly and gives us a response. To do this we will check if the placeholder page of the API is available. If it's loaded we know the service has started and it is properly configured. For this we use the PageTester as explained in the "Testing a page" blog.
public class ApiTest { private final String PAGE_NAME = "Api"; @Test public void confirmPlaceholderIsLoaded() { String appPackage = "nl.uglyduckling.hello"; String appName = "app"; String context = WEBAPP_FOLDER; PageTester tester = new PageTester(appPackage, appName, context); Document document = tester.renderPage(PAGE_NAME); String markup = document.toString(); String expectedWelcome = "placeholder"; assertTrue(markup.contains(expectedWelcome)); } }
Testing an API
To test the implementation of the service we need to be able to call the service using a very specific url. The url contains the parameters that configure the webservice so we need to be able to control them. Unfortunately this isn't possible using the PageTester. This means we need to setup and configure a Jetty instance ourselves to execute any tests we want to execute. We do so for the XML example localhost:8080/hello/api/xml/fred.
@Test() public void validateJettySetup() throws Exception { String baseFolder = TapestryRunnerConstants.MODULE_BASE_DIR.getAbsolutePath(); String webappFolder ="src/main/webapp"; String fullFolder = baseFolder + "/" + webappFolder; // Create Server Server server = new Server(8080); ServletHolder defaultServ = new ServletHolder("default", DefaultServlet.class); defaultServ.setInitParameter("resourceBase", fullFolder); defaultServ.setInitParameter("dirAllowed","true"); WebAppContext webapp = new WebAppContext(); webapp.setContextPath("/"); webapp.setWar(fullFolder); webapp.setDescriptor(fullFolder + "/" + "WEB-INF/web.xml"); server.setHandler(webapp); server.start(); String url = "http://localhost:8080/api/xml/fred"; HttpURLConnection httpConnection = (HttpURLConnection)new URL(url).openConnection(); assertEquals("Response Code", httpConnection.getResponseCode(), HttpStatus.OK_200); server.stop(); }
In this basic test we confirm that our configuration is okay and we get a valid response from the server. In order to check what is happening, put a break point just after server.start() and run the test in debug mode. Once the break point has been hit, open your browser and enter the url http://localhost:8080/api/xml/fred
To execute the same test using MVN we need to add a dependency on JUnit in the POM. Something like this should to the trick:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.7</version> </dependency>
It is probably smart to use the same version of JUnit here as the one that is configured for Surefire. The configuration of Surefire can be found on line 99 in the POM as shown at in the code files at the end of this blog.
Now that we have the basic setup for testing the API let's add a test to check if the XML comes out as expected. We expect two values to change for a call made to the API:
- name, which is input
- time stamp that is given in the XML.
With some refactoring we can clean up the validate Jetty setup test and check if we get back the name we expected by checking the stream response we get from the webserver. We do this in the confirmXmlResponseContainsFred test. Here we take the response stream from Jetty and check if the stream contains the name we expect (in this case "Fred").
public class ApiTest { private final String PAGE_NAME = "Api"; private final String URL = "http://localhost:8080/api/xml/fred"; private final String WEBAPP_FOLDER = "src/main/webapp"; private Server jettyServer; @Before public void setup() throws Exception { String baseFolder = TapestryRunnerConstants.MODULE_BASE_DIR.getAbsolutePath(); String fullFolder = baseFolder + "/" + WEBAPP_FOLDER; // Create Server jettyServer = new Server(8080); ServletHolder defaultServ = new ServletHolder("default", DefaultServlet.class); defaultServ.setInitParameter("resourceBase", fullFolder); defaultServ.setInitParameter("dirAllowed","true"); WebAppContext webapp = new WebAppContext(); webapp.setContextPath("/"); webapp.setWar(fullFolder); webapp.setDescriptor(fullFolder + "/" + "WEB-INF/web.xml"); jettyServer.setHandler(webapp); jettyServer.start(); } @After public void tearDown() throws Exception { jettyServer.stop(); } @Test() public void confirmXmlResponseContainsFred() throws Exception { HttpURLConnection httpConnection = (HttpURLConnection)new URL(URL).openConnection(); InputStream in = httpConnection.getInputStream(); String encoding = httpConnection.getContentEncoding(); encoding = encoding == null ? "UTF-8" : encoding; String body = IOUtils.toString(in, encoding); String expected = "fred"; assertEquals("Response Code", httpConnection.getResponseCode(), HttpStatus.OK_200); assertTrue("Expected " +expected + ", but was " + body, body.contains(expected)); } @Test() public void validateJettySetup() throws Exception { HttpURLConnection httpConnection = (HttpURLConnection)new URL(URL).openConnection(); assertEquals("Response Code", httpConnection.getResponseCode(), HttpStatus.OK_200); } }
Finally we would like to check if we get back the expected time stamp. This won't work using the strategy we followed up until this point as that time stamp will be different each time we make a call to the API. To get around this problem we will create a data service. The date service can be injected into Tapestry using inversion of control (IoC) making the service implementation configurable. Using the Page Tester we can in most case easily swap implementations of a service using IoC, but since we can't use Page Tester of the API we need to set things up at Jetty server level. This mean messing about with the web.xml, which will be explained in the next section in detail.
Injecting Mock Service in API
In the previous implementation we used XML and JSON responders based on ResponderBase. We used the Date object to get the time stamp information. The Date object was passed from the API object. Here lies the problem: the "new Date()" was a mistake with hindsight. We can fix this by using a Date Service that gives us the correct date.
public class Api { @Property @Inject @Symbol(SymbolConstants.APPLICATION_VERSION) private String version; @Inject private Logger logger; private final String TYPE_JSON = "JSON"; private final String TYPE_XML = "XML"; StreamResponse onActivate(String type, String userName) { ApiResponder apiResponder; Date date = new Date(); if (type.compareToIgnoreCase(TYPE_JSON) == 0) { apiResponder = new JsonResponder(version, date); } else if(type.compareToIgnoreCase(TYPE_XML) == 0) { apiResponder = new XmlResponder(version, date); } else { return new TextStreamResponse("plain/text", "first parameter must be XML or JSON. " + "Check http:\\\\localhost:8080\\hello\\api for the online documentation."); } return apiResponder.Render(userName); } }
Consider the DataService
public interface DateService { Date CurrentDate(); }
and implementation
public class DateServiceImpl implements DateService { @Override public Date CurrentDate() { Date date = new Date(); return date; } }
We also create a Mock or Stub for testing. (Of course you can make a smarter Mock or use a framework. Here we only want to demonstrate how to make use of the setup.)
public class DateServiceMock implements DateService { @Override public Date CurrentDate() { Calendar calendar = GregorianCalendar.getInstance(); int year = 1900; int month = 3; int date = 4; int hourOfDay = 10; int minute = 45; int second = 9; calendar.set(year, month, date, hourOfDay, minute, second); return calendar.getTime(); } }
With our service ready to go we can get Tapestry to use it by adding it to the binder in the AppModule. This will make the service ready to be used by Tapestry.
public class AppModule { public static void bind(ServiceBinder binder) { // binder.bind(MyServiceInterface.class, MyServiceImpl.class); // Make bind() calls on the binder object to define most IoC services. // Use service builder methods (example below) when the implementation // is provided inline, or requires more initialization than simply // invoking the constructor. binder.bind(DateService.class, DateServiceImpl.class); } ..... }
Now that Tapestry knows about our service we can inject it on the Api.java page. This is done in the following way:
@Inject private DateService dateService;
Putting everything together we get now Api page code with small changes from the original. The big advantage is that we can inject different implementations of the service using the configuration.
public class Api { @Property @Inject @Symbol(SymbolConstants.APPLICATION_VERSION) private String version; @Inject private Logger logger; @Inject private DateService dateService; private final String TYPE_JSON = "JSON"; private final String TYPE_XML = "XML"; StreamResponse onActivate(String type, String userName) { ApiResponder apiResponder; if (type.compareToIgnoreCase(TYPE_JSON) == 0) { apiResponder = new JsonResponder(version, dateService); } else if(type.compareToIgnoreCase(TYPE_XML) == 0) { apiResponder = new XmlResponder(version, dateService); } else { return new TextStreamResponse("plain/text", "first parameter must be XML or JSON. " + "Check http:\\\\localhost:8080\\hello\\api for the online documentation."); } return apiResponder.Render(userName); } }
Now we can refactor the XML and JSON renders to use the service like so
public class XmlResponder extends ResponderBase { public XmlResponder(String version, DateService dateService) { super(version, dateService); } @Override public TextStreamResponse Render(String userName) { try { String xml = getXmlDocumentResult(userName); return new TextStreamResponse("application/xml", xml); } catch (Exception e) { return new TextStreamResponse("plain/text", "Internal error " + e.getMessage()); } } private String getXmlDocumentResult(String userName) throws ParserConfigurationException, TransformerFactoryConfigurationError, TransformerException { DocumentBuilderFactory icFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder icBuilder; icBuilder = icFactory.newDocumentBuilder(); Document doc = icBuilder.newDocument(); Element mainRootElement = doc.createElementNS("http://uglyduckling.nl/CreateXMLDOM", "HelloWorld"); doc.appendChild(mainRootElement); mainRootElement.appendChild(buildXmlNode("result", "Hello " + userName, doc)); mainRootElement.appendChild(buildXmlNode("version", version, doc)); mainRootElement.appendChild(buildXmlNode("timestamp", dateService.CurrentDate(), doc)); String finalstring = documentToString(doc); return finalstring; } private String documentToString(Document doc) throws TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); DOMSource source = new DOMSource(doc); StringWriter outWriter = new StringWriter(); StreamResult result = new StreamResult( outWriter ); transformer.transform(source, result); StringBuffer sb = outWriter.getBuffer(); String finalstring = sb.toString(); return finalstring; } private Node buildXmlNode(String name, Date date, Document dom) { return buildXmlNode(name, date.toString(), dom); } private Node buildXmlNode(String name, String value, Document dom) { Element node = dom.createElement(name); node.appendChild(dom.createTextNode(value)); return node; } }
Before running tests against the API we need to have a configuration that runs injecting the Mock Data Service. This is tricky, since we are starting the Jetty server directly from the test in order to call the relevant urls. What can we do to configure the Jetty server to use a different web.xml for the tests? By convention we will use web-test.xml for testing and web.xml for production. The two configurations will be very similar, with the exception of the filter configuration.
<!-- Filter configuration --> <filter> <filter-name>test</filter-name> <filter-class>org.apache.tapestry5.TapestryFilter</filter-class> </filter> <filter-mapping> <filter-name>test</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping>
The default web.xml will have filter called app. Tapestry picks the module you configure here. So app will result in AppModule.java to be loaded and test in TestModule. All we need to do now is to create a TestModule with the binding to inject the mock and we are ready to test the API.
public class TestModule extends AppModule { public static void bind(ServiceBinder binder) { binder.bind(DateService.class, DateServiceMock.class); } }
In this object we make the binding of the service to the Mock explicit. We can now extend the Api Test with test for the date.
@Test() public void confirmXmlResponse() throws Exception { HttpURLConnection httpConnection = (HttpURLConnection)new URL(URL).openConnection(); InputStream in = httpConnection.getInputStream(); String encoding = httpConnection.getContentEncoding(); encoding = encoding == null ? "UTF-8" : encoding; String body = IOUtils.toString(in, encoding); String expected = "<timestamp>Wed Apr 04 10:45:09 CET 1900</timestamp>"; assertEquals("Response Code", httpConnection.getResponseCode(), HttpStatus.OK_200); assertTrue("Expected " +expected + ", but was " + body, body.contains(expected)); }
Conclusion
In this blog we showed how to deal with testing a stream response in Tapestry 5. Specifically we looked at API responding in XML or JSON streams. To make the testing a little more interesting we added a service that needed to be Mocked. We show how to configure the setup so the implementation of the service can be injected for production and a Mock of the service for testing.
The code of the project is available here : hello_tapestry_api_test
The code is also on GitHub here.
Code
Api.Java
package nl.uglyduckling.hello.pages; import nl.uglyduckling.hello.responder.ApiResponder; import nl.uglyduckling.hello.responder.JsonResponder; import nl.uglyduckling.hello.responder.XmlResponder; import nl.uglyduckling.hello.services.DateService; import org.apache.tapestry5.StreamResponse; import org.apache.tapestry5.SymbolConstants; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.ioc.annotations.Inject; import org.apache.tapestry5.ioc.annotations.Symbol; import org.apache.tapestry5.util.TextStreamResponse; import org.slf4j.Logger; public class Api { @Property @Inject @Symbol(SymbolConstants.APPLICATION_VERSION) private String version; @Inject private Logger logger; @Inject private DateService dateService; private final String TYPE_JSON = "JSON"; private final String TYPE_XML = "XML"; StreamResponse onActivate(String type, String userName) { ApiResponder apiResponder; if (type.compareToIgnoreCase(TYPE_JSON) == 0) { apiResponder = new JsonResponder(version, dateService); } else if(type.compareToIgnoreCase(TYPE_XML) == 0) { apiResponder = new XmlResponder(version, dateService); } else { return new TextStreamResponse("plain/text", "first parameter must be XML or JSON. " + "Check http:\\\\localhost:8080\\hello\\api for the online documentation."); } return apiResponder.Render(userName); } }
ApiTest.Java
package nl.uglyduckling.hello.pages; import static org.junit.Assert.*; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import org.apache.commons.io.IOUtils; import org.apache.tapestry5.dom.Document; import org.apache.tapestry5.test.PageTester; import org.apache.tapestry5.test.TapestryRunnerConstants; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.webapp.WebAppContext; import org.junit.After; import org.junit.Before; import org.junit.Test; public class ApiTest { private final String PAGE_NAME = "Api"; private final String URL = "http://localhost:8080/api/xml/fred"; private final String WEBAPP_FOLDER = "src/main/webapp"; private Server jettyServer; @Before public void setup() throws Exception { String baseFolder = TapestryRunnerConstants.MODULE_BASE_DIR.getAbsolutePath(); String fullFolder = baseFolder + "/" + WEBAPP_FOLDER; // Create Server jettyServer = new Server(8080); ServletHolder defaultServ = new ServletHolder("default", DefaultServlet.class); defaultServ.setInitParameter("resourceBase", fullFolder); defaultServ.setInitParameter("dirAllowed","true"); WebAppContext webapp = new WebAppContext(); webapp.setContextPath("/"); webapp.setWar(fullFolder); webapp.setDescriptor(fullFolder + "/" + "WEB-INF/web-test.xml"); jettyServer.setHandler(webapp); jettyServer.start(); } @After public void tearDown() throws Exception { jettyServer.stop(); } @Test public void confirmPlaceholderIsLoaded() { String appPackage = "nl.uglyduckling.hello"; String appName = "app"; String context = WEBAPP_FOLDER; PageTester tester = new PageTester(appPackage, appName, context); Document document = tester.renderPage(PAGE_NAME); String markup = document.toString(); String expectedWelcome = "placeholder"; assertTrue(markup.contains(expectedWelcome)); } @Test() public void confirmXmlResponse() throws Exception { HttpURLConnection httpConnection = (HttpURLConnection)new URL(URL).openConnection(); InputStream in = httpConnection.getInputStream(); String encoding = httpConnection.getContentEncoding(); encoding = encoding == null ? "UTF-8" : encoding; String body = IOUtils.toString(in, encoding); String expected = "<timestamp>Wed Apr 04 10:45:09 CET 1900</timestamp>"; assertEquals("Response Code", httpConnection.getResponseCode(), HttpStatus.OK_200); assertTrue("Expected " +expected + ", but was " + body, body.contains(expected)); } @Test() public void confirmXmlResponseContainsFred() throws Exception { HttpURLConnection httpConnection = (HttpURLConnection)new URL(URL).openConnection(); InputStream in = httpConnection.getInputStream(); String encoding = httpConnection.getContentEncoding(); encoding = encoding == null ? "UTF-8" : encoding; String body = IOUtils.toString(in, encoding); String expected = "fred"; assertEquals("Response Code", httpConnection.getResponseCode(), HttpStatus.OK_200); assertTrue("Expected " +expected + ", but was " + body, body.contains(expected)); } @Test() public void validateJettySetup() throws Exception { HttpURLConnection httpConnection = (HttpURLConnection)new URL(URL).openConnection(); assertEquals("Response Code", httpConnection.getResponseCode(), HttpStatus.OK_200); } }
TestModule.java
package nl.uglyduckling.hello.services; import org.apache.tapestry5.ioc.ServiceBinder; /** * This module is automatically included as part of the Tapestry IoC Registry, it's a good place to * configure and extend Tapestry, or to place your own service definitions. */ public class TestModule extends AppModule { public static void bind(ServiceBinder binder) { binder.bind(DateService.class, DateServiceMock.class); } }
AppModule.java
(Important bi
package nl.uglyduckling.hello.services; import java.io.IOException; import org.apache.tapestry5.*; import org.apache.tapestry5.ioc.MappedConfiguration; import org.apache.tapestry5.ioc.OrderedConfiguration; import org.apache.tapestry5.ioc.ServiceBinder; import org.apache.tapestry5.ioc.annotations.Contribute; import org.apache.tapestry5.ioc.annotations.Local; import org.apache.tapestry5.ioc.services.ApplicationDefaults; import org.apache.tapestry5.ioc.services.SymbolProvider; import org.apache.tapestry5.services.*; import org.slf4j.Logger; /** * This module is automatically included as part of the Tapestry IoC Registry, it's a good place to * configure and extend Tapestry, or to place your own service definitions. */ public class AppModule { public static void bind(ServiceBinder binder) { // binder.bind(MyServiceInterface.class, MyServiceImpl.class); // Make bind() calls on the binder object to define most IoC services. // Use service builder methods (example below) when the implementation // is provided inline, or requires more initialization than simply // invoking the constructor. binder.bind(DateService.class, DateServiceImpl.class); } public static void contributeFactoryDefaults( MappedConfiguration<String, Object> configuration) { // The values defined here (as factory default overrides) are themselves // overridden with application defaults by DevelopmentModule and QaModule. // The application version is primarily useful as it appears in // any exception reports (HTML or textual). configuration.override(SymbolConstants.APPLICATION_VERSION, "0.0.1-SNAPSHOT"); // This is something that should be removed when going to production, but is useful // in the early stages of development. configuration.override(SymbolConstants.PRODUCTION_MODE, false); } public static void contributeApplicationDefaults( MappedConfiguration<String, Object> configuration) { // Contributions to ApplicationDefaults will override any contributions to // FactoryDefaults (with the same key). Here we're restricting the supported // locales to just "en" (English). As you add localised message catalogs and other assets, // you can extend this list of locales (it's a comma separated series of locale names; // the first locale name is the default when there's no reasonable match). configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en"); // You should change the passphrase immediately; the HMAC passphrase is used to secure // the hidden field data stored in forms to encrypt and digitally sign client-side data. configuration.add(SymbolConstants.HMAC_PASSPHRASE, "change this immediately"); } /** * Use annotation or method naming convention: <code>contributeApplicationDefaults</code> */ @Contribute(SymbolProvider.class) @ApplicationDefaults public static void setupEnvironment(MappedConfiguration<String, Object> configuration) { // Support for jQuery is new in Tapestry 5.4 and will become the only supported // option in 5.5. configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "jquery"); configuration.add(SymbolConstants.BOOTSTRAP_ROOT, "context:mybootstrap"); configuration.add(SymbolConstants.MINIFICATION_ENABLED, true); } /** * This is a service definition, the service will be named "TimingFilter". The interface, * RequestFilter, is used within the RequestHandler service pipeline, which is built from the * RequestHandler service configuration. Tapestry IoC is responsible for passing in an * appropriate Logger instance. Requests for static resources are handled at a higher level, so * this filter will only be invoked for Tapestry related requests. * * * Service builder methods are useful when the implementation is inline as an inner class * (as here) or require some other kind of special initialization. In most cases, * use the static bind() method instead. * * * If this method was named "build", then the service id would be taken from the * service interface and would be "RequestFilter". Since Tapestry already defines * a service named "RequestFilter" we use an explicit service id that we can reference * inside the contribution method. */ public RequestFilter buildTimingFilter(final Logger log) { return new RequestFilter() { public boolean service(Request request, Response response, RequestHandler handler) throws IOException { long startTime = System.currentTimeMillis(); try { // The responsibility of a filter is to invoke the corresponding method // in the handler. When you chain multiple filters together, each filter // received a handler that is a bridge to the next filter. return handler.service(request, response); } finally { long elapsed = System.currentTimeMillis() - startTime; log.info("Request time: {} ms", elapsed); } } }; } /** * This is a contribution to the RequestHandler service configuration. This is how we extend * Tapestry using the timing filter. A common use for this kind of filter is transaction * management or security. The @Local annotation selects the desired service by type, but only * from the same module. Without @Local, there would be an error due to the other service(s) * that implement RequestFilter (defined in other modules). */ @Contribute(RequestHandler.class) public void addTimingFilter(OrderedConfiguration<RequestFilter> configuration, @Local RequestFilter filter) { // Each contribution to an ordered configuration has a name, When necessary, you may // set constraints to precisely control the invocation order of the contributed filter // within the pipeline. configuration.add("Timing", filter); } }
POM.xml
On line 35 the new reference to JUnit can be found. This is required to run the test
confirmXmlResponse in ApiTest that uses the Jetty server directly.
<?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>nl.uglyduckling</groupId> <artifactId>hello</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <name>hello Tapestry 5 Application</name> <dependencies> <!-- To set up an application with a database, change the artifactId below to tapestry-hibernate, and add a dependency on your JDBC driver. You'll also need to add Hibernate configuration files, such as hibernate.cfg.xml. --> <dependency> <groupId>org.apache.tapestry</groupId> <artifactId>tapestry-core</artifactId> <version>${tapestry-release-version}</version> </dependency> <!-- Include the Log4j implementation for the SLF4J logging framework --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j-release-version}</version> </dependency> <dependency> <groupId>org.apache.tapestry</groupId> <artifactId>tapestry-webresources</artifactId> <version>${tapestry-release-version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.7</version> </dependency> <!-- Uncomment this to add support for file uploads: --> <!-- <dependency> <groupId>org.apache.tapestry</groupId> <artifactId>tapestry-upload</artifactId> <version>${tapestry-release-version}</version> </dependency> --> <!-- A dependency on either JUnit or TestNG is required, or the surefire plugin (which runs the tests) will fail, preventing Maven from packaging the WAR. Tapestry includes a large number of testing facilities designed for use with TestNG (http://testng.org/), so it's recommended. --> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>${testng-release-version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.apache.tapestry</groupId> <artifactId>tapestry-test</artifactId> <version>${tapestry-release-version}</version> <scope>test</scope> </dependency> <!-- Provided by the servlet container, but sometimes referenced in the application code. --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>${servlet-api-release-version}</version> <scope>provided</scope> </dependency> <!-- Provide dependency to the Tapestry javadoc taglet which replaces the Maven component report --> <dependency> <groupId>org.apache.tapestry</groupId> <artifactId>tapestry-javadoc</artifactId> <version>${tapestry-release-version}</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>hello</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.6</source> <target>1.6</target> <optimize>true</optimize> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.7.2</version> <dependencies> <dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit47</artifactId> <version>2.18.1</version> </dependency> </dependencies> <configuration> <systemPropertyVariables> <tapestry.execution-mode>Qa</tapestry.execution-mode> </systemPropertyVariables> </configuration> </plugin> <!-- Run the application using "mvn jetty:run" --> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> <version>6.1.16</version> <configuration> <!-- Log to the console. --> <requestLog implementation="org.mortbay.jetty.NCSARequestLog"> <!-- This doesn't do anything for Jetty, but is a workaround for a Maven bug that prevents the requestLog from being set. --> <append>true</append> </requestLog> <systemProperties> <systemProperty> <name>tapestry.execution-mode</name> <value>development</value> </systemProperty> </systemProperties> </configuration> </plugin> </plugins> </build> <reporting /> <repositories> <repository> <id>jboss</id> <url>http://repository.jboss.org/nexus/content/groups/public/</url> </repository> <!-- This repository is only needed when the Tapestry version is a preview release, rather than a final release. --> <repository> <id>apache-staging</id> <url>https://repository.apache.org/content/groups/staging/</url> </repository> </repositories> <properties> <tapestry-release-version>5.4.0</tapestry-release-version> <servlet-api-release-version>2.5</servlet-api-release-version> <testng-release-version>6.8.21</testng-release-version> <slf4j-release-version>1.7.13</slf4j-release-version> </properties> </project>
Web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <display-name>hello Tapestry 5 Application</display-name> <context-param> <!-- The only significant configuration for Tapestry 5, this informs Tapestry of where to look for pages, components and mixins. --> <param-name>tapestry.app-package</param-name> <param-value>nl.uglyduckling.hello</param-value> </context-param> <!-- Specify some additional Modules for two different execution modes: development and qa. Remember that the default execution mode is production --> <context-param> <param-name>tapestry.development-modules</param-name> <param-value> nl.uglyduckling.hello.services.DevelopmentModule </param-value> </context-param> <context-param> <param-name>tapestry.qa-modules</param-name> <param-value> nl.uglyduckling.hello.services.QaModule </param-value> </context-param> <!-- Filter configuration --> <filter> <filter-name>app</filter-name> <filter-class>org.apache.tapestry5.TapestryFilter</filter-class> </filter> <filter-mapping> <filter-name>app</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping> <error-page> <error-code>404</error-code> <location>/error404</location> </error-page> </web-app>
Web-test.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4"> <display-name>hello Tapestry 5 Application</display-name> <context-param> <!-- The only significant configuration for Tapestry 5, this informs Tapestry of where to look for pages, components and mixins. --> <param-name>tapestry.app-package</param-name> <param-value>nl.uglyduckling.hello</param-value> </context-param> <!-- Specify some additional Modules for two different execution modes: development and qa. Remember that the default execution mode is production --> <context-param> <param-name>tapestry.development-modules</param-name> <param-value> nl.uglyduckling.hello.services.DevelopmentModule </param-value> </context-param> <context-param> <param-name>tapestry.qa-modules</param-name> <param-value> nl.uglyduckling.hello.services.QaModule </param-value> </context-param> <!-- Filter configuration --> <filter> <filter-name>test</filter-name> <filter-class>org.apache.tapestry5.TapestryFilter</filter-class> </filter> <filter-mapping> <filter-name>test</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping> <error-page> <error-code>404</error-code> <location>/error404</location> </error-page> </web-app>