From f62ddda733f73ed635974880bb31d12204bba76f Mon Sep 17 00:00:00 2001 From: Erich Eichinger Date: Fri, 20 Jun 2014 13:38:52 +0100 Subject: [PATCH] initial checkin --- .gitignore | 4 + README.md | 91 +++++ acceptance-tests/pom.xml | 49 +++ .../esi/helloworld/HomeControllerAT.java | 35 ++ integration-tests/pom.xml | 30 ++ .../esi/helloworld/HomeControllerIT.java | 19 + pom.xml | 355 ++++++++++++++++++ sonar-jacoco-remotelistener/pom.xml | 28 ++ .../AbstractJacocoControllerHttpProxy.java | 36 ++ .../jacoco/JUnitJacocoRemoteListener.java | 71 ++++ web/pom.xml | 135 +++++++ .../helloworld/HelloWorldConfiguration.java | 54 +++ .../helloworld/HelloWorldWebInitializer.java | 24 ++ .../spike/esi/helloworld/HomeController.java | 23 ++ .../helloworld/JacocoControllerHttpProxy.java | 23 ++ .../ThymeleafMasterLayoutViewResolver.java | 248 ++++++++++++ web/src/main/resources/logback.xml | 11 + .../main/webapp/WEB-INF/templates/edit.html | 1 + .../main/webapp/WEB-INF/templates/home.html | 1 + .../templates/layout/fullPageLayout.html | 13 + web/src/test/java/RunJetty.java | 101 +++++ .../esi/helloworld/HomeControllerTest.java | 18 + 22 files changed, 1370 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 acceptance-tests/pom.xml create mode 100644 acceptance-tests/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerAT.java create mode 100644 integration-tests/pom.xml create mode 100644 integration-tests/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerIT.java create mode 100644 pom.xml create mode 100644 sonar-jacoco-remotelistener/pom.xml create mode 100644 sonar-jacoco-remotelistener/src/main/java/jacoco/AbstractJacocoControllerHttpProxy.java create mode 100644 sonar-jacoco-remotelistener/src/main/java/jacoco/JUnitJacocoRemoteListener.java create mode 100644 web/pom.xml create mode 100644 web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HelloWorldConfiguration.java create mode 100644 web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HelloWorldWebInitializer.java create mode 100644 web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HomeController.java create mode 100644 web/src/main/java/uk/co/postoffice/spike/esi/helloworld/JacocoControllerHttpProxy.java create mode 100644 web/src/main/java/uk/co/postoffice/spike/esi/helloworld/ThymeleafMasterLayoutViewResolver.java create mode 100644 web/src/main/resources/logback.xml create mode 100644 web/src/main/webapp/WEB-INF/templates/edit.html create mode 100644 web/src/main/webapp/WEB-INF/templates/home.html create mode 100644 web/src/main/webapp/WEB-INF/templates/layout/fullPageLayout.html create mode 100644 web/src/test/java/RunJetty.java create mode 100644 web/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..13a9f54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target +.idea +*.iml +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..26f56da --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ + +# Capturing remote/ui acceptance test code coverage results using JaCoCo + +## Prerequisites + +The below describes an example setup of Sonar using MySQL as db backend (see also http://chapter31.com/2013/05/02/installing-sonar-source-on-mac-osx/) and has been +tested with MySQL 5.6, SonarQube 4.3.1 and Maven 3.0.5 + +1. MySQL for SonarQube (e.g.) + + OSX: $>brew install mysql + + create the sonar database using e.g. [create_database.sql](https://github.com/SonarSource/sonar-examples/blob/master/scripts/database/mysql/create_database.sql) + + ``` +# File: create_database.sql +# Create SonarQube database and user. +# Command: mysql -u root -p < create_database.sql +# +CREATE DATABASE sonar CHARACTER SET utf8 COLLATE utf8_general_ci; +CREATE USER 'sonar' IDENTIFIED BY 'sonar'; +GRANT ALL ON sonar.* TO 'sonar'@'%' IDENTIFIED BY 'sonar'; +GRANT ALL ON sonar.* TO 'sonar'@'localhost' IDENTIFIED BY 'sonar'; +FLUSH PRIVILEGES; + ``` + +2. Download and run [SonarQube](http://www.sonarsource.org/downloads/) 4.3 or higher + + OSX: + + a) $>brew install sonar + + b) with (at least) version 4.3.1 there may be a conflict with the currently installed version of ruby on your system (see https://jira.codehaus.org/browse/SONAR-3579) + in order to fix that, open /usr/local/Cellar/sonar/4.3.1/libexec/web/WEB-INF/config/environment.rb and after the line + + ``` +ENV['GEM_HOME'] = $servlet_context.getRealPath('/WEB-INF/gems') + ``` + + add + + ``` +ENV['GEM_HOME'] = $servlet_context.getRealPath('/WEB-INF/gems') +# avoid ruby version conflicts +ENV['GEM_PATH']='' + ``` + + c) ensure you have the Sonar Java plugin installed + + open http://localhost:9000, login as admin/admin and go to Settings->System->Update Center to install the Java plugin + +3. Maven 3.0.5 + + OSX: $>brew install maven30 + +## Running + +from the toplevel folder of the project run + +``` +$>mvn clean install +$>mvn sonar:sonar +``` + +Now open the project dashboard in Sonar. You should see the IT coverage at 44.1%. Drill down and you should see that both methods (home(), edit()) in HomeController are covered by integration tests. + + +## Steps to configure your own project + +1. *absolute* file path for jacoco-it.exec coverage record +2. add javaagent to target jvm with output=none +3. add jacocoremotelistener controller to your application + +## How does it work + +## TODO + +*) read target url in junit listener from config property +*) migrate from spring mvc controller to servlet/filter for better reuse +*) maybe no need to dump() after every request? IT don't support per-test coverage anyway + +## Useful Links + +http://docs.codehaus.org/display/SONAR/Analysis+Parameters +http://www.eclemma.org/jacoco/trunk/doc/examples/java/ExecutionDataClient.java +http://www.eclemma.org/jacoco/trunk/doc/examples/java/ReportGenerator.java +https://groups.google.com/forum/#!topic/jacoco/04MOA-C22SM +http://docs.codehaus.org/display/SONAR/Code+Coverage+by+Unit+Tests+for+Java+Project +http://dougonjava.blogspot.co.il/2013/07/integration-testing-using-maven-tomcat.html +https://github.com/SonarSource/sonar-examples + diff --git a/acceptance-tests/pom.xml b/acceptance-tests/pom.xml new file mode 100644 index 0000000..0b49f2c --- /dev/null +++ b/acceptance-tests/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + sample + reactor + 0.0.1-SNAPSHOT + + + acceptance-tests + jar + + + + junit + junit + test + + + ${project.groupId} + web + ${project.version} + classes + test + + + + + + + org.eclipse.jetty + jetty-maven-plugin + + + stop-jetty + post-integration-test + + stop + + + + + + + + + diff --git a/acceptance-tests/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerAT.java b/acceptance-tests/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerAT.java new file mode 100644 index 0000000..33f9609 --- /dev/null +++ b/acceptance-tests/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerAT.java @@ -0,0 +1,35 @@ +package uk.co.postoffice.spike.esi.helloworld; + +import org.junit.Before; +import org.junit.Test; +import sun.misc.IOUtils; + +import java.io.*; +import java.net.URL; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +/** + * @author Erich Eichinger + * @since 05/06/2014 + */ +public class HomeControllerAT { + + @Test + public void home_should_render_helloFromHOME() throws Exception { + final String strUrl = "http://localhost:8080/home"; + final byte[] bytes = fetchBytes(strUrl); + + String content = new String(bytes); + + assertThat(content, containsString("Hello from HOME")); + } + + private byte[] fetchBytes(String strUrl) throws IOException { + URL url = new URL(strUrl); + return IOUtils.readFully((InputStream) url.getContent(), -1, true); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml new file mode 100644 index 0000000..14f4112 --- /dev/null +++ b/integration-tests/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + reactor + sample + 0.0.1-SNAPSHOT + + + integration-tests + + + + junit + junit + test + + + ${project.groupId} + web + ${project.version} + classes + test + + + + diff --git a/integration-tests/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerIT.java b/integration-tests/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerIT.java new file mode 100644 index 0000000..1e088ac --- /dev/null +++ b/integration-tests/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerIT.java @@ -0,0 +1,19 @@ +package uk.co.postoffice.spike.esi.helloworld; + +import org.junit.Ignore; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; + +/** + * @author Erich Eichinger + * @since 03/06/2014 + */ +public class HomeControllerIT { + + @Test + public void edit_should_return_editview() throws Exception { + assertThat(new HomeController().edit(), equalTo("edit")); + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3ca1f3b --- /dev/null +++ b/pom.xml @@ -0,0 +1,355 @@ + + + 4.0.0 + + sample + reactor + 0.0.1-SNAPSHOT + pom + + + 3.0.5 + + + + + ${user.dir} + + + 1.7 + utf-8 + ${file.encoding} + ${file.encoding} + + + 0.7.1.201405082137 + 9.2.1.v20140609 + 3.2.8.RELEASE + 1.7.2 + 1.1.2 + 3.1.3.RELEASE + 2.1.3.RELEASE + 2.1.1.RELEASE + + + reuseReports + http://localhost:9000 + jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8 + sonar + sonar + + java + 1.7 + + jacoco + + target/surefire-reports + target/failsafe-reports + + target/jacoco-ut.exec + ${project.root.basedir}/target/jacoco-it.exec + + + + + sonar-jacoco-remotelistener + web + integration-tests + acceptance-tests + + + + + org.codehaus.sonar-plugins.java + sonar-jacoco-listeners + 2.2.1 + test + + + org.jacoco + org.jacoco.agent + ${jacoco.version} + runtime + test + + + + + + ${project.artifactId} + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + jacoco-initialize + initialize + + prepare-agent + + + + + true + jacoco.agent.argLine + + + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar + + + + + + + + + org.codehaus.mojo + sonar-maven-plugin + 2.3 + + + mysql + mysql-connector-java + 5.1.29 + + + + + org.apache.maven.plugins + maven-clean-plugin + 2.5 + + + org.apache.maven.plugins + maven-resources-plugin + 2.6 + + ${file.encoding} + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + org.apache.maven.plugins + maven-war-plugin + 2.4 + + false + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + ${file.encoding} + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.16 + + false + false + ${project.basedir}/${sonar.junit.reportsPath} + ${jacoco.agent.argLine},destfile=${sonar.jacoco.reportPath} + + + listener + org.sonar.java.jacoco.JUnitListener + + + + + + org.apache.maven.surefire + surefire-junit47 + 2.16 + + + + + integration-test + integration-test + + test + + + ${project.basedir}/${sonar.junit.itReportsPath} + + ${jacoco.agent.argLine},destfile=${sonar.jacoco.itReportPath} + + **/*IT.class + + + + + acceptance-test + integration-test + + test + + + ${project.basedir}/${sonar.junit.itReportsPath} + + + **/*AT.class + + + + listener + jacoco.JUnitJacocoRemoteListener + + + + ${sonar.jacoco.itReportPath} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.7 + + + org.eclipse.jetty + jetty-maven-plugin + ${jetty.version} + + / + stopJetty + 9999 + / + 10 + + + + + + + + + + org.springframework + spring-framework-bom + ${spring.version} + pom + import + + + org.springframework + spring-core + ${spring.version} + + + commons-logging + commons-logging + + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + commons-fileupload + commons-fileupload + 1.2.2 + runtime + + + com.fasterxml.jackson.core + jackson-databind + 2.1.3 + + + com.jayway.jsonpath + json-path + 0.8.1 + + + xmlunit + xmlunit + 1.5 + + + org.hamcrest + hamcrest-all + 1.3 + + + org.hibernate + hibernate-validator + 4.2.0.Final + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + runtime + + + ch.qos.logback + logback-classic + ${logback.version} + runtime + + + org.projectlombok + lombok + 1.12.6 + provided + + + junit + junit + 4.11 + test + + + org.eclipse.jetty + jetty-runner + ${jetty.version} + test + + + com.ning + async-http-client + 1.7.19 + test + + + + + diff --git a/sonar-jacoco-remotelistener/pom.xml b/sonar-jacoco-remotelistener/pom.xml new file mode 100644 index 0000000..0ab9c1e --- /dev/null +++ b/sonar-jacoco-remotelistener/pom.xml @@ -0,0 +1,28 @@ + + + + reactor + sample + 0.0.1-SNAPSHOT + + 4.0.0 + + sonar-jacoco-remotelistener + + + + junit + junit + provided + + + org.jacoco + org.jacoco.agent + ${jacoco.version} + runtime + provided + + + \ No newline at end of file diff --git a/sonar-jacoco-remotelistener/src/main/java/jacoco/AbstractJacocoControllerHttpProxy.java b/sonar-jacoco-remotelistener/src/main/java/jacoco/AbstractJacocoControllerHttpProxy.java new file mode 100644 index 0000000..fbd2b42 --- /dev/null +++ b/sonar-jacoco-remotelistener/src/main/java/jacoco/AbstractJacocoControllerHttpProxy.java @@ -0,0 +1,36 @@ +package jacoco; + +import org.jacoco.agent.rt.IAgent; +import org.jacoco.agent.rt.RT; + +/** + * @author Erich Eichinger + * @since 06/06/2014 + */ +public abstract class AbstractJacocoControllerHttpProxy { + +// IAgent agent; + +// public JacocoControllerHttpProxy() { +// agent = getAgent(); +// } + + private IAgent getAgent() { + try { + return RT.getAgent(); + } catch (Exception e) { + System.out.println(e); + } + return null; + } + + public byte[] getExecutionData(String sessionId, boolean reset) { + IAgent agent = getAgent(); + if (agent != null) { + System.out.println("JacocoAgent setting sessionid '" + sessionId + "'"); + agent.setSessionId(sessionId); + return agent.getExecutionData(true); + } + return new byte[0]; + } +} diff --git a/sonar-jacoco-remotelistener/src/main/java/jacoco/JUnitJacocoRemoteListener.java b/sonar-jacoco-remotelistener/src/main/java/jacoco/JUnitJacocoRemoteListener.java new file mode 100644 index 0000000..37e8ca1 --- /dev/null +++ b/sonar-jacoco-remotelistener/src/main/java/jacoco/JUnitJacocoRemoteListener.java @@ -0,0 +1,71 @@ +package jacoco; + +import org.junit.runner.Description; +import org.junit.runner.notification.RunListener; +import sun.misc.IOUtils; + +import java.io.*; +import java.net.URL; +import java.net.URLEncoder; + +/** + * @author Erich Eichinger + * @since 06/06/2014 + */ +public class JUnitJacocoRemoteListener extends RunListener { + + @Override + public void testStarted(Description description) throws Exception { + onTestStart(getName(description)); + } + + @Override + public void testFinished(Description description) throws Exception { + onTestFinish(getName(description)); + } + + public void onTestStart(String name) throws Exception { + System.out.println("JacocoController: onTestStart(" + name + ")"); + dumpFromRemote(""); + } + + public void onTestFinish(String name) throws Exception { + System.out.println("JacocoController: onTestFinish(" + name + ")"); + dumpFromRemote(name); + } + + private void dumpFromRemote(String name) throws IOException { + byte[] data = fetchBytes("http://localhost:8080/jacoco/dump?sessionid="+ URLEncoder.encode(name, "utf-8")+"&reset=true"); + String destfile = System.getProperty("destfile"); + if (!destfile.endsWith(".exec")) { + throw new IllegalArgumentException("missing destfile config property"); + } + final File file = new File(destfile); // "../target/jacoco-it.exec" + System.out.println("JacocoController: dump(" + name + ") to " + file.getAbsolutePath()); + save(file, true, data); + } + + private static String getName(Description description) { + return description.getClassName() + " " + description.getMethodName(); + } + + private byte[] fetchBytes(String strUrl) throws IOException { + URL url = new URL(strUrl); + return IOUtils.readFully((InputStream) url.getContent(), -1, true); + } + + public void save(final File file, final boolean append, byte[] data) throws IOException { + final File folder = file.getParentFile(); + if (folder != null) { + folder.mkdirs(); + } + final FileOutputStream fileStream = new FileOutputStream(file, append); + // Avoid concurrent writes from other processes: + fileStream.getChannel().lock(); + try (OutputStream bufferedStream = new BufferedOutputStream(fileStream)) { + bufferedStream.write(data); + bufferedStream.flush(); + } + } + +} diff --git a/web/pom.xml b/web/pom.xml new file mode 100644 index 0000000..81a618e --- /dev/null +++ b/web/pom.xml @@ -0,0 +1,135 @@ + + 4.0.0 + + + sample + reactor + 0.0.1-SNAPSHOT + + + web + war + + + 3.0.5 + + + + + ${project.groupId} + sonar-jacoco-remotelistener + ${project.version} + + + + + org.projectlombok + lombok + provided + + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + + + org.slf4j + slf4j-api + + + org.slf4j + jcl-over-slf4j + runtime + + + ch.qos.logback + logback-classic + runtime + + + org.springframework + spring-context + + + org.springframework + spring-web + + + org.springframework + spring-webmvc + + + org.thymeleaf + thymeleaf + ${thymeleaf.version} + compile + + + org.thymeleaf + thymeleaf-spring3 + ${thymeleaf.version} + compile + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity3 + ${thymeleafspringsecurity3.version} + compile + + + org.springframework.security + spring-security-core + ${springsecurity.version} + runtime + + + org.springframework.security + spring-security-web + ${springsecurity.version} + runtime + + + + + junit + junit + test + + + org.eclipse.jetty + jetty-runner + ${jetty.version} + test + + + + + + + org.eclipse.jetty + jetty-maven-plugin + + ${jacoco.agent.argLine},output=none + false + + + + start-jetty + pre-integration-test + + stop + run-forked + + + + + + + + + diff --git a/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HelloWorldConfiguration.java b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HelloWorldConfiguration.java new file mode 100644 index 0000000..d3e80c3 --- /dev/null +++ b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HelloWorldConfiguration.java @@ -0,0 +1,54 @@ +package uk.co.postoffice.spike.esi.helloworld; + +import uk.co.postoffice.spike.esi.helloworld.JacocoControllerHttpProxy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; +import org.thymeleaf.extras.springsecurity3.dialect.SpringSecurityDialect; +import org.thymeleaf.spring3.SpringTemplateEngine; +import org.thymeleaf.spring3.view.ThymeleafViewResolver; +import org.thymeleaf.templateresolver.TemplateResolver; + +@Configuration +@ComponentScan +public class HelloWorldConfiguration extends WebMvcConfigurationSupport { + + @Override + protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } + +// @Bean +// public JacocoControllerHttpProxy jacocoControllerHttpProxy() { +// return new JacocoControllerHttpProxy(); +// } + + @Bean + public ThymeleafViewResolver thymeleafViewResolver() { + final TemplateResolver servletContextTemplateResolver = new org.thymeleaf.templateresolver.ServletContextTemplateResolver() {{ + setOrder(20); + setPrefix("/WEB-INF/templates/"); + setSuffix(".html"); + setTemplateMode("HTML5"); + setCharacterEncoding("UTF-8"); + setCacheable(false); + }}; + + final SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine() {{ + addTemplateResolver(servletContextTemplateResolver); + addDialect(new SpringSecurityDialect()); + }}; + + final ThymeleafMasterLayoutViewResolver viewResolver = new ThymeleafMasterLayoutViewResolver() {{ + setCharacterEncoding("UTF-8"); + setTemplateEngine(springTemplateEngine); + setCache(false); +// setFullPageLayout("layout/fullPageLayout"); + }}; + + return viewResolver; + } +} diff --git a/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HelloWorldWebInitializer.java b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HelloWorldWebInitializer.java new file mode 100644 index 0000000..71b206c --- /dev/null +++ b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HelloWorldWebInitializer.java @@ -0,0 +1,24 @@ +package uk.co.postoffice.spike.esi.helloworld; + +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +/** + * @author Erich Eichinger + * @since 26/08/2013 + */ +public class HelloWorldWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + @Override + protected Class[] getRootConfigClasses() { + return new Class[] { }; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[] { HelloWorldConfiguration.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } +} diff --git a/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HomeController.java b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HomeController.java new file mode 100644 index 0000000..1c86e02 --- /dev/null +++ b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/HomeController.java @@ -0,0 +1,23 @@ +package uk.co.postoffice.spike.esi.helloworld; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Erich Eichinger + * @since 26/08/2013 + */ +@Controller +public class HomeController { + + @RequestMapping("/home") + public String home() { + return "home"; + } + + @RequestMapping("/edit") + public String edit() { + return "edit"; + } +} diff --git a/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/JacocoControllerHttpProxy.java b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/JacocoControllerHttpProxy.java new file mode 100644 index 0000000..0b8d703 --- /dev/null +++ b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/JacocoControllerHttpProxy.java @@ -0,0 +1,23 @@ +package uk.co.postoffice.spike.esi.helloworld; + +import jacoco.AbstractJacocoControllerHttpProxy; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * @author Erich Eichinger + * @since 06/06/2014 + */ +@Controller +@RequestMapping("/jacoco") +public class JacocoControllerHttpProxy extends AbstractJacocoControllerHttpProxy { + + @Override + @RequestMapping("/dump") + @ResponseBody + public byte[] getExecutionData(@RequestParam("sessionid") String sessionId, @RequestParam("reset") boolean reset) { + return super.getExecutionData(sessionId, reset); + } +} diff --git a/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/ThymeleafMasterLayoutViewResolver.java b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/ThymeleafMasterLayoutViewResolver.java new file mode 100644 index 0000000..8843b4e --- /dev/null +++ b/web/src/main/java/uk/co/postoffice/spike/esi/helloworld/ThymeleafMasterLayoutViewResolver.java @@ -0,0 +1,248 @@ +/* + * Copyright 2008-2013 the original author or authors. + * + * Licensed 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 uk.co.postoffice.spike.esi.helloworld; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.PatternMatchUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.view.RedirectView; +import org.thymeleaf.spring3.view.AbstractThymeleafView; +import org.thymeleaf.spring3.view.ThymeleafViewResolver; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletRequest; + +/** + * This class extends the default ThymeleafViewResolver to facilitate rendering + * template fragments (such as those used by AJAX modals or iFrames) within a + * full page container should the request for that template have occurred in a + * stand-alone context. + * + * @author Andre Azzolini (apazzolini) + */ +public class ThymeleafMasterLayoutViewResolver extends ThymeleafViewResolver { + private final Logger LOG = LoggerFactory.getLogger(this.getClass()); + + /** + *

+ * Prefix to be used in view names (returned by controllers) for specifying an + * HTTP redirect with AJAX support. That is, if you want a redirect to be followed + * by the browser as the result of an AJAX call or within an iFrame at the parent + * window, you can utilize this prefix. Note that this requires a JavaScript component, + * which is provided as part of BLC.js + * + * If the request was not performed in an AJAX / iFrame context, this method will + * delegate to the normal "redirect:" prefix. + *

+ *

+ * Value: ajaxredirect: + *

+ */ + public static final String AJAX_REDIRECT_URL_PREFIX = "ajaxredirect:"; + + protected Map layoutMap = new HashMap(); + protected String fullPageLayout = "layout/fullPageLayout"; + protected String iframeLayout = "layout/iframeLayout"; + + /* + * This method is a copy of the same method in ThymeleafViewResolver, but since it is marked private, + * we are unable to call it from the BroadleafThymeleafViewResolver + */ + protected boolean canHandle(final String viewName) { + final String[] viewNamesToBeProcessed = getViewNames(); + final String[] viewNamesNotToBeProcessed = getExcludedViewNames(); + return ((viewNamesToBeProcessed == null || PatternMatchUtils.simpleMatch(viewNamesToBeProcessed, viewName)) && + (viewNamesNotToBeProcessed == null || !PatternMatchUtils.simpleMatch(viewNamesNotToBeProcessed, viewName))); + } + + /** + * Determines which internal method to call for creating the appropriate view. If no + * Broadleaf specific methods match the viewName, it delegates to the parent + * ThymeleafViewResolver createView method + */ + @Override + protected View createView(final String viewName, final Locale locale) throws Exception { + if (!canHandle(viewName)) { + LOG.trace("[THYMELEAF] View {" + viewName + "} cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain"); + return null; + } + + if (viewName.startsWith(AJAX_REDIRECT_URL_PREFIX)) { + LOG.trace("[THYMELEAF] View {" + viewName + "} is an ajax redirect, and will be handled directly by BroadleafThymeleafViewResolver"); + String redirectUrl = viewName.substring(AJAX_REDIRECT_URL_PREFIX.length()); + return loadAjaxRedirectView(redirectUrl, locale); + } + + return super.createView(viewName, locale); + } + + /** + * Performs a Broadleaf AJAX redirect. This is used in conjunction with BLC.js to support + * doing a browser page change as as result of an AJAX call. + * + * @param redirectUrl + * @param locale + * @return + * @throws Exception + */ + protected View loadAjaxRedirectView(String redirectUrl, final Locale locale) throws Exception { + if (isAjaxRequest()) { + // TODO + /* + // utility/blcRedirect.html + + */ +// String viewName = "utility/blcRedirect"; +// addStaticVariable("blc_redirect", redirectUrl); +// return super.loadView(viewName, locale); + return new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); + } else { + return new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); + } + } + + @Override + protected View loadView(final String originalViewName, final Locale locale) throws Exception { + String viewName = originalViewName; + + if (!isAjaxRequest()) { + String longestPrefix = ""; + + for (Entry entry : layoutMap.entrySet()) { + String viewPrefix = entry.getKey(); + String viewLayout = entry.getValue(); + + if (viewPrefix.length() > longestPrefix.length()) { + if (originalViewName.startsWith(viewPrefix)) { + longestPrefix = viewPrefix; + + if (!"NONE".equals(viewLayout)) { + viewName = viewLayout; + } + } + } + } + + if (longestPrefix.equals("")) { + viewName = getFullPageLayout(); + } + } + + AbstractThymeleafView view = (AbstractThymeleafView) super.loadView(viewName, locale); + + if (!isAjaxRequest()) { + view.addStaticVariable("templateName", originalViewName); + } + + return view; + } + + @Override + protected Object getCacheKey(String viewName, Locale locale) { + return viewName + "_" + locale + "_" + isAjaxRequest(); + } + + protected boolean isAjaxRequest() { + HttpServletRequest request = null; + try { + request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + } catch (ClassCastException e) { + // In portlet environments, we won't be able to cast to a ServletRequestAttributes. We don't want to + // blow up in these scenarios. + LOG.warn("Unable to cast to ServletRequestAttributes and the request in BroadleafRequestContext " + + "was not set. This may introduce incorrect AJAX behavior."); + } + + // If we still don't have a request object, we'll default to non-ajax + if (request == null) { + return false; + } + + return isAjaxRequest(request); + } + + private boolean isAjaxRequest(HttpServletRequest request) { + String requestedWithHeader = request.getHeader("X-Requested-With"); + boolean result = "XMLHttpRequest".equals(requestedWithHeader); + + if (LOG.isTraceEnabled()) { + LOG.trace("Request URL: [" + request.getServletPath() + "]" + " - " + "X-Requested-With: [" + requestedWithHeader + "]" + " - " + "Returning: [" + result + "]"); + } + + return result; + } + + /** + * Gets the map of prefix : layout for use in determining which layout + * to dispatch the request to in non-AJAX calls + * + * @return the layout map + */ + public Map getLayoutMap() { + return layoutMap; + } + + /** + * @see #getLayoutMap() + * @param layoutMap + */ + public void setLayoutMap(Map layoutMap) { + this.layoutMap = layoutMap; + } + + /** + * The default layout to use if there is no specifc entry in the layout map + * + * @return the full page layout + */ + public String getFullPageLayout() { + return fullPageLayout; + } + + /** + * @see #getFullPageLayout() + * @param fullPageLayout + */ + public void setFullPageLayout(String fullPageLayout) { + this.fullPageLayout = fullPageLayout; + } + + /** + * The layout to use for iframe requests + * + * @return the iframe layout + */ + public String getIframeLayout() { + return iframeLayout; + } + + /** + * @see #getIframeLayout() + * @param iframeLayout + */ + public void setIframeLayout(String iframeLayout) { + this.iframeLayout = iframeLayout; + } + +} diff --git a/web/src/main/resources/logback.xml b/web/src/main/resources/logback.xml new file mode 100644 index 0000000..20da1d0 --- /dev/null +++ b/web/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + [%logger{36}] - %msg%n + + + + + + + diff --git a/web/src/main/webapp/WEB-INF/templates/edit.html b/web/src/main/webapp/WEB-INF/templates/edit.html new file mode 100644 index 0000000..bd8fca1 --- /dev/null +++ b/web/src/main/webapp/WEB-INF/templates/edit.html @@ -0,0 +1 @@ +Hello from EDIT diff --git a/web/src/main/webapp/WEB-INF/templates/home.html b/web/src/main/webapp/WEB-INF/templates/home.html new file mode 100644 index 0000000..ace2f43 --- /dev/null +++ b/web/src/main/webapp/WEB-INF/templates/home.html @@ -0,0 +1 @@ +Hello from HOME diff --git a/web/src/main/webapp/WEB-INF/templates/layout/fullPageLayout.html b/web/src/main/webapp/WEB-INF/templates/layout/fullPageLayout.html new file mode 100644 index 0000000..95a8db1 --- /dev/null +++ b/web/src/main/webapp/WEB-INF/templates/layout/fullPageLayout.html @@ -0,0 +1,13 @@ + + + + Thymeleaf Example - Load master from LOCAL + + + +
Hello from Master
+
+ + diff --git a/web/src/test/java/RunJetty.java b/web/src/test/java/RunJetty.java new file mode 100644 index 0000000..b81af27 --- /dev/null +++ b/web/src/test/java/RunJetty.java @@ -0,0 +1,101 @@ +import org.eclipse.jetty.annotations.AnnotationConfiguration; +import org.eclipse.jetty.plus.webapp.EnvConfiguration; +import org.eclipse.jetty.plus.webapp.PlusConfiguration; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.ConcurrentHashSet; +import org.eclipse.jetty.util.MultiMap; +import org.eclipse.jetty.util.resource.FileResource; +import org.eclipse.jetty.util.resource.JarResource; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.webapp.*; +import org.springframework.core.io.ClassPathResource; +import org.springframework.web.SpringServletContainerInitializer; +import org.springframework.web.WebApplicationInitializer; +import uk.co.postoffice.spike.esi.helloworld.HelloWorldWebInitializer; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; + +/** + * @author Erich Eichinger + * @since 26/08/2013 + */ +public class RunJetty { + + public static void main(String[] args) throws Exception { + String currentDir = System.getProperty("user.dir"); + final Properties properties = System.getProperties(); + System.out.println("Current Dir: " + properties.getProperty("user.dir")); + + Server server = new Server(); + ServerConnector scc = new ServerConnector(server); + scc.setPort(Integer.parseInt(System.getProperty("jetty.port", "8080"))); + server.setConnectors(new Connector[] { scc }); + + server.setAttribute("org.eclipse.jetty.webapp.configuration" ,""); + + WebAppContext context = new WebAppContext(); + context.setServer(server); + context.setContextPath("/"); + context.setResourceBase("src/main/webapp"); + context.setWar("src/main/webapp"); +// context.getMetaData().setWebInfClassesDirs(Arrays.asList(context.newResource("target/classes"))); +/* + URLClassLoader classLoader = (URLClassLoader)WebApplicationInitializer.class.getClassLoader(); + final URL[] urLs = classLoader.getURLs(); + + ArrayList dirResources = new ArrayList<>(); + MetaData metaData = context.getMetaData(); + for(URL url:urLs) { + final Resource classpathResource = context.newResource(url); + if (!classpathResource.isDirectory()) { + metaData.addContainerResource(classpathResource); + } else { + dirResources.add(classpathResource); + } + } + context.getMetaData().setWebInfClassesDirs(dirResources); +*/ + + context.setConfigurations(new Configuration[]{ + new AnnotationConfiguration() { + @Override + public void preConfigure(WebAppContext context) throws Exception { + ClassInheritanceMap map = new ClassInheritanceMap(); + map.put(WebApplicationInitializer.class.getName(), new ConcurrentHashSet() {{ + add(HelloWorldWebInitializer.class.getName()); + }}); + context.setAttribute(CLASS_INHERITANCE_MAP, map); + } + }, + new WebXmlConfiguration(), + new WebInfConfiguration(), + // new TagLibConfiguration(), + new PlusConfiguration(), + new MetaInfConfiguration(), + new FragmentConfiguration(), + new EnvConfiguration() + }); + + server.setHandler(context); + + try { + System.out.println(">>> STARTING EMBEDDED JETTY SERVER, PRESS ANY KEY TO STOP"); + System.out.println(String.format(">>> open http://localhost:%s/", scc.getPort())); + server.start(); + while (System.in.available() == 0) { + Thread.sleep(5000); + } + server.stop(); + server.join(); + } catch (Throwable t) { + t.printStackTrace(); + System.exit(100); + } + + } +} diff --git a/web/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerTest.java b/web/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerTest.java new file mode 100644 index 0000000..d89647c --- /dev/null +++ b/web/src/test/java/uk/co/postoffice/spike/esi/helloworld/HomeControllerTest.java @@ -0,0 +1,18 @@ +package uk.co.postoffice.spike.esi.helloworld; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +/** +* @author Erich Eichinger +* @since 04/06/2014 +*/ +public class HomeControllerTest { + + @Test + public void home_should_return_homeview() throws Exception { + assertThat( new HomeController().home(), equalTo("home") ); + } +}