diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..0cca3f2d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +sudo: false +language: java +jdk: + - oraclejdk8 +env: + - TESTFOLDER=cdi + - TESTFOLDER=security + - TESTFOLDER=servlet + - TESTFOLDER=jpa + - TESTFOLDER=jsf + - TESTFOLDER=jsonb + - TESTFOLDER=validation + +install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V + +script: mvn --batch-mode --fail-at-end --projects $TESTFOLDER -U --also-make-dependents install 2>&1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7796f683 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Except where otherwise indicated, everything in this repository is licensed under the MIT license: + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.asciidoc b/README.asciidoc deleted file mode 100644 index 43d8f64d..00000000 --- a/README.asciidoc +++ /dev/null @@ -1,36 +0,0 @@ -Java EE 8 Samples -================= - -. Download http://download.jboss.org/wildfly/8.2.0.Final/wildfly-8.2.0.Final.zip[WildFly 8.2.0] and unzip -. Download Weld3 Alpha 3 -+ -[source, text] ----- -curl -L -o weld3-alpha3-patch-wildfly8.2.zip http://sourceforge.net/projects/jboss/files/Weld/3.0.0.Alpha3/wildfly-8.2.0.Final-weld-3.0.0.Alpha3-patch.zip/download ----- -+ -. Apply the patch -+ -[source, text] ----- -./wildfly-8.2.0.Final/bin/jboss-cli.sh --command="patch apply ./weld3-alpha3-patch-wildfly8.2.zip" -{ - "outcome" : "success", - "result" : {} -} ----- -+ -. Start WildFly -+ -[source, text] ----- -./wildfly-8.2.0.Final/bin/standalone.sh ----- -+ -. Run tests -+ -[source, text] ----- -mvn test ----- - diff --git a/README.md b/README.md new file mode 100644 index 00000000..b97afe22 --- /dev/null +++ b/README.md @@ -0,0 +1,166 @@ +# Java EE 8 Samples # + +This workspace consists of Java EE 8 Samples and unit tests. They are categorized in different directories, one for each Technology/JSR. + +Some samples/tests have documentation, otherwise read the code. + +## How to run? ## + +Samples are tested on Payara, GlassFish and Tomcat using Arquillian. Arquillian uses container profiles to start up and deploy tests to individual containers. + +Only one container profile can be active at a given time, otherwise there will be dependency conflicts. + +These are the available container profiles: + +* Payara and GlassFish + * ``payara-ci-managed`` + + This profile will install a Payara server and start up the server per sample. + Useful for CI servers. The Payara version that's used can be set via the ``payara.version`` property. + This is the default profile and does not have to be specified explicitly. + + * ``payara-micro-managed`` + + This profile will install Payara Micro and start up the jar per sample. + Useful for CI servers. The Payara Micro version that's used can be set via the ``payara.micro.version`` property. + + * ``payara-remote`` + + This profile requires you to start up a Payara server outside of the build. Each sample will then + reuse this instance to run the tests. + Useful for development to avoid the server start up cost per sample. + + This profile supports for some tests to set the location where Payara is installed via the ``glassfishRemote_gfHome`` + system property. E.g. + + ``-DglassfishRemote_gfHome=/opt/payara173`` + + This is used for sending asadmin commands to create container resources, such as users in an identity store. + + * ``glassfish-embedded`` + + This profile uses the GlassFish embedded server and runs in the same JVM as the TestClass. + Useful for development, but has the downside of server startup per sample. + + * ``glassfish-remote`` + + This profile requires you to start up a GlassFish server outside of the build. Each sample will then + reuse this instance to run the tests. + Useful for development to avoid the server start up cost per sample. + + This profile supports for some tests to set the location where GlassFish is installed via the ``glassfishRemote_gfHome`` + system property. E.g. + + ``-DglassfishRemote_gfHome=/opt/glassfish50`` + + This is used for sending asadmin commands to create container resources, such as users in an identity store. + +* Tomcat + + * ``tomcat-remote`` + + This profile requires you to start up a plain Tomcat 9 server outside of the build. Each sample will then + reuse this instance to run the tests. + + Tomcat supports samples that make use of Servlet, JSP, Expression Language (EL), WebSocket and JASPIC. + + This profile requires you to enable JMX in Tomcat. This can be done by adding the following to ``[tomcat home]/bin/catalina.sh``: + + ``` + JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=8089 -Dcom.sun.management.jmxremote=true " + JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false " + JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false" + JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=localhost " + ``` + + This profile also requires you to set a username (``tomcat``) and password (``manager``) for the management application in + ``tomcat-users.xml``. See the file ``test-utils/src/main/resources/tomcat-users.xml`` in this repository for a full example. + + Be aware that this should *only* be done for a Tomcat instance that's used exclusively for testing, as the above will make + the Tomcat installation **totally insecure!** + + * ``tomcat-ci-managed`` + + This profile will install a Tomcat server and start up the server per sample. + Useful for CI servers. The Tomcat version that's used can be set via the ``tomcat.version`` property. + + + +The containers that download and install a server (the \*-ci-managed profiles) allow you to override the version used, e.g.: + +* `-Dpayara.version=5.0.0.174` + + This will change the version from the current one (e.g 5.0.0.172) to 5.0.0.173 for Payara testing purposes. + +* `-Dglassfish.version=5.0` + + This will change the version from the current one (e.g 5.1.1) to 5.0 for GlassFish testing purposes. + + + +**To run them in the console do**: + +1. In the terminal, ``mvn test -fae`` at the top-level directory to start the tests for the default profile. + +When developing and runing them from IDE, remember to activate the profile before running the test. + +To learn more about Arquillian please refer to the [Arquillian Guides](http://arquillian.org/guides/) + +**To run only a subset of the tests do at the top-level directory**: + +1. Install top level dependencies: ``mvn clean install -pl "test-utils" -am`` +1. cd into desired module, e.g.: ``cd cdi`` +1. Run tests against desired server, e.g.: ``mvn clean test -P glassfish-ci-managed`` + + +## How to contribute ## + +With your help we can improve this set of samples, learn from each other and grow the community full of passionate people who care about the technology, innovation and code quality. Every contribution matters! + +There is just a bunch of things you should keep in mind before sending a pull request, so we can easily get all the new things incorporated into the master branch. + +Standard tests are jUnit based. Test classes naming must comply with surefire naming standards `**/*Test.java`, `**/*Test*.java` or `**/*TestCase.java`. + +For the sake of clarity and consistency, and to minimize the upfront complexity, we prefer standard jUnit tests using Java, with as additional helpers HtmlUnit, Hamcrest and of course Arquillian. Please don't use alternatives for these technologies. If any new dependency has to be introduced into this project it should provide something that's not covered by these existing dependencies. + + +### Some coding principles ### + +* When creating new source file do not put (or copy) any license header, as we use top-level license (MIT) for each and every file in this repository. +* Please follow JBoss Community code formatting profile as defined in the [jboss/ide-config](https://github.com/jboss/ide-config#readme) repository. The details are explained there, as well as configurations for Eclipse, IntelliJ and NetBeans. + + +### Small Git tips ### + +* Make sure your [fork](https://help.github.com/articles/fork-a-repo) is always up-to-date. Simply run ``git pull upstream master`` and you are ready to hack. +* When developing new features please create a feature branch so that we incorporate your changes smoothly. It's also convenient for you as you could work on few things in parallel ;) In order to create a feature branch and switch to it in one swoop you can use ``git checkout -b my_new_cool_feature`` + +That's it! Welcome in the community! + +## CI Job ## + +CI jobs are executed by [Travis](https://travis-ci.org/javaee-samples/javaee8-samples). Note that by the very nature of the samples provided here it's perfectly normal that not all tests pass. This normally would indicate a bug in the server on which the samples are executed. If you think it's really the test that's faulty, then please submit an issue or provide a PR with a fix. + + +## Run each sample in Docker + +* Install Docker client from http://boot2docker.io +* Build the sample that you want to run as + + ``mvn clean package -DskipTests`` + + For example: (note the exact module doens't exist yet, wip here) + + ``mvn -f jaxrs/jaxrs-client/pom.xml clean package -DskipTests`` + +* Change the second line in ``Dockerfile`` to specify the location of the generated WAR file +* Run boot2docker and give the command + + ``docker build -it -p 80:8080 javaee8-sample`` + +* In a different shell, find out the IP address of the running container as: + + ``boot2docker ip`` + +* Access the sample as http://IP_ADDRESS:80/jaxrs-client/webresources/persons. The exact URL would differ based upon the sample. + diff --git a/cdi/bean-discovery-modes/pom.xml b/cdi/bean-discovery-modes/pom.xml new file mode 100644 index 00000000..84454b49 --- /dev/null +++ b/cdi/bean-discovery-modes/pom.xml @@ -0,0 +1,15 @@ + + + 4.0.0 + + + org.javaee8 + cdi + 1.0-SNAPSHOT + + + bean-discovery-modes + war + + Java EE 8 Samples: CDI - Discovery Modes + diff --git a/cdi/bean-discovery-modes/src/main/java/org/javaee8/cdi/bean/discovery/disabled/CdiDisabledBean.java b/cdi/bean-discovery-modes/src/main/java/org/javaee8/cdi/bean/discovery/disabled/CdiDisabledBean.java new file mode 100644 index 00000000..b51bbbd9 --- /dev/null +++ b/cdi/bean-discovery-modes/src/main/java/org/javaee8/cdi/bean/discovery/disabled/CdiDisabledBean.java @@ -0,0 +1,5 @@ +package org.javaee8.cdi.bean.discovery.disabled; + +public class CdiDisabledBean { + +} \ No newline at end of file diff --git a/cdi/bean-discovery-modes/src/main/java/org/javaee8/cdi/bean/discovery/enabled/CdiEnabledBean.java b/cdi/bean-discovery-modes/src/main/java/org/javaee8/cdi/bean/discovery/enabled/CdiEnabledBean.java new file mode 100644 index 00000000..95eec2a5 --- /dev/null +++ b/cdi/bean-discovery-modes/src/main/java/org/javaee8/cdi/bean/discovery/enabled/CdiEnabledBean.java @@ -0,0 +1,8 @@ +package org.javaee8.cdi.bean.discovery.enabled; + +import javax.enterprise.context.RequestScoped; + +@RequestScoped +public class CdiEnabledBean { + +} \ No newline at end of file diff --git a/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/all/CdiEnabledTest.java b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/all/CdiEnabledTest.java new file mode 100644 index 00000000..8aff9201 --- /dev/null +++ b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/all/CdiEnabledTest.java @@ -0,0 +1,44 @@ +package org.javaee8.cdi.bean.discovery.all; + +import static org.junit.Assert.assertFalse; + +import java.util.Set; + +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; + +import org.javaee8.cdi.bean.discovery.disabled.CdiDisabledBean; +import org.javaee8.cdi.bean.discovery.enabled.CdiEnabledBean; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class CdiEnabledTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(WebArchive.class).addClasses(CdiDisabledBean.class, CdiEnabledBean.class) + .addAsWebInfResource("all-beans.xml", "beans.xml"); + } + + @Inject + BeanManager beanManager; + + /** + * Both of the beans should be found. + */ + @Test + public void should_beans_be_injected() throws Exception { + Set> disabledBeans = beanManager.getBeans(CdiDisabledBean.class); + assertFalse("Instances of disabled bean expected.", disabledBeans.isEmpty()); + + Set> enabledBeans = beanManager.getBeans(CdiEnabledBean.class); + assertFalse("Instances of enabled bean expected.", enabledBeans.isEmpty()); + } +} diff --git a/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/annotated/CdiAnnotatedTest.java b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/annotated/CdiAnnotatedTest.java new file mode 100644 index 00000000..8eb084a6 --- /dev/null +++ b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/annotated/CdiAnnotatedTest.java @@ -0,0 +1,45 @@ +package org.javaee8.cdi.bean.discovery.annotated; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Set; + +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; + +import org.javaee8.cdi.bean.discovery.disabled.CdiDisabledBean; +import org.javaee8.cdi.bean.discovery.enabled.CdiEnabledBean; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class CdiAnnotatedTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(WebArchive.class).addClasses(CdiDisabledBean.class, CdiEnabledBean.class) + .addAsWebInfResource("annotated-beans.xml", "beans.xml"); + } + + @Inject + BeanManager beanManager; + + /** + * Only the explicit CDI bean should be found. + */ + @Test + public void should_beans_be_injected() throws Exception { + Set> disabledBeans = beanManager.getBeans(CdiDisabledBean.class); + assertTrue("No instances of disabled bean expected.", disabledBeans.isEmpty()); + + Set> enabledBeans = beanManager.getBeans(CdiEnabledBean.class); + assertFalse("Instances of enabled bean expected.", enabledBeans.isEmpty()); + } +} diff --git a/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/base/CdiDefaultTest.java b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/base/CdiDefaultTest.java new file mode 100644 index 00000000..326b6973 --- /dev/null +++ b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/base/CdiDefaultTest.java @@ -0,0 +1,44 @@ +package org.javaee8.cdi.bean.discovery.base; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Set; + +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; + +import org.javaee8.cdi.bean.discovery.disabled.CdiDisabledBean; +import org.javaee8.cdi.bean.discovery.enabled.CdiEnabledBean; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class CdiDefaultTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(WebArchive.class).addClasses(CdiDisabledBean.class, CdiEnabledBean.class); + } + + @Inject + BeanManager beanManager; + + /** + * Should work the same as annotated. + */ + @Test + public void should_beans_be_injected() throws Exception { + Set> disabledBeans = beanManager.getBeans(CdiDisabledBean.class); + assertTrue("No instances of disabled bean expected.", disabledBeans.isEmpty()); + + Set> enabledBeans = beanManager.getBeans(CdiEnabledBean.class); + assertFalse("Instances of enabled bean expected.", enabledBeans.isEmpty()); + } +} diff --git a/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/empty/CdiEmptyTest.java b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/empty/CdiEmptyTest.java new file mode 100644 index 00000000..66e3c648 --- /dev/null +++ b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/empty/CdiEmptyTest.java @@ -0,0 +1,44 @@ +package org.javaee8.cdi.bean.discovery.empty; + +import static org.junit.Assert.assertFalse; + +import java.util.Set; + +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; + +import org.javaee8.cdi.bean.discovery.disabled.CdiDisabledBean; +import org.javaee8.cdi.bean.discovery.enabled.CdiEnabledBean; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class CdiEmptyTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(WebArchive.class).addClasses(CdiDisabledBean.class, CdiEnabledBean.class) + .addAsWebInfResource("empty-beans.xml", "beans.xml"); + } + + @Inject + BeanManager beanManager; + + /** + * Should work the same as 'all'. + */ + @Test + public void should_beans_be_injected() throws Exception { + Set> disabledBeans = beanManager.getBeans(CdiDisabledBean.class); + assertFalse("Instances of disabled bean expected.", disabledBeans.isEmpty()); + + Set> enabledBeans = beanManager.getBeans(CdiEnabledBean.class); + assertFalse("Instances of enabled bean expected.", enabledBeans.isEmpty()); + } +} diff --git a/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/none/CdiDisabledTest.java b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/none/CdiDisabledTest.java new file mode 100644 index 00000000..430b1144 --- /dev/null +++ b/cdi/bean-discovery-modes/src/test/java/org/javaee8/cdi/bean/discovery/none/CdiDisabledTest.java @@ -0,0 +1,37 @@ +package org.javaee8.cdi.bean.discovery.none; + +import static org.junit.Assert.assertTrue; + +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; + +import org.javaee8.cdi.bean.discovery.disabled.CdiDisabledBean; +import org.javaee8.cdi.bean.discovery.enabled.CdiEnabledBean; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class CdiDisabledTest { + + @Deployment + public static Archive deploy() { + return ShrinkWrap.create(WebArchive.class).addClasses(CdiDisabledBean.class, CdiEnabledBean.class) + .addAsWebInfResource("none-beans.xml", "beans.xml"); + } + + @Inject + BeanManager beanManager; + + /** + * The BeanManager should be null. + */ + @Test + public void should_bean_manager_be_injected() throws Exception { + assertTrue("BeanManager shouldn't be present for a CDI disabled archive", beanManager == null); + } +} diff --git a/cdi/bean-discovery-modes/src/test/resources/all-beans.xml b/cdi/bean-discovery-modes/src/test/resources/all-beans.xml new file mode 100644 index 00000000..b073abf8 --- /dev/null +++ b/cdi/bean-discovery-modes/src/test/resources/all-beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/cdi/bean-discovery-modes/src/test/resources/annotated-beans.xml b/cdi/bean-discovery-modes/src/test/resources/annotated-beans.xml new file mode 100644 index 00000000..3a35235f --- /dev/null +++ b/cdi/bean-discovery-modes/src/test/resources/annotated-beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/cdi/bean-discovery-modes/src/test/resources/empty-beans.xml b/cdi/bean-discovery-modes/src/test/resources/empty-beans.xml new file mode 100644 index 00000000..e69de29b diff --git a/cdi/bean-discovery-modes/src/test/resources/none-beans.xml b/cdi/bean-discovery-modes/src/test/resources/none-beans.xml new file mode 100644 index 00000000..e5b2a5ac --- /dev/null +++ b/cdi/bean-discovery-modes/src/test/resources/none-beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/cdi/dynamic-bean-decorated/pom.xml b/cdi/dynamic-bean-decorated/pom.xml new file mode 100644 index 00000000..33fbd1c4 --- /dev/null +++ b/cdi/dynamic-bean-decorated/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + + org.javaee8 + cdi + 1.0-SNAPSHOT + + + dynamic-bean-decorated + Java EE 8 Samples: CDI - Dynamic Bean Decorated + diff --git a/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/CdiExtension.java b/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/CdiExtension.java new file mode 100644 index 00000000..9a1517e1 --- /dev/null +++ b/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/CdiExtension.java @@ -0,0 +1,24 @@ +package org.javaee8.cdi.dynamic.bean.decorated; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Extension; + +/** + * + * @author Arjan Tijms + * + */ +public class CdiExtension implements Extension { + + public void afterBean(final @Observes AfterBeanDiscovery afterBeanDiscovery) { + afterBeanDiscovery + .addBean() + .scope(ApplicationScoped.class) + .types(MyBean.class) + .id("Created by " + CdiExtension.class) + .createWith(e -> new MyBeanImpl("Hi!")); + } + +} diff --git a/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/MyBean.java b/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/MyBean.java new file mode 100644 index 00000000..51b6a17c --- /dev/null +++ b/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/MyBean.java @@ -0,0 +1,10 @@ +package org.javaee8.cdi.dynamic.bean.decorated; + +/** + * + * @author Arjan Tijms + * + */ +public interface MyBean { + String sayHi(); +} diff --git a/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/MyBeanImpl.java b/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/MyBeanImpl.java new file mode 100644 index 00000000..7fb6d32e --- /dev/null +++ b/cdi/dynamic-bean-decorated/src/main/java/org/javaee8/cdi/dynamic/bean/decorated/MyBeanImpl.java @@ -0,0 +1,29 @@ +package org.javaee8.cdi.dynamic.bean.decorated; + +import javax.enterprise.inject.Typed; + +/** + * + * @author Arjan Tijms + * + */ +// Typed: Extra guard so that MyBeanImpl has no types of itself, but extension archive is not scanned +// so not strictly needed. +@Typed +public class MyBeanImpl implements MyBean { + + private final String greet; + + // Note: There's no default ctor, so CDI cannot directly inject an instance of this + // bean. + + public MyBeanImpl(String greet) { + this.greet = greet; + } + + @Override + public String sayHi() { + return greet; + } + +} diff --git a/cdi/dynamic-bean-decorated/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/cdi/dynamic-bean-decorated/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000..9c4a3530 --- /dev/null +++ b/cdi/dynamic-bean-decorated/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +org.javaee8.cdi.dynamic.bean.decorated.CdiExtension \ No newline at end of file diff --git a/cdi/dynamic-bean-decorated/src/test/java/org/javaee8/cdi/dynamic/bean/decorated/DynamicBeanTest.java b/cdi/dynamic-bean-decorated/src/test/java/org/javaee8/cdi/dynamic/bean/decorated/DynamicBeanTest.java new file mode 100644 index 00000000..8fc3b192 --- /dev/null +++ b/cdi/dynamic-bean-decorated/src/test/java/org/javaee8/cdi/dynamic/bean/decorated/DynamicBeanTest.java @@ -0,0 +1,41 @@ +package org.javaee8.cdi.dynamic.bean.decorated; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertEquals; + +import javax.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * + * @author Arjan Tijms + * + */ +@RunWith(Arquillian.class) +public class DynamicBeanTest { + + @Deployment + public static WebArchive deploy() { + return create(WebArchive.class) + .addAsLibraries( + create(JavaArchive.class) + .addClasses(CdiExtension.class, MyBean.class, MyBeanImpl.class) + .addAsResource("META-INF/services/javax.enterprise.inject.spi.Extension")) + .addClass(MyDecorator.class) + .addAsManifestResource("beans.xml"); + } + + @Inject + private MyBean myBean; + + @Test + public void test() { + assertEquals("Hi! decorated", myBean.sayHi()); + } +} diff --git a/cdi/dynamic-bean-decorated/src/test/java/org/javaee8/cdi/dynamic/bean/decorated/MyDecorator.java b/cdi/dynamic-bean-decorated/src/test/java/org/javaee8/cdi/dynamic/bean/decorated/MyDecorator.java new file mode 100644 index 00000000..3251afcf --- /dev/null +++ b/cdi/dynamic-bean-decorated/src/test/java/org/javaee8/cdi/dynamic/bean/decorated/MyDecorator.java @@ -0,0 +1,21 @@ +package org.javaee8.cdi.dynamic.bean.decorated; + +import javax.annotation.Priority; +import javax.decorator.Decorator; +import javax.decorator.Delegate; +import javax.inject.Inject; + +@Decorator +@Priority(100) +public class MyDecorator implements MyBean { + + @Inject + @Delegate + MyBean mybean; + + @Override + public String sayHi() { + return mybean.sayHi() + " decorated"; + } + +} diff --git a/cdi/dynamic-bean-decorated/src/test/resources/beans.xml b/cdi/dynamic-bean-decorated/src/test/resources/beans.xml new file mode 100644 index 00000000..d306e5f6 --- /dev/null +++ b/cdi/dynamic-bean-decorated/src/test/resources/beans.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/cdi/dynamic-bean/pom.xml b/cdi/dynamic-bean/pom.xml new file mode 100644 index 00000000..76f64bf5 --- /dev/null +++ b/cdi/dynamic-bean/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + + org.javaee8 + cdi + 1.0-SNAPSHOT + + + dynamic-bean + Java EE 8 Samples: CDI - Dynamic Bean + diff --git a/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/CdiExtension.java b/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/CdiExtension.java new file mode 100644 index 00000000..0e45ddc8 --- /dev/null +++ b/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/CdiExtension.java @@ -0,0 +1,24 @@ +package org.javaee8.cdi.dynamic.bean; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Extension; + +/** + * + * @author Arjan Tijms + * + */ +public class CdiExtension implements Extension { + + public void afterBean(final @Observes AfterBeanDiscovery afterBeanDiscovery) { + afterBeanDiscovery + .addBean() + .scope(ApplicationScoped.class) + .types(MyBean.class) + .id("Created by " + CdiExtension.class) + .createWith(e -> new MyBeanImpl("Hi!")); + } + +} diff --git a/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/MyBean.java b/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/MyBean.java new file mode 100644 index 00000000..01f15bc8 --- /dev/null +++ b/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/MyBean.java @@ -0,0 +1,10 @@ +package org.javaee8.cdi.dynamic.bean; + +/** + * + * @author Arjan Tijms + * + */ +public interface MyBean { + String sayHi(); +} diff --git a/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/MyBeanImpl.java b/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/MyBeanImpl.java new file mode 100644 index 00000000..f99c365c --- /dev/null +++ b/cdi/dynamic-bean/src/main/java/org/javaee8/cdi/dynamic/bean/MyBeanImpl.java @@ -0,0 +1,29 @@ +package org.javaee8.cdi.dynamic.bean; + +import javax.enterprise.inject.Typed; + +/** + * + * @author Arjan Tijms + * + */ +// Typed: Extra guard so that MyBeanImpl has no types of itself, but extension archive is not scanned +// so not strictly needed. +@Typed +public class MyBeanImpl implements MyBean { + + private final String greet; + + // Note: There's no default ctor, so CDI cannot directly inject an instance of this + // bean. + + public MyBeanImpl(String greet) { + this.greet = greet; + } + + @Override + public String sayHi() { + return greet; + } + +} diff --git a/cdi/dynamic-bean/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/cdi/dynamic-bean/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000..897cc2d1 --- /dev/null +++ b/cdi/dynamic-bean/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1 @@ +org.javaee8.cdi.dynamic.bean.CdiExtension \ No newline at end of file diff --git a/cdi/dynamic-bean/src/test/java/org/javaee8/cdi/dynamic/bean/DynamicBeanTest.java b/cdi/dynamic-bean/src/test/java/org/javaee8/cdi/dynamic/bean/DynamicBeanTest.java new file mode 100644 index 00000000..dfc66791 --- /dev/null +++ b/cdi/dynamic-bean/src/test/java/org/javaee8/cdi/dynamic/bean/DynamicBeanTest.java @@ -0,0 +1,43 @@ +package org.javaee8.cdi.dynamic.bean; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import javax.inject.Inject; + +import org.hamcrest.Matchers; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * + * @author Arjan Tijms + * + */ +@RunWith(Arquillian.class) +public class DynamicBeanTest { + + @Deployment + public static WebArchive deploy() { + return create(WebArchive.class) + .addAsLibraries( + create(JavaArchive.class) + .addClasses(CdiExtension.class, MyBean.class, MyBeanImpl.class) + .addAsResource("META-INF/services/javax.enterprise.inject.spi.Extension")) + .addAsWebInfResource("beans.xml"); + } + + @Inject + private MyBean myBean; + + @Test + public void test() { + assertThat("myBean", myBean, Matchers.notNullValue()); + assertEquals("Hi!", myBean.sayHi()); + } +} diff --git a/cdi/dynamic-bean/src/test/resources/beans.xml b/cdi/dynamic-bean/src/test/resources/beans.xml new file mode 100644 index 00000000..d306e5f6 --- /dev/null +++ b/cdi/dynamic-bean/src/test/resources/beans.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/cdi/events-async/pom.xml b/cdi/events-async/pom.xml new file mode 100644 index 00000000..46c83de9 --- /dev/null +++ b/cdi/events-async/pom.xml @@ -0,0 +1,13 @@ + +4.0.0 + + + org.javaee8 + cdi + 1.0-SNAPSHOT + + + events-async + Java EE 8 Samples: CDI - Events Async + + diff --git a/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/EventReceiver.java b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/EventReceiver.java new file mode 100644 index 00000000..32109c4f --- /dev/null +++ b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/EventReceiver.java @@ -0,0 +1,10 @@ +package org.javaee8.cdi.events.async; + +/** + * @author Radim Hanus + * @author Arun Gupta + */ +public interface EventReceiver { + + String getGreet(); +} diff --git a/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/EventSender.java b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/EventSender.java new file mode 100644 index 00000000..c6bb6019 --- /dev/null +++ b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/EventSender.java @@ -0,0 +1,14 @@ +package org.javaee8.cdi.events.async; + +import java.util.concurrent.CompletionStage; + +/** + * @author Radim Hanus + * @author Arun Gupta + * @author Arjan Tijms + */ +public interface EventSender { + + void sendSync(String message); + CompletionStage sendAsync(String message); +} diff --git a/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/GreetingReceiver.java b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/GreetingReceiver.java new file mode 100644 index 00000000..80d4d5c3 --- /dev/null +++ b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/GreetingReceiver.java @@ -0,0 +1,61 @@ +package org.javaee8.cdi.events.async; + +import java.io.Serializable; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.enterprise.event.ObservesAsync; +import javax.inject.Inject; + +/** + * @author Radim Hanus + * @author Arun Gupta + * @author Arjan Tijms + */ +@ApplicationScoped +public class GreetingReceiver implements EventReceiver, Serializable { + + private static final long serialVersionUID = 1L; + + private String greet = "Willkommen"; + + @Inject + private Synchronizer synchronizer; + + /** + * Synchronous observer + * + * @param greet + */ + void receiveSync(@Observes String greet) { + System.out.println("receivesync"); + this.greet = greet + "-sync"; + } + + /** + * Asynchronous observer + * + * @param greet + */ + void receiveAsync(@ObservesAsync String greet) { + // Signal that we've started + synchronizer.receiverStarted(); + + // Wait till we're allowed to process this event + synchronizer.waitTillReceiverMayProcess(); + + try { + // Simulate some amount of work + Thread.sleep(2000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + this.greet += greet + "-async"; + } + + @Override + public String getGreet() { + return greet; + } +} diff --git a/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/GreetingSender.java b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/GreetingSender.java new file mode 100644 index 00000000..e0ef1362 --- /dev/null +++ b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/GreetingSender.java @@ -0,0 +1,27 @@ +package org.javaee8.cdi.events.async; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.event.Event; +import javax.inject.Inject; + +/** + * @author Radim Hanus + * @author Arun Gupta + */ +public class GreetingSender implements EventSender { + + @Inject + private Event event; + + @Override + public void sendSync(String message) { + event.fire(message); + } + + @Override + public CompletionStage sendAsync(String message) { + System.out.println("Sending async"); + return event.fireAsync(message); + } +} diff --git a/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/Synchronizer.java b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/Synchronizer.java new file mode 100644 index 00000000..e3c7665a --- /dev/null +++ b/cdi/events-async/src/main/java/org/javaee8/cdi/events/async/Synchronizer.java @@ -0,0 +1,50 @@ +package org.javaee8.cdi.events.async; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.CountDownLatch; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class Synchronizer { + + private static final Logger logger = Logger.getLogger(Synchronizer.class.getName()); + + private final CountDownLatch receiverStarted = new CountDownLatch(1); + private final CountDownLatch receiverMayProcess = new CountDownLatch(1); + + public void waitTillReceiverStarted() { + logger.info("Waiting for receiver to start"); + try { + if (!receiverStarted.await(5,SECONDS)) { + throw new IllegalStateException("Receiver of event not called within 5 seconds"); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void receiverStarted() { + logger.info("Receiver started"); + receiverStarted.countDown(); + } + + public void waitTillReceiverMayProcess() { + logger.info("Waiting for receiver may process"); + try { + if (!receiverMayProcess.await(5,SECONDS)) { + throw new IllegalStateException("Receiver not given permission to process within 5 seconds"); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + public void receiverMayProcess() { + logger.info("Receiver may process"); + receiverMayProcess.countDown(); + } + +} diff --git a/cdi/events-async/src/test/java/org/javaee8/cdi/events/async/AsyncGreetingTest.java b/cdi/events-async/src/test/java/org/javaee8/cdi/events/async/AsyncGreetingTest.java new file mode 100644 index 00000000..4c17b628 --- /dev/null +++ b/cdi/events-async/src/test/java/org/javaee8/cdi/events/async/AsyncGreetingTest.java @@ -0,0 +1,79 @@ +package org.javaee8.cdi.events.async; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.util.concurrent.CompletionStage; + +import javax.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * @author Radim Hanus + * @author Arjan Tijms + */ +@RunWith(Arquillian.class) +public class AsyncGreetingTest { + + @Deployment + public static Archive deploy() { + return create(JavaArchive.class) + .addClasses( + EventReceiver.class, EventSender.class, + GreetingReceiver.class, GreetingSender.class, + Synchronizer.class) + .addAsManifestResource("beans.xml"); + } + + @Inject + private EventSender sender; + + @Inject + private EventReceiver receiver; + + @Inject + private Synchronizer synchronizer; + + @Test + public void test() throws Exception { + assertThat(sender, is(notNullValue())); + assertThat(sender, instanceOf(GreetingSender.class)); + + assertThat(receiver, is(notNullValue())); + assertThat(receiver, instanceOf(GreetingReceiver.class)); + + // Default greet + assertEquals("Willkommen", receiver.getGreet()); + + // Send a new greet synchronously + sender.sendSync("Welcome"); + + // Receiver must not belong to the dependent pseudo-scope since we are checking the result + assertEquals("Welcome-sync", receiver.getGreet()); + + // Send a new greet asynchronously + CompletionStage completionStage = sender.sendAsync("Hi"); + + synchronizer.waitTillReceiverStarted(); + + // The receiver has started, signal that it may now start processing + // This is done to test that the receiver is really on a different thread + synchronizer.receiverMayProcess(); + + completionStage.toCompletableFuture().get(15, SECONDS); + + assertEquals("Welcome-syncHi-async", receiver.getGreet()); + + } +} diff --git a/cdi/events-async/src/test/resources/beans.xml b/cdi/events-async/src/test/resources/beans.xml new file mode 100644 index 00000000..aa8e5774 --- /dev/null +++ b/cdi/events-async/src/test/resources/beans.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/cdi/events-priority/pom.xml b/cdi/events-priority/pom.xml index b2b561f2..a4de7677 100644 --- a/cdi/events-priority/pom.xml +++ b/cdi/events-priority/pom.xml @@ -1,12 +1,12 @@ - - 4.0.0 +4.0.0 + - org.javaee8.cdi - cdi-samples + org.javaee8 + cdi 1.0-SNAPSHOT - ../pom.xml events-priority + Java EE 8 Samples: CDI - Events Priority diff --git a/cdi/events-priority/src/main/java/org/javaee8/cdi/events/priority/GreetingReceiver.java b/cdi/events-priority/src/main/java/org/javaee8/cdi/events/priority/GreetingReceiver.java index 94941cc2..139e3719 100644 --- a/cdi/events-priority/src/main/java/org/javaee8/cdi/events/priority/GreetingReceiver.java +++ b/cdi/events-priority/src/main/java/org/javaee8/cdi/events/priority/GreetingReceiver.java @@ -1,10 +1,12 @@ package org.javaee8.cdi.events.priority; +import static javax.interceptor.Interceptor.Priority.APPLICATION; + +import java.io.Serializable; + +import javax.annotation.Priority; import javax.enterprise.context.SessionScoped; import javax.enterprise.event.Observes; -import java.io.Serializable; -import javax.interceptor.Interceptor; -import org.jboss.weld.experimental.Priority; /** * @author Radim Hanus @@ -13,13 +15,15 @@ @SessionScoped public class GreetingReceiver implements EventReceiver, Serializable { + private static final long serialVersionUID = 1L; + private String greet = "Willkommen"; /** * Lower priority * @param greet */ - void receive(@Observes @Priority(Interceptor.Priority.APPLICATION + 200) String greet) { + void receive(@Observes @Priority(APPLICATION + 200) String greet) { this.greet += greet + "2"; } @@ -27,7 +31,7 @@ void receive(@Observes @Priority(Interceptor.Priority.APPLICATION + 200) String * Higher priority * @param greet */ - void receive2(@Observes @Priority(Interceptor.Priority.APPLICATION) String greet) { + void receive2(@Observes @Priority(APPLICATION) String greet) { this.greet = greet + "1"; } diff --git a/cdi/events-priority/src/test/java/org/javaee8/cdi/events/priority/GreetingTest.java b/cdi/events-priority/src/test/java/org/javaee8/cdi/events/priority/GreetingTest.java index 45780880..a41e460a 100644 --- a/cdi/events-priority/src/test/java/org/javaee8/cdi/events/priority/GreetingTest.java +++ b/cdi/events-priority/src/test/java/org/javaee8/cdi/events/priority/GreetingTest.java @@ -1,25 +1,21 @@ package org.javaee8.cdi.events.priority; -import org.javaee8.cdi.events.priority.EventSender; -import org.javaee8.cdi.events.priority.GreetingSender; -import org.javaee8.cdi.events.priority.GreetingReceiver; -import org.javaee8.cdi.events.priority.EventReceiver; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import javax.inject.Inject; + import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.junit.Arquillian; import org.jboss.shrinkwrap.api.Archive; -import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.Test; import org.junit.runner.RunWith; -import javax.inject.Inject; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThat; - /** * @author Radim Hanus */ @@ -28,7 +24,7 @@ public class GreetingTest { @Deployment public static Archive deploy() { - return ShrinkWrap.create(JavaArchive.class) + return create(JavaArchive.class) .addClasses(EventReceiver.class, EventSender.class, GreetingReceiver.class, GreetingSender.class) .addAsManifestResource("beans.xml"); } @@ -47,11 +43,13 @@ public void test() throws Exception { assertThat(receiver, is(notNullValue())); assertThat(receiver, instanceOf(GreetingReceiver.class)); - // default greet + // Default greet assertEquals("Willkommen", receiver.getGreet()); - // send a new greet + + // Send a new greet sender.send("Welcome"); - // receiver must not belongs to the dependent pseudo-scope since we are checking the result + + // Receiver must not belongs to the dependent pseudo-scope since we are checking the result assertEquals("Welcome1Welcome2", receiver.getGreet()); } } diff --git a/cdi/interception-factory/pom.xml b/cdi/interception-factory/pom.xml new file mode 100644 index 00000000..78e2338d --- /dev/null +++ b/cdi/interception-factory/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + + org.javaee8 + cdi + 1.0-SNAPSHOT + + + interception-factory + Java EE 8 Samples: CDI - Interception factory + diff --git a/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/HelloAdder.java b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/HelloAdder.java new file mode 100644 index 00000000..ba68cc69 --- /dev/null +++ b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/HelloAdder.java @@ -0,0 +1,25 @@ +package org.javaee8.cdi.interception.factory; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.util.AnnotationLiteral; +import javax.interceptor.InterceptorBinding; + +@Inherited +@InterceptorBinding +@Retention(RUNTIME) +@Target({ METHOD, TYPE }) +public @interface HelloAdder { + + @SuppressWarnings("all") + public static final class Literal extends AnnotationLiteral implements HelloAdder { + public static final Literal INSTANCE = new Literal(); + private static final long serialVersionUID = 1L; + } +} diff --git a/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/HelloAdderInterceptor.java b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/HelloAdderInterceptor.java new file mode 100644 index 00000000..845ccd3d --- /dev/null +++ b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/HelloAdderInterceptor.java @@ -0,0 +1,22 @@ +package org.javaee8.cdi.interception.factory; + +import javax.annotation.Priority; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InvocationContext; + +@HelloAdder +@Interceptor +@Priority(500) +public class HelloAdderInterceptor { + + @AroundInvoke + public Object modifyGreet(InvocationContext context) throws Exception { + + if (context.getMethod().getName().equals("setGreet")) { + context.setParameters(new Object[] { "Hello " + context.getParameters()[0] }); + } + + return context.proceed(); + } +} diff --git a/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreeting.java b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreeting.java new file mode 100644 index 00000000..09ee6bb9 --- /dev/null +++ b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreeting.java @@ -0,0 +1,6 @@ +package org.javaee8.cdi.interception.factory; + +public interface MyGreeting { + String getGreet(); + void setGreet(String greet); +} diff --git a/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreetingImpl.java b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreetingImpl.java new file mode 100644 index 00000000..1e2e2b6a --- /dev/null +++ b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreetingImpl.java @@ -0,0 +1,14 @@ +package org.javaee8.cdi.interception.factory; + +public class MyGreetingImpl implements MyGreeting { + + private String greet; + + public String getGreet() { + return greet; + } + + public void setGreet(String greet) { + this.greet = greet; + } +} diff --git a/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreetingProducer.java b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreetingProducer.java new file mode 100644 index 00000000..ee7dfc51 --- /dev/null +++ b/cdi/interception-factory/src/main/java/org/javaee8/cdi/interception/factory/MyGreetingProducer.java @@ -0,0 +1,41 @@ +package org.javaee8.cdi.interception.factory; + +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.InterceptionFactory; + +@Alternative +@Priority(500) +@ApplicationScoped +public class MyGreetingProducer { + + private static final Logger logger = Logger.getLogger(MyGreetingProducer.class.getName()); + + /** + * This producer produces a MyGreeting alternative for MyGreetingImpl. + *

+ * Note that the alternative is set by making the class, not the method, an + * alternative.The alternative is activated via the @Priority annotation. + * + * @param interceptionFactory InterceptionFactory injected by CDI + * @return MyGreeting instance, programmatically proxied + */ + @Produces + public MyGreeting produce(InterceptionFactory interceptionFactory) { + + logger.info("Producing a MyGreeting"); + + // We're telling the InterceptionFactory here to dynamically add the @HelloAdder + // annotation. + interceptionFactory.configure().add(HelloAdder.Literal.INSTANCE); + + // This will create a proxy as configured above around the bare + // instance of MyGreetingImpl that we provide. + return interceptionFactory.createInterceptedInstance(new MyGreetingImpl()); + } + +} diff --git a/cdi/interception-factory/src/test/java/org/javaee8/cdi/interception/factory/InterceptorFactoryTest.java b/cdi/interception-factory/src/test/java/org/javaee8/cdi/interception/factory/InterceptorFactoryTest.java new file mode 100644 index 00000000..99acc63a --- /dev/null +++ b/cdi/interception-factory/src/test/java/org/javaee8/cdi/interception/factory/InterceptorFactoryTest.java @@ -0,0 +1,36 @@ +package org.javaee8.cdi.interception.factory; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertEquals; + +import javax.inject.Inject; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(Arquillian.class) +public class InterceptorFactoryTest { + + @Deployment + public static Archive deploy() { + return create(JavaArchive.class) + .addClasses( + MyGreeting.class, MyGreetingImpl.class, MyGreetingProducer.class, + HelloAdder.class, HelloAdderInterceptor.class) + .addAsManifestResource("beans.xml"); + } + + @Inject + private MyGreeting myGreeting; + + @Test + public void test() { + myGreeting.setGreet("Reza"); + + assertEquals("Hello Reza", myGreeting.getGreet()); + } +} diff --git a/cdi/interception-factory/src/test/resources/beans.xml b/cdi/interception-factory/src/test/resources/beans.xml new file mode 100644 index 00000000..d306e5f6 --- /dev/null +++ b/cdi/interception-factory/src/test/resources/beans.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/cdi/pom.xml b/cdi/pom.xml index fb364e1a..b1fdae02 100644 --- a/cdi/pom.xml +++ b/cdi/pom.xml @@ -1,20 +1,30 @@ - - 4.0.0 + 4.0.0 + org.javaee8 - javaee8-samples + samples-parent 1.0-SNAPSHOT - ../pom.xml - org.javaee8.cdi - cdi-samples - 1.0-SNAPSHOT + cdi pom - Java EE 8 CDI Samples + Java EE 8 Samples: CDI + dynamic-bean + events-async events-priority + interception-factory + qualified-lookup + bean-discovery-modes + + + + org.javaee8 + test-utils + ${project.version} + + diff --git a/cdi/qualified-lookup/pom.xml b/cdi/qualified-lookup/pom.xml new file mode 100644 index 00000000..a1ca6fca --- /dev/null +++ b/cdi/qualified-lookup/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + + org.javaee8 + cdi + 1.0-SNAPSHOT + + + qualified-lookup + Java EE 8 Samples: CDI - Qualified Lookup + diff --git a/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting.java b/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting.java new file mode 100644 index 00000000..3b5cba47 --- /dev/null +++ b/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting.java @@ -0,0 +1,5 @@ +package org.javaee8.cdi.qualified.lookup; + +public interface MyGreeting { + String getGreet(); +} diff --git a/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting1.java b/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting1.java new file mode 100644 index 00000000..c82eca94 --- /dev/null +++ b/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting1.java @@ -0,0 +1,13 @@ +package org.javaee8.cdi.qualified.lookup; + +import javax.inject.Named; + +@Named("northern") +public class MyGreeting1 implements MyGreeting { + + @Override + public String getGreet() { + return "Ay-up!"; + } + +} diff --git a/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting2.java b/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting2.java new file mode 100644 index 00000000..195d8b7b --- /dev/null +++ b/cdi/qualified-lookup/src/main/java/org/javaee8/cdi/qualified/lookup/MyGreeting2.java @@ -0,0 +1,13 @@ +package org.javaee8.cdi.qualified.lookup; + +import javax.inject.Named; + +@Named("informal") +public class MyGreeting2 implements MyGreeting { + + @Override + public String getGreet() { + return "Hiya!"; + } + +} diff --git a/cdi/qualified-lookup/src/test/java/org/javaee8/cdi/qualified/lookup/QualifiedLookupTest.java b/cdi/qualified-lookup/src/test/java/org/javaee8/cdi/qualified/lookup/QualifiedLookupTest.java new file mode 100644 index 00000000..d402f0bd --- /dev/null +++ b/cdi/qualified-lookup/src/test/java/org/javaee8/cdi/qualified/lookup/QualifiedLookupTest.java @@ -0,0 +1,47 @@ +package org.javaee8.cdi.qualified.lookup; + +import static java.lang.System.out; +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertEquals; + +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.literal.NamedLiteral; +import javax.enterprise.inject.spi.CDI; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * @author Arjan Tijms + * + */ +@RunWith(Arquillian.class) +public class QualifiedLookupTest { + + @Deployment + public static Archive deploy() { + return create(JavaArchive.class) + .addClasses( + MyGreeting.class, MyGreeting1.class, MyGreeting2.class) + .addAsManifestResource("beans.xml"); + } + + @Test + public void test() { + + Instance myGreetings = CDI.current().select(MyGreeting.class); + + myGreetings.stream() + .forEach(e -> out.println(e.getGreet())); + + assertEquals("Ay-up!", myGreetings.select(NamedLiteral.of("northern")).get().getGreet()); + + assertEquals("Hiya!", myGreetings.select(NamedLiteral.of("informal")).get().getGreet()); + + assertEquals(false, myGreetings.select(NamedLiteral.of("formal")).isResolvable()); + } +} diff --git a/cdi/qualified-lookup/src/test/resources/beans.xml b/cdi/qualified-lookup/src/test/resources/beans.xml new file mode 100644 index 00000000..d306e5f6 --- /dev/null +++ b/cdi/qualified-lookup/src/test/resources/beans.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/ejb/pom.xml b/ejb/pom.xml new file mode 100644 index 00000000..ce90c570 --- /dev/null +++ b/ejb/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + ejb + pom + + Java EE 8 Sample: ejb + + + remote + + + + + org.javaee8 + test-utils + ${project.version} + test + + + diff --git a/ejb/remote/pom.xml b/ejb/remote/pom.xml new file mode 100644 index 00000000..99efa34a --- /dev/null +++ b/ejb/remote/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + org.javaee8 + ejb + 1.0-SNAPSHOT + + + ejb-remote + pom + + Java EE 8 Sample: ejb - remote + + + vendor + roles-allowed + + + diff --git a/ejb/remote/roles-allowed/pom.xml b/ejb/remote/roles-allowed/pom.xml new file mode 100644 index 00000000..5d3fb4ec --- /dev/null +++ b/ejb/remote/roles-allowed/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + + + org.javaee8 + ejb-remote + 1.0-SNAPSHOT + + + ejb-remote-roles-allowed + war + + Java EE 8 Sample: ejb - remote - Roles Allowed + + + + payara-ci-managed + + + org.javaee8.ejb.remote.vendor + ejb.remote.vendor.payara-rest + 1.0-SNAPSHOT + + + + + + payara-remote + + + org.javaee8.ejb.remote.vendor + ejb.remote.vendor.payara-rest + 1.0-SNAPSHOT + + + + + + glassfish-remote + + + org.javaee8.ejb.remote.vendor + ejb.remote.vendor.payara-rest + 1.0-SNAPSHOT + + + + + + + diff --git a/ejb/remote/roles-allowed/src/main/java/org/javaee8/ejb/remote/remote/Bean.java b/ejb/remote/roles-allowed/src/main/java/org/javaee8/ejb/remote/remote/Bean.java new file mode 100644 index 00000000..1434ae12 --- /dev/null +++ b/ejb/remote/roles-allowed/src/main/java/org/javaee8/ejb/remote/remote/Bean.java @@ -0,0 +1,20 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8.ejb.remote.remote; + +import java.io.Serializable; + +import javax.annotation.security.RolesAllowed; +import javax.ejb.Stateless; + +@Stateless +public class Bean implements BeanRemote, Serializable { + + private static final long serialVersionUID = 1L; + + @Override + @RolesAllowed("g1") + public String method() { + return "method"; + } + +} diff --git a/ejb/remote/roles-allowed/src/main/java/org/javaee8/ejb/remote/remote/BeanRemote.java b/ejb/remote/roles-allowed/src/main/java/org/javaee8/ejb/remote/remote/BeanRemote.java new file mode 100644 index 00000000..674fd766 --- /dev/null +++ b/ejb/remote/roles-allowed/src/main/java/org/javaee8/ejb/remote/remote/BeanRemote.java @@ -0,0 +1,9 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8.ejb.remote.remote; + +import javax.ejb.Remote; + +@Remote +public interface BeanRemote { + String method(); +} diff --git a/ejb/remote/roles-allowed/src/test/java/org/javaee8/ejb/remote/RemoteBeanTest.java b/ejb/remote/roles-allowed/src/test/java/org/javaee8/ejb/remote/RemoteBeanTest.java new file mode 100644 index 00000000..cf6bee83 --- /dev/null +++ b/ejb/remote/roles-allowed/src/test/java/org/javaee8/ejb/remote/RemoteBeanTest.java @@ -0,0 +1,77 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8.ejb.remote; + +import static org.javaee8.ServerOperations.addUsersToContainerIdentityStore; +import static org.jboss.shrinkwrap.api.asset.EmptyAsset.INSTANCE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; + +import javax.naming.Context; +import javax.naming.NamingException; + +import org.javaee8.RemoteEJBContextFactory; +import org.javaee8.RemoteEJBContextProvider; +import org.javaee8.ejb.remote.remote.Bean; +import org.javaee8.ejb.remote.remote.BeanRemote; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * This class demonstrates and tests how to request an EJB bean from a remote server. + * + *

+ * {@link RemoteEJBContextProvider} is used, which is a test artifact abstracting the different + * ways this is done for different servers. + * + * @author Arjan Tijms + * + */ +@RunWith(Arquillian.class) +public class RemoteBeanTest { + + private RemoteEJBContextProvider remoteEJBContextProvider; + + @Deployment + public static Archive deployment() { + + // Add user u1 with password p1 and group g1 to the container's native identity store + addUsersToContainerIdentityStore(); + + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Bean.class, BeanRemote.class) + .addAsManifestResource(INSTANCE, "beans.xml"); + } + + @Before + public void before() { + remoteEJBContextProvider = RemoteEJBContextFactory.getProvider(); + assumeTrue( + "No RemoteEJBContextProvider available in current profile", + remoteEJBContextProvider != null); + } + + @After + public void after() { + remoteEJBContextProvider.releaseContext(); + } + + @Test + @RunAsClient + public void callProtectedRemoteBean() throws NamingException { + // Obtain the JNDI naming context in a vendor specific way. + Context ejbRemoteContext = remoteEJBContextProvider.getContextWithCredentialsSet("u1", "p1"); + + BeanRemote beanRemote = (BeanRemote) ejbRemoteContext.lookup("java:global/test/Bean"); + + assertEquals("method", beanRemote.method()); + } + +} \ No newline at end of file diff --git a/ejb/remote/roles-allowed/src/test/resources/addUsersPayara.txt b/ejb/remote/roles-allowed/src/test/resources/addUsersPayara.txt new file mode 100644 index 00000000..037cdbd6 --- /dev/null +++ b/ejb/remote/roles-allowed/src/test/resources/addUsersPayara.txt @@ -0,0 +1 @@ +create-file-user --groups g1 --passwordfile ${project.build.directory}/test-classes/password.txt u1 \ No newline at end of file diff --git a/ejb/remote/roles-allowed/src/test/resources/password.txt b/ejb/remote/roles-allowed/src/test/resources/password.txt new file mode 100644 index 00000000..c00bb4ca --- /dev/null +++ b/ejb/remote/roles-allowed/src/test/resources/password.txt @@ -0,0 +1 @@ +AS_ADMIN_USERPASSWORD=p1 diff --git a/ejb/remote/vendor/README.md b/ejb/remote/vendor/README.md new file mode 100644 index 00000000..7295bd08 --- /dev/null +++ b/ejb/remote/vendor/README.md @@ -0,0 +1,12 @@ +# Java EE 8 Samples: EJB - Remote - Vendor # + +This module contains vendor specific implementations to obtain the JNDI context from where remote EJB beans can be requested +from with a username/password credential. + +## Implementations ## + + - payara-glassfish - An implementation that works for both Payara and GlassFish + + + + diff --git a/ejb/remote/vendor/payara-rest/README.md b/ejb/remote/vendor/payara-rest/README.md new file mode 100644 index 00000000..b1a3a57b --- /dev/null +++ b/ejb/remote/vendor/payara-rest/README.md @@ -0,0 +1,9 @@ +# Java EE 7 Samples: EJB - Remote - Vendor - Payara and GlassFish # + +This modules contains a class that returns a JNDI context suitable for remote lookups against the default URL +for a remote Payara or GlassFish server (localhost). It sets the provided credentials +in a Payara/GlassFish specific way and puts the required client jar on the classpath. + + + + diff --git a/ejb/remote/vendor/payara-rest/pom.xml b/ejb/remote/vendor/payara-rest/pom.xml new file mode 100644 index 00000000..e29fc862 --- /dev/null +++ b/ejb/remote/vendor/payara-rest/pom.xml @@ -0,0 +1,30 @@ + + + + + 4.0.0 + + + org.javaee8 + ejb-remote-vendor + 1.0-SNAPSHOT + + + org.javaee8.ejb.remote.vendor + ejb.remote.vendor.payara-rest + + Java EE 8 Sample: ejb - remote - vendor - Payara REST Remote EJB Provider + + + + org.javaee8 + test-utils + 1.0-SNAPSHOT + + + fish.payara.extras + ejb-http-client + 5.191 + + + diff --git a/ejb/remote/vendor/payara-rest/src/main/java/org/javaee8/PayaraEJBContextProvider.java b/ejb/remote/vendor/payara-rest/src/main/java/org/javaee8/PayaraEJBContextProvider.java new file mode 100644 index 00000000..a1414863 --- /dev/null +++ b/ejb/remote/vendor/payara-rest/src/main/java/org/javaee8/PayaraEJBContextProvider.java @@ -0,0 +1,42 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8; + +import static javax.naming.Context.INITIAL_CONTEXT_FACTORY; +import static javax.naming.Context.PROVIDER_URL; +import static javax.naming.Context.SECURITY_CREDENTIALS; +import static javax.naming.Context.SECURITY_PRINCIPAL; + +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; + +/** + * This class returns a JNDI context suitable for remote lookups against the default URL + * for a remote Payara or GlassFish server (localhost). It sets the provided credentials + * in a Payara/GlassFish specific way. + * + * @author Arjan Tijms + * + */ +public class PayaraEJBContextProvider implements RemoteEJBContextProvider { + public Context getContextWithCredentialsSet(String username, String password) { + Hashtable environment = new Hashtable(); + environment.put(INITIAL_CONTEXT_FACTORY, "fish.payara.ejb.rest.client.RemoteEJBContextFactory"); + environment.put(PROVIDER_URL, "http://localhost:8080/ejb-invoker"); + environment.put(SECURITY_PRINCIPAL, "u1"); + environment.put(SECURITY_CREDENTIALS, "p1"); + + try { + return new InitialContext(environment); + } catch (NamingException e) { + throw new IllegalStateException(e); + } + + } + + public void releaseContext() { + } + +} diff --git a/ejb/remote/vendor/payara-rest/src/main/resources/META-INF/services/org.javaee8.RemoteEJBContextProvider b/ejb/remote/vendor/payara-rest/src/main/resources/META-INF/services/org.javaee8.RemoteEJBContextProvider new file mode 100644 index 00000000..039b7cb9 --- /dev/null +++ b/ejb/remote/vendor/payara-rest/src/main/resources/META-INF/services/org.javaee8.RemoteEJBContextProvider @@ -0,0 +1 @@ +org.javaee8.PayaraEJBContextProvider \ No newline at end of file diff --git a/ejb/remote/vendor/pom.xml b/ejb/remote/vendor/pom.xml new file mode 100644 index 00000000..05f59217 --- /dev/null +++ b/ejb/remote/vendor/pom.xml @@ -0,0 +1,37 @@ + + + + + 4.0.0 + + org.javaee8 + ejb-remote-vendor + 1.0-SNAPSHOT + pom + + Java EE 8 Sample: ejb - remote - vendor + + + + payara-ci-managed + + payara-rest + + + + + payara-remote + + payara-rest + + + + + glassfish-remote + + payara-rest + + + + + diff --git a/jaxrs/README.md b/jaxrs/README.md new file mode 100644 index 00000000..d702d957 --- /dev/null +++ b/jaxrs/README.md @@ -0,0 +1,7 @@ +# Java EE 8 Samples: JAX-RS 2.1# + +The [JSR 370](https://www.jcp.org/en/jsr/detail?id=370) specifies the JavaTM API for RESTful Web Services. + +## Samples ## + + - producer \ No newline at end of file diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml new file mode 100644 index 00000000..d0bb4e37 --- /dev/null +++ b/jaxrs/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + jaxrs + pom + + Java EE 8 Samples: JAX-RS + + + sse-producer + + + + + org.javaee8 + test-utils + ${project.version} + + + diff --git a/jaxrs/sse-producer/pom.xml b/jaxrs/sse-producer/pom.xml new file mode 100644 index 00000000..faabd965 --- /dev/null +++ b/jaxrs/sse-producer/pom.xml @@ -0,0 +1,13 @@ + + 4.0.0 + + + org.javaee8 + jaxrs + 1.0-SNAPSHOT + + + sse-producer + war + Java EE 8 Samples: JAX-RS - SSE Producer + diff --git a/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/data/EventData.java b/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/data/EventData.java new file mode 100644 index 00000000..8c208462 --- /dev/null +++ b/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/data/EventData.java @@ -0,0 +1,48 @@ +package org.javaee8.jaxrs.sseproducer.data; + +import java.util.Date; +import java.util.UUID; + +/** + * + * @author daniel + */ +public class EventData { + + private Date time; + private String id; + private String comment; + + public EventData() { + } + + public EventData(String comment) { + this.setTime(new Date()); + this.setId(UUID.randomUUID().toString()); + this.setComment(comment); + } + + public Date getTime() { + return time; + } + + public void setTime(Date time) { + this.time = time; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} diff --git a/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/producer/SseResource.java b/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/producer/SseResource.java new file mode 100644 index 00000000..b82a60de --- /dev/null +++ b/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/producer/SseResource.java @@ -0,0 +1,58 @@ +package org.javaee8.jaxrs.sseproducer.producer; + +import javax.annotation.PostConstruct; +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.sse.Sse; +import javax.ws.rs.sse.SseBroadcaster; +import javax.ws.rs.sse.SseEventSink; + +import org.javaee8.jaxrs.sseproducer.data.EventData; + +/** + * Produces server side events. + * + * @author Daniel Contreras + */ +@Path("sse") +public class SseResource { + + @Context + private Sse sse; + + private volatile SseBroadcaster sseBroadcaster; + + @PostConstruct + public void init() { + this.sseBroadcaster = sse.newBroadcaster(); + } + + @GET + @Path("register") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void register(@Context SseEventSink eventSink) { + + final Jsonb json = JsonbBuilder.create(); + eventSink.send(sse.newEvent("INIT", json.toJson(new EventData("event:intialized")))); + + sseBroadcaster.register(eventSink); + + for (int i = 0; i < 5; i++) { + + sseBroadcaster.broadcast(sse.newEvent("EVENT", json.toJson(new EventData("event:" + i)))); + + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + eventSink.send(sse.newEvent("FINISH", json.toJson(new EventData("event:finished")))); + } +} diff --git a/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/rest/RestApplication.java b/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/rest/RestApplication.java new file mode 100644 index 00000000..66e3d31d --- /dev/null +++ b/jaxrs/sse-producer/src/main/java/org/javaee8/jaxrs/sseproducer/rest/RestApplication.java @@ -0,0 +1,13 @@ +package org.javaee8.jaxrs.sseproducer.rest; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * + * @author daniel + */ +@ApplicationPath("rest") +public class RestApplication extends Application { + +} diff --git a/jaxrs/sse-producer/src/main/webapp/index.html b/jaxrs/sse-producer/src/main/webapp/index.html new file mode 100644 index 00000000..a7763133 --- /dev/null +++ b/jaxrs/sse-producer/src/main/webapp/index.html @@ -0,0 +1,43 @@ + + + + + SSE + + + + + + + +

+ + + + + diff --git a/jaxrs/sse-producer/src/test/java/org/javaee8/jaxrs/sseproducer/SseResourceTest.java b/jaxrs/sse-producer/src/test/java/org/javaee8/jaxrs/sseproducer/SseResourceTest.java new file mode 100644 index 00000000..aa411b7a --- /dev/null +++ b/jaxrs/sse-producer/src/test/java/org/javaee8/jaxrs/sseproducer/SseResourceTest.java @@ -0,0 +1,132 @@ +package org.javaee8.jaxrs.sseproducer; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import java.net.URL; +import java.util.Date; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import javax.ws.rs.sse.InboundSseEvent; +import javax.ws.rs.sse.SseEventSource; + +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.HttpUrlConnectorProvider; +import org.hamcrest.Matchers; +import org.javaee8.jaxrs.sseproducer.data.EventData; +import org.javaee8.jaxrs.sseproducer.producer.SseResource; +import org.javaee8.jaxrs.sseproducer.rest.RestApplication; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test example for the Server-Sent Events with the Jersey JAX-RS implementation. + * + * @author Daniel Contreras + * @author David Matějček + */ +@RunWith(Arquillian.class) +public class SseResourceTest { + + private static final String[] EVENT_TYPES = {"INIT", "EVENT", "FINISH"}; + + @ArquillianResource + private URL base; + + private Client sseClient; + private WebTarget target; + private SseEventSource eventSource; + + @Deployment(testable = true) + public static WebArchive createDeployment() { + return create(WebArchive.class).addClasses(RestApplication.class, SseResource.class, EventData.class); + } + + + /** + * Initializes the client, target and the eventSource used to create event consumers + */ + @Before + public void setup() { + // this is needed to avoid a conflict with embedded server, that can have + // customized configuration and connector providers. + final ClientConfig configuration = new ClientConfig(); + configuration.property(ClientProperties.CONNECT_TIMEOUT, 100); + configuration.property(ClientProperties.READ_TIMEOUT, 5000); + configuration.connectorProvider(new HttpUrlConnectorProvider()); + this.sseClient = ClientBuilder.newClient(configuration); + this.target = this.sseClient.target(this.base + "rest/sse/register"); + this.eventSource = SseEventSource.target(this.target).build(); + System.out.println("SSE Event source created........"); + final Response response = this.target.request().get(); + assertThat("GET response status - server is not ready", response.getStatus(), + Matchers.equalTo(Response.Status.OK.getStatusCode())); + } + + + /** + * Closes all client resources. + */ + @After + public void teardown() { + this.eventSource.close(); + System.out.println("Closed SSE Event source.."); + this.sseClient.close(); + System.out.println("Closed JAX-RS client.."); + } + + + /** + * Registers reaction on events, waits for events and checks their content. + * + * @throws Exception + */ + @Test(timeout = 5000) + @RunAsClient + public void testSSE() throws Exception { + final Queue asyncExceptions = new ConcurrentLinkedQueue<>(); + final Queue receivedEvents = new ConcurrentLinkedQueue<>(); + // jsonb is thread safe! + final Jsonb jsonb = JsonbBuilder.create(); + final Consumer onEvent = (sseEvent) -> { + assertThat("event type", sseEvent.getName(), Matchers.isOneOf(EVENT_TYPES)); + final String data = sseEvent.readData(); + System.out.println("Data received as string:\n" + data); + assertNotNull("data received as string", data); + final EventData event = jsonb.fromJson(data, EventData.class); + receivedEvents.add(event); + assertThat("event.time", event.getTime(), instanceOf(Date.class)); + assertNotNull("event.id", event.getId()); + assertThat("event.comment", event.getComment(), Matchers.containsString("event:")); + }; + this.eventSource.register(onEvent, asyncExceptions::add); + System.out.println("Server Side Events Client registered in the test thread."); + // following line starts acceptation of events. + this.eventSource.open(); + // don't end the test until we have all events or timeout or error comes. + // this is not an obvious implementation, we only need to hold the test until all events + // are asynchronously processed. + while (receivedEvents.size() <= 5 && asyncExceptions.isEmpty()) { + Thread.sleep(10L); + } + assertThat("receiver exceptions", asyncExceptions, Matchers.emptyIterable()); + } +} diff --git a/jpa/dynamic-tx/pom.xml b/jpa/dynamic-tx/pom.xml new file mode 100644 index 00000000..ba8511cc --- /dev/null +++ b/jpa/dynamic-tx/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + + + org.javaee8 + jpa + 1.0-SNAPSHOT + + + dynamic-tx + war + Java EE 8 Samples: JPA - Dynamic Transaction + + diff --git a/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/ApplicationInit.java b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/ApplicationInit.java new file mode 100644 index 00000000..b6c03e4e --- /dev/null +++ b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/ApplicationInit.java @@ -0,0 +1,62 @@ +package org.javaee8.jpa.dynamic.tx; + +import static java.util.stream.Collectors.toSet; + +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.InterceptionFactory; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.javaee8.jpa.dynamic.tx.util.EmployeeBeanWrapper; +import org.javaee8.jpa.dynamic.tx.util.TransactionLiteral; + + +@Alternative +@Priority(500) +@ApplicationScoped +public class ApplicationInit { + + private static final Logger logger = Logger.getLogger(ApplicationInit.class.getName()); + + @Produces @PersistenceContext + private EntityManager entityManager; + + @Produces + public EmployeeService produce(InterceptionFactory interceptionFactory, BeanManager beanManager) { + + logger.info("Producing EmployeeService"); + + EmployeeService employeeBean = + createRef( + beanManager.resolve( + beanManager.getBeans(EmployeeService.class) + .stream() + .filter(e -> !e.getBeanClass().equals(ApplicationInit.class)) + .collect(toSet())), beanManager); + + interceptionFactory + .configure() + .filterMethods(am -> am.getJavaMember().getName().equals("persist")) + .forEach( + amc -> amc.add(new TransactionLiteral())); + + return interceptionFactory.createInterceptedInstance( + new EmployeeBeanWrapper(employeeBean)); + } + + EmployeeService createRef(Bean bean, BeanManager beanManager) { + return (EmployeeService) + beanManager.getReference( + bean, + EmployeeService.class, + beanManager.createCreationalContext(bean)); + } + +} diff --git a/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/Employee.java b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/Employee.java new file mode 100644 index 00000000..d30e3a20 --- /dev/null +++ b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/Employee.java @@ -0,0 +1,31 @@ +package org.javaee8.jpa.dynamic.tx; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class Employee { + + @Id + @GeneratedValue + private int id; + + private String name; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/EmployeeService.java b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/EmployeeService.java new file mode 100644 index 00000000..6ccb96d2 --- /dev/null +++ b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/EmployeeService.java @@ -0,0 +1,20 @@ +package org.javaee8.jpa.dynamic.tx; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; + +@ApplicationScoped +public class EmployeeService { + + @Inject + private EntityManager entityManager; + + public void persist(Employee employee) { + entityManager.persist(employee); + } + + public Employee getById(int id) { + return entityManager.find(Employee.class, id); + } +} diff --git a/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/util/EmployeeBeanWrapper.java b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/util/EmployeeBeanWrapper.java new file mode 100644 index 00000000..3cc74c9d --- /dev/null +++ b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/util/EmployeeBeanWrapper.java @@ -0,0 +1,32 @@ +package org.javaee8.jpa.dynamic.tx.util; + +import org.javaee8.jpa.dynamic.tx.Employee; +import org.javaee8.jpa.dynamic.tx.EmployeeService; + +public class EmployeeBeanWrapper extends EmployeeService { + + private EmployeeService employeeBean; + + public EmployeeBeanWrapper() { + } + + public EmployeeBeanWrapper(EmployeeService employeeBean) { + this.employeeBean = employeeBean; + } + + EmployeeService getWrapped() { + return employeeBean; + } + + @Override + public void persist(Employee employee) { + getWrapped().persist(employee); + } + + @Override + public Employee getById(int id) { + return getWrapped().getById(id); + } + +} + diff --git a/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/util/TransactionLiteral.java b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/util/TransactionLiteral.java new file mode 100644 index 00000000..af814783 --- /dev/null +++ b/jpa/dynamic-tx/src/main/java/org/javaee8/jpa/dynamic/tx/util/TransactionLiteral.java @@ -0,0 +1,28 @@ +package org.javaee8.jpa.dynamic.tx.util; + +import static javax.transaction.Transactional.TxType.REQUIRED; + +import javax.enterprise.util.AnnotationLiteral; +import javax.transaction.Transactional; + +@SuppressWarnings("all") +public class TransactionLiteral extends AnnotationLiteral implements Transactional { + + private static final long serialVersionUID = 1L; + + @Override + public TxType value() { + return REQUIRED; + } + + @Override + public Class[] rollbackOn() { + return new Class[0]; + } + + @Override + public Class[] dontRollbackOn() { + return new Class[0]; + } + +} \ No newline at end of file diff --git a/jpa/dynamic-tx/src/main/resources/META-INF/persistence.xml b/jpa/dynamic-tx/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000..020731f9 --- /dev/null +++ b/jpa/dynamic-tx/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/jpa/dynamic-tx/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/jpa/dynamic-tx/src/main/webapp/WEB-INF/jboss-deployment-structure.xml new file mode 100644 index 00000000..4feda966 --- /dev/null +++ b/jpa/dynamic-tx/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/jpa/dynamic-tx/src/test/java/org/javaee8/jpa/dynamic/tx/DynamicTXTest.java b/jpa/dynamic-tx/src/test/java/org/javaee8/jpa/dynamic/tx/DynamicTXTest.java new file mode 100644 index 00000000..9b77e54d --- /dev/null +++ b/jpa/dynamic-tx/src/test/java/org/javaee8/jpa/dynamic/tx/DynamicTXTest.java @@ -0,0 +1,58 @@ +package org.javaee8.jpa.dynamic.tx; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.io.File; + +import javax.inject.Inject; + +import org.hamcrest.Matchers; +import org.javaee8.jpa.dynamic.tx.ApplicationInit; +import org.javaee8.jpa.dynamic.tx.Employee; +import org.javaee8.jpa.dynamic.tx.EmployeeService; +import org.javaee8.jpa.dynamic.tx.util.EmployeeBeanWrapper; +import org.javaee8.jpa.dynamic.tx.util.TransactionLiteral; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.junit.runner.RunWith; + + +@RunWith(Arquillian.class) +public class DynamicTXTest { + + @Inject + private EmployeeService employeeService; + + @Deployment + public static WebArchive createDeployment() { + return create(WebArchive.class) + .addClasses( + ApplicationInit.class, + Employee.class, + EmployeeBeanWrapper.class, + EmployeeService.class) + .addPackage( + TransactionLiteral.class.getPackage()) + .addAsResource( + "META-INF/persistence.xml") + .addAsWebInfResource(new File("src/main/webapp/WEB-INF/jboss-deployment-structure.xml")); + } + + @Test + public void testPersist() throws Exception { + Employee employee = new Employee(); + employee.setName("reza"); + + assertThat("employeeService", employeeService, Matchers.notNullValue()); + employeeService.persist(employee); + + Employee persistedEmployee = employeeService.getById(employee.getId()); + + assertEquals("reza", persistedEmployee.getName()); + } + +} diff --git a/jpa/pom.xml b/jpa/pom.xml new file mode 100644 index 00000000..523a9fc5 --- /dev/null +++ b/jpa/pom.xml @@ -0,0 +1,37 @@ + + 4.0.0 + + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + jpa + pom + Java EE 8 Samples: JPA + + + dynamic-tx + stream + + + + + org.javaee8 + test-utils + ${project.version} + + + + + thorntail + + + com.h2database + h2 + + + + + diff --git a/jpa/stream/pom.xml b/jpa/stream/pom.xml new file mode 100644 index 00000000..910c3cb0 --- /dev/null +++ b/jpa/stream/pom.xml @@ -0,0 +1,13 @@ + + 4.0.0 + + + org.javaee8 + jpa + 1.0-SNAPSHOT + + + stream + war + Java EE 8 Samples: JPA - Stream + diff --git a/jpa/stream/src/main/java/org/javaee8/jpa/stream/domain/Person.java b/jpa/stream/src/main/java/org/javaee8/jpa/stream/domain/Person.java new file mode 100644 index 00000000..e6c11668 --- /dev/null +++ b/jpa/stream/src/main/java/org/javaee8/jpa/stream/domain/Person.java @@ -0,0 +1,52 @@ +package org.javaee8.jpa.stream.domain; + +import java.io.Serializable; +import javax.persistence.Basic; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +/** + * + * @author Gaurav Gupta + * + */ +@Entity +public class Person implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Basic + private String name; + + @Basic + private String address; + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return this.address; + } + + public void setAddress(String address) { + this.address = address; + } + +} \ No newline at end of file diff --git a/jpa/stream/src/main/java/org/javaee8/jpa/stream/repository/PersonRepository.java b/jpa/stream/src/main/java/org/javaee8/jpa/stream/repository/PersonRepository.java new file mode 100644 index 00000000..4be6190c --- /dev/null +++ b/jpa/stream/src/main/java/org/javaee8/jpa/stream/repository/PersonRepository.java @@ -0,0 +1,25 @@ +package org.javaee8.jpa.stream.repository; + +import java.util.stream.Stream; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.criteria.CriteriaQuery; +import org.javaee8.jpa.stream.domain.Person; + +/** + * + * @author Gaurav Gupta + * + */ +public class PersonRepository { + + @PersistenceContext + private EntityManager entityManager; + + public Stream findAll() { + CriteriaQuery criteriaQuery = entityManager.getCriteriaBuilder().createQuery(); + criteriaQuery.select(criteriaQuery.from(Person.class)); + return entityManager.createQuery(criteriaQuery).getResultStream(); + } + +} diff --git a/jpa/stream/src/test/java/org/javaee8/jpa/stream/controller/PersonControllerTest.java b/jpa/stream/src/test/java/org/javaee8/jpa/stream/controller/PersonControllerTest.java new file mode 100644 index 00000000..54b0f75f --- /dev/null +++ b/jpa/stream/src/test/java/org/javaee8/jpa/stream/controller/PersonControllerTest.java @@ -0,0 +1,43 @@ +package org.javaee8.jpa.stream.controller; + +import java.util.stream.Stream; +import org.javaee8.jpa.stream.repository.PersonRepository; +import org.javaee8.jpa.stream.domain.Person; +import javax.inject.Inject; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.junit.Test; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import static org.junit.Assert.assertEquals; +import org.junit.runner.RunWith; + +/** + * + * @author Gaurav Gupta + * + */ +@RunWith(Arquillian.class) +public class PersonControllerTest { + + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class) + .addAsWebInfResource("beans.xml") + .addAsResource("test-persistence.xml", "META-INF/persistence.xml") + .addAsResource("META-INF/sql/insert.sql") + .addClass(Person.class) + .addClass(PersonRepository.class); + } + + @Inject + private PersonRepository personRepository; + + @Test + public void testStream() throws Exception { + Stream personStream = personRepository.findAll(); + long personCount = personStream.count(); + assertEquals(2, personCount); + } + +} diff --git a/jpa/stream/src/test/resources/META-INF/sql/insert.sql b/jpa/stream/src/test/resources/META-INF/sql/insert.sql new file mode 100644 index 00000000..8be18f13 --- /dev/null +++ b/jpa/stream/src/test/resources/META-INF/sql/insert.sql @@ -0,0 +1,2 @@ +INSERT INTO person (id, name, address) VALUES (1002, 'Arjan', 'abc') +INSERT INTO person (id, name, address) VALUES (1001, 'Gaurav', 'xyz') diff --git a/jpa/stream/src/test/resources/beans.xml b/jpa/stream/src/test/resources/beans.xml new file mode 100644 index 00000000..2777559c --- /dev/null +++ b/jpa/stream/src/test/resources/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/jpa/stream/src/test/resources/test-persistence.xml b/jpa/stream/src/test/resources/test-persistence.xml new file mode 100644 index 00000000..fc0138df --- /dev/null +++ b/jpa/stream/src/test/resources/test-persistence.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/jsf/README.md b/jsf/README.md new file mode 100644 index 00000000..5d31707a --- /dev/null +++ b/jsf/README.md @@ -0,0 +1,11 @@ +# Java EE 8 Samples: JSF 2.3# + +The [JSR 372](https://jcp.org/en/jsr/detail?id=372) specifies the next version of JavaServer Faces - JSF 2.3. + +## Samples ## + +- hello-world shows a typical "hello world" that prints this often used sentence coming from a backing bean +- extensionless-mapping shows how to configure JSF so that views can be requested without using an extension + + + diff --git a/jsf/extensionless-mapping/pom.xml b/jsf/extensionless-mapping/pom.xml new file mode 100644 index 00000000..1f078a5e --- /dev/null +++ b/jsf/extensionless-mapping/pom.xml @@ -0,0 +1,15 @@ + + 4.0.0 + + + org.javaee8 + jsf + 1.0-SNAPSHOT + + + extensionless-mapping + war + Java EE 8 Samples: JSF - Extensionless Mapping + + + diff --git a/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/ApplicationInit.java b/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/ApplicationInit.java new file mode 100644 index 00000000..a232e89a --- /dev/null +++ b/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/ApplicationInit.java @@ -0,0 +1,20 @@ +package org.javaee8.cdi.dynamic.bean; + +import static javax.faces.annotation.FacesConfig.Version.JSF_2_3; + +import javax.enterprise.context.ApplicationScoped; +import javax.faces.annotation.FacesConfig; + +/** + * This class is needed to activate JSF and configure it to be the + * right version. Without this being present an explicit mapping + * of the FacesServlet in web.xml would be required, but JSF 2.3 + * would then run in a JSF 2.2 compatibility mode. + * + * @author Arjan Tijms + */ +@FacesConfig(version = JSF_2_3) +@ApplicationScoped +public class ApplicationInit { + +} diff --git a/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/MappingInit.java b/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/MappingInit.java new file mode 100644 index 00000000..50bb1276 --- /dev/null +++ b/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/MappingInit.java @@ -0,0 +1,48 @@ +package org.javaee8.cdi.dynamic.bean; + +import static javax.faces.application.ViewVisitOption.RETURN_AS_MINIMAL_IMPLICIT_OUTCOME; + +import java.util.Map; + +import javax.faces.application.Application; +import javax.faces.context.FacesContext; +import javax.faces.event.AbortProcessingException; +import javax.faces.event.SystemEvent; +import javax.faces.event.SystemEventListener; +import javax.faces.webapp.FacesServlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; + +/** + * + * @author Arjan Tijms + */ +public class MappingInit implements SystemEventListener { + + + @Override + public void processEvent(SystemEvent event) throws AbortProcessingException { + + FacesContext facesContext = event.getFacesContext(); + ServletContext sc = (ServletContext) facesContext.getExternalContext().getContext(); + + if (Boolean.valueOf((String) sc.getAttribute("mappingsAdded"))) { + return; + } + + Map servletRegistrations = (Map) sc.getAttribute("mappings"); + + if (servletRegistrations == null) { + return; + } + + MappingServletContextListener.addServletMappings(servletRegistrations, facesContext); + } + + + @Override + public boolean isListenerForSource(Object source) { + return source instanceof Application; + } + +} diff --git a/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/MappingServletContextListener.java b/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/MappingServletContextListener.java new file mode 100644 index 00000000..d56b8468 --- /dev/null +++ b/jsf/extensionless-mapping/src/main/java/org/javaee8/cdi/dynamic/bean/MappingServletContextListener.java @@ -0,0 +1,44 @@ +package org.javaee8.cdi.dynamic.bean; + +import static javax.faces.application.ViewVisitOption.RETURN_AS_MINIMAL_IMPLICIT_OUTCOME; + +import java.util.Map; + +import javax.faces.application.Application; +import javax.faces.context.FacesContext; +import javax.faces.webapp.FacesServlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletRegistration; +import javax.servlet.annotation.WebListener; + +@WebListener +public class MappingServletContextListener implements ServletContextListener { + + @Override + public void contextInitialized(ServletContextEvent sce) { + ServletContext sc = sce.getServletContext(); + + sc.setAttribute("mappings", sce.getServletContext().getServletRegistrations()); + + FacesContext facesContext = FacesContext.getCurrentInstance(); + if (facesContext == null) { + //It 's possible that JSF isn't available at this time depending on JSF implementation and Java server container + return; + } + + addServletMappings(sc.getServletRegistrations(), facesContext); + + //Add a flag to not add the mappings again later in MappingInit + sc.setAttribute("mappingsAdded", "true"); + } + + public static void addServletMappings(Map servletRegistrations, FacesContext facesContext) { + servletRegistrations.values().stream().filter(e -> e.getClassName().equals(FacesServlet.class.getName())) + .findAny() + .ifPresent(reg -> facesContext.getApplication().getViewHandler().getViews( + facesContext, "/", RETURN_AS_MINIMAL_IMPLICIT_OUTCOME).forEach(e -> reg.addMapping(e))); + } + +} diff --git a/jsf/extensionless-mapping/src/main/webapp/WEB-INF/faces-config.xml b/jsf/extensionless-mapping/src/main/webapp/WEB-INF/faces-config.xml new file mode 100644 index 00000000..0eae5f11 --- /dev/null +++ b/jsf/extensionless-mapping/src/main/webapp/WEB-INF/faces-config.xml @@ -0,0 +1,12 @@ + + + + + org.javaee8.cdi.dynamic.bean.MappingInit + javax.faces.event.PostConstructApplicationEvent + + + \ No newline at end of file diff --git a/jsf/extensionless-mapping/src/main/webapp/bar.xhtml b/jsf/extensionless-mapping/src/main/webapp/bar.xhtml new file mode 100755 index 00000000..a0f08748 --- /dev/null +++ b/jsf/extensionless-mapping/src/main/webapp/bar.xhtml @@ -0,0 +1,16 @@ + + + + + + Bar page + + + + This is page bar + + + diff --git a/jsf/extensionless-mapping/src/main/webapp/foo.xhtml b/jsf/extensionless-mapping/src/main/webapp/foo.xhtml new file mode 100755 index 00000000..395d3e61 --- /dev/null +++ b/jsf/extensionless-mapping/src/main/webapp/foo.xhtml @@ -0,0 +1,16 @@ + + + + + + Test JSF 2.3 Exact Mapping + + + + This is page foo
+
+ + diff --git a/jsf/extensionless-mapping/src/main/webapp/sub/bar.xhtml b/jsf/extensionless-mapping/src/main/webapp/sub/bar.xhtml new file mode 100755 index 00000000..cc886e63 --- /dev/null +++ b/jsf/extensionless-mapping/src/main/webapp/sub/bar.xhtml @@ -0,0 +1,16 @@ + + + + + + Sub-Bar page + + + + This is page sub-bar + + + diff --git a/jsf/extensionless-mapping/src/test/java/org/javaee8/cdi/dynamic/bean/ExtensionlessMappingTest.java b/jsf/extensionless-mapping/src/test/java/org/javaee8/cdi/dynamic/bean/ExtensionlessMappingTest.java new file mode 100644 index 00000000..c98f4e97 --- /dev/null +++ b/jsf/extensionless-mapping/src/test/java/org/javaee8/cdi/dynamic/bean/ExtensionlessMappingTest.java @@ -0,0 +1,99 @@ +package org.javaee8.cdi.dynamic.bean; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +/** + * + * @author Arjan Tijms + * + */ +@RunWith(Arquillian.class) +public class ExtensionlessMappingTest { + + @ArquillianResource + private URL base; + + private WebClient webClient; + + @Before + public void setup() { + webClient = new WebClient(); + } + + @After + public void teardown() { + webClient.close(); + } + + @Deployment + public static WebArchive deploy() { + WebArchive war = + create(WebArchive.class) + .addClasses(MappingInit.class, ApplicationInit.class, MappingServletContextListener.class) + .addAsWebResource(new File("src/main/webapp/foo.xhtml")) + .addAsWebResource(new File("src/main/webapp/bar.xhtml")) + .addAsWebResource(new File("src/main/webapp/sub/bar.xhtml"), "/sub/bar.xhtml") + .addAsWebInfResource("beans.xml") + .addAsWebInfResource(new File("src/main/webapp/WEB-INF/faces-config.xml")); + + System.out.println("War to be deployed contains: \n" + war.toString(true)); + + return war; + } + + + @Test + @RunAsClient + public void testExtensionlessMappingFoo() throws IOException { + + HtmlPage page = webClient.getPage(base + "foo"); + String content = page.asXml(); + + System.out.println("\nContent for `"+ base + "foo" + "` :\n" + content + "\n"); + + assertTrue(content.contains("This is page foo")); + } + + @Test + @RunAsClient + public void testExtensionlessMappingBar() throws IOException { + + HtmlPage page = webClient.getPage(base + "bar"); + String content = page.asXml(); + + System.out.println("\nContent for `"+ base + "bar" + "` :\n" + content + "\n"); + + assertTrue(content.contains("This is page bar")); + } + + @Test + @RunAsClient + public void testExtensionlessMappingSubBar() throws IOException { + + HtmlPage page = webClient.getPage(base + "sub/bar"); + String content = page.asXml(); + + System.out.println("\nContent for `"+ base + "sub/bar" + "` :\n" + content + "\n"); + + assertTrue(content.contains("This is page sub-bar")); + } + +} diff --git a/jsf/extensionless-mapping/src/test/resources/beans.xml b/jsf/extensionless-mapping/src/test/resources/beans.xml new file mode 100644 index 00000000..d306e5f6 --- /dev/null +++ b/jsf/extensionless-mapping/src/test/resources/beans.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/jsf/hello-world/README.md b/jsf/hello-world/README.md new file mode 100644 index 00000000..d48aecf7 --- /dev/null +++ b/jsf/hello-world/README.md @@ -0,0 +1,9 @@ +# Java EE 8 Samples: JSF 2.3 - Hello world# + +The [JSR 372](https://jcp.org/en/jsr/detail?id=372) specifies the next version of JavaServer Faces - JSF 2.3. + +A very simple example consisting out of a single page, a backing bean and a bean that activates JSF. +The page prints "Hello world, from JSF!" which comes from the backing bean. + + + diff --git a/jsf/hello-world/pom.xml b/jsf/hello-world/pom.xml new file mode 100644 index 00000000..9eef89d4 --- /dev/null +++ b/jsf/hello-world/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + + + org.javaee8 + jsf + 1.0-SNAPSHOT + + + hello-world + war + + Java EE 8 Samples: JSF - Hello World + diff --git a/jsf/hello-world/src/main/java/org/javaee8/jsf/hello/world/ApplicationInit.java b/jsf/hello-world/src/main/java/org/javaee8/jsf/hello/world/ApplicationInit.java new file mode 100644 index 00000000..c24a81ef --- /dev/null +++ b/jsf/hello-world/src/main/java/org/javaee8/jsf/hello/world/ApplicationInit.java @@ -0,0 +1,19 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8.jsf.hello.world; + +import javax.enterprise.context.ApplicationScoped; +import javax.faces.annotation.FacesConfig; + +/** + * This class is needed to activate JSF and configure it to be the + * right version. Without this being present an explicit mapping + * of the FacesServlet in web.xml would be required, but JSF 2.3 + * would then run in a JSF 2.2 compatibility mode. + * + * @author Arjan Tijms + */ +@FacesConfig +@ApplicationScoped +public class ApplicationInit { + +} diff --git a/jsf/hello-world/src/main/java/org/javaee8/jsf/hello/world/HelloBacking.java b/jsf/hello-world/src/main/java/org/javaee8/jsf/hello/world/HelloBacking.java new file mode 100644 index 00000000..bcd3f1c8 --- /dev/null +++ b/jsf/hello-world/src/main/java/org/javaee8/jsf/hello/world/HelloBacking.java @@ -0,0 +1,15 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8.jsf.hello.world; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Named; + +@Named +@RequestScoped +public class HelloBacking { + + public String getHello() { + return "Hello world, from JSF!"; + } + +} diff --git a/jsf/hello-world/src/main/webapp/hello.xhtml b/jsf/hello-world/src/main/webapp/hello.xhtml new file mode 100755 index 00000000..d352e640 --- /dev/null +++ b/jsf/hello-world/src/main/webapp/hello.xhtml @@ -0,0 +1,21 @@ + + + + + + + Hello + + + +

+ JSF says: +

+

+ #{helloBacking.hello} +

+ + + diff --git a/jsf/hello-world/src/test/java/org/javaee8/jsf/hello/world/JSFHelloWorldTest.java b/jsf/hello-world/src/test/java/org/javaee8/jsf/hello/world/JSFHelloWorldTest.java new file mode 100644 index 00000000..ce08fefc --- /dev/null +++ b/jsf/hello-world/src/test/java/org/javaee8/jsf/hello/world/JSFHelloWorldTest.java @@ -0,0 +1,72 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8.jsf.hello.world; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.net.URL; + +import org.javaee8.jsf.hello.world.ApplicationInit; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlPage; + +/** + * + * @author Arjan Tijms + * + */ +@RunWith(Arquillian.class) +public class JSFHelloWorldTest { + + @ArquillianResource + private URL base; + + private WebClient webClient; + + @Before + public void setup() { + webClient = new WebClient(); + } + + @After + public void teardown() { + webClient.close(); + } + + @Deployment + public static WebArchive deploy() { + WebArchive war = + create(WebArchive.class) + .addClasses(ApplicationInit.class, HelloBacking.class) + .addAsWebResource(new File("src/main/webapp/hello.xhtml")) + ; + + System.out.println("War to be deployed contains: \n" + war.toString(true)); + + return war; + } + + + @Test + @RunAsClient + public void testHelloWorld() throws IOException { + HtmlPage page = webClient.getPage(base + "hello.xhtml"); + + assertTrue(page.asXml().contains("Hello world, from JSF!")); + } + + + +} diff --git a/jsf/pom.xml b/jsf/pom.xml new file mode 100644 index 00000000..5f2daea2 --- /dev/null +++ b/jsf/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + jsf + pom + Java EE 8 Samples: JSF + + + hello-world + extensionless-mapping + + + + + org.javaee8 + test-utils + ${project.version} + + + diff --git a/json-p/merge/pom.xml b/json-p/merge/pom.xml new file mode 100644 index 00000000..46b22973 --- /dev/null +++ b/json-p/merge/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + + org.javaee8 + json-p + 1.0-SNAPSHOT + + + merge + Java EE 8 Samples: JSON-P - Merge + \ No newline at end of file diff --git a/json-p/merge/src/test/java/org/javaee8/jsonp/merge/JsonpMergeTest.java b/json-p/merge/src/test/java/org/javaee8/jsonp/merge/JsonpMergeTest.java new file mode 100644 index 00000000..ba1660b4 --- /dev/null +++ b/json-p/merge/src/test/java/org/javaee8/jsonp/merge/JsonpMergeTest.java @@ -0,0 +1,177 @@ +package org.javaee8.jsonp.merge; + +import javax.json.Json; +import javax.json.JsonMergePatch; +import javax.json.JsonObject; +import javax.json.JsonValue; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Class that tests and demonstrates the JSON-P 1.1 Merge Operations. + * @author Andrew Pielage + */ +@RunWith(Arquillian.class) +public class JsonpMergeTest { + + // Create a JsonObject with some values to be used in each test + private static final JsonObject json = Json.createObjectBuilder() + .add("Wibbly", "Wobbly") + .add("Replaced", false) + .add("Lexicon", Json.createArrayBuilder() + .add("Wibbles") + .add("Wobbles") + .build()) + .add("Nested", Json.createObjectBuilder() + .add("Birdie", "Wordie") + .add("Bestiary", Json.createArrayBuilder() + .add("Drowner") + .add("Werewolf") + .add("Chimera") + .build()) + .build()) + .build(); + + @Deployment + public static JavaArchive createDeployment() { + // Create a JavaArchive to deploy + JavaArchive jar = ShrinkWrap.create(JavaArchive.class); + + // Print out directory contents + System.out.println(jar.toString(true)); + + // Return Arquillian Test Archive for application server + return jar; + } + + /** + * Test that the JSON Merge operation replaces values as intended. + */ + @Test + public void replaceTest() { + // Create a JSON object that we'll merge into the class variable, replacing object members and array values + JsonObject jsonToMerge = Json.createObjectBuilder() + .add("Wibbly", "Bibbly") + .add("Replaced", "Yes") + .add("Lexicon", Json.createArrayBuilder() + .add("Wibbles") + .add("Bibbles") + .build()) + .add("Nested", Json.createObjectBuilder() + .add("Bestiary", Json.createArrayBuilder() + .add("Slyzard") + .add("Dragon") + .add("Ekimmara") + .build()) + .build()) + .build(); + + // Create a merge patch and apply it + JsonMergePatch mergePatch = Json.createMergePatch(jsonToMerge); + JsonValue mergedJson = mergePatch.apply(json); + + // Print out to more easily see what we've done + System.out.println("JsonpMergeTest.replaceTest: Before Merge: " + json); + System.out.println("JsonpMergeTest.replaceTest: JSON to Merge: " + jsonToMerge); + System.out.println("JsonpMergeTest.replaceTest: After Merge: " + mergedJson); + + // Test that everything is as it should be + JsonObject mergedJsonObject = mergedJson.asJsonObject(); + assertTrue("Merged JSON didn't merge correctly!", mergedJsonObject.getString("Wibbly").equals("Bibbly")); + assertTrue("Merged JSON didn't merge correctly!", mergedJsonObject.getString("Replaced").equals("Yes")); + assertTrue("JSON Array didn't merge correctly!", + mergedJsonObject.getJsonArray("Lexicon").getString(0).equals("Wibbles") + && mergedJsonObject.getJsonArray("Lexicon").getString(1).equals("Bibbles")); + assertTrue("Nested JSON didn't merge correctly!", + mergedJsonObject.getJsonObject("Nested").getString("Birdie").equals("Wordie")); + assertTrue("Nested JSON Array didn't merge correctly!", + mergedJsonObject.getJsonObject("Nested").getJsonArray("Bestiary").getString(0).equals("Slyzard") + && mergedJsonObject.getJsonObject("Nested").getJsonArray("Bestiary").getString(1).equals("Dragon") + && mergedJsonObject.getJsonObject("Nested").getJsonArray("Bestiary").getString(2).equals("Ekimmara")); + } + + /** + * Test that the JSON Merge operation adds values as intended. + */ + @Test + public void addTest() { + // Create a JSON object that we'll merge into the class variable, adding object members and array values + JsonObject jsonToMerge = Json.createObjectBuilder() + .add("Bibbly", "Bobbly") + .add("Lexicon", Json.createArrayBuilder() + .add("Wibbles") + .add("Wobbles") + .add("Bibbles") + .add("Bobbles") + .build()) + .build(); + + // Create a merge patch and apply it + JsonMergePatch mergePatch = Json.createMergePatch(jsonToMerge); + JsonValue mergedJson = mergePatch.apply(json); + + // Print out to more easily see what we've done + System.out.println("JsonpMergeTest.addTest: Before Merge: " + json); + System.out.println("JsonpMergeTest.addTest: JSON to Merge: " + jsonToMerge); + System.out.println("JsonpMergeTest.addTest: After Merge: " + mergedJson); + + // Test that everything is as it should be + JsonObject mergedJsonObject = mergedJson.asJsonObject(); + assertTrue("Merged JSON didn't merge correctly!", mergedJsonObject.getString("Wibbly").equals("Wobbly")); + assertTrue("Merged JSON didn't merge correctly!", mergedJsonObject.getString("Bibbly").equals("Bobbly")); + assertTrue("Merged JSON didn't merge correctly!", !mergedJsonObject.getBoolean("Replaced")); + assertTrue("JSON Array didn't merge correctly!", + mergedJsonObject.getJsonArray("Lexicon").getString(0).equals("Wibbles") + && mergedJsonObject.getJsonArray("Lexicon").getString(1).equals("Wobbles") + && mergedJsonObject.getJsonArray("Lexicon").getString(2).equals("Bibbles") + && mergedJsonObject.getJsonArray("Lexicon").getString(3).equals("Bobbles")); + assertTrue("Nested JSON didn't merge correctly!", + mergedJsonObject.getJsonObject("Nested").getString("Birdie").equals("Wordie")); + assertTrue("Nested JSON Array didn't merge correctly!", + mergedJsonObject.getJsonObject("Nested").getJsonArray("Bestiary").getString(0).equals("Drowner") + && mergedJsonObject.getJsonObject("Nested").getJsonArray("Bestiary").getString(1).equals("Werewolf") + && mergedJsonObject.getJsonObject("Nested").getJsonArray("Bestiary").getString(2).equals("Chimera")); + } + + /** + * Test that the JSON Merge operation removes values as intended. + */ + @Test + public void removeTest() { + // Create a JSON object that we'll merge into the class variable, removing object members and array values + JsonObject jsonToMerge = Json.createObjectBuilder() + .addNull("Wibbly") + .add("Lexicon", Json.createArrayBuilder() + .add("Wibbles") + .build()) + .add("Nested", Json.createObjectBuilder() + .addNull("Bestiary") + .build()) + .build(); + + // Create a merge patch and apply it + JsonMergePatch mergePatch = Json.createMergePatch(jsonToMerge); + JsonValue mergedJson = mergePatch.apply(json); + + // Print out to more easily see what we've done + System.out.println("JsonpMergeTest.removeTest: Before Merge: " + json); + System.out.println("JsonpMergeTest.removeTest: JSON to Merge: " + jsonToMerge); + System.out.println("JsonpMergeTest.removeTest: After Merge: " + mergedJson); + + // Test that everything is as it should be + JsonObject mergedJsonObject = mergedJson.asJsonObject(); + assertTrue("Merged JSON didn't merge correctly!", !mergedJsonObject.containsKey("Wibbly")); + assertTrue("Merged JSON didn't merge correctly!", !mergedJsonObject.getBoolean("Replaced")); + assertTrue("JSON Array didn't merge correctly!", + mergedJsonObject.getJsonArray("Lexicon").getString(0).equals("Wibbles")); + assertTrue("Nested JSON didn't merge correctly!", + mergedJsonObject.getJsonObject("Nested").getString("Birdie").equals("Wordie")); + assertTrue("Nested JSON Array didn't merge correctly!", + !mergedJsonObject.getJsonObject("Nested").containsKey("Bestiary")); + } +} diff --git a/json-p/patch/pom.xml b/json-p/patch/pom.xml new file mode 100644 index 00000000..47731a0d --- /dev/null +++ b/json-p/patch/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + + org.javaee8 + json-p + 1.0-SNAPSHOT + + + patch + Java EE 8 Samples: JSON-P - Patch + \ No newline at end of file diff --git a/json-p/patch/src/test/java/org/javaee8/jsonp/patch/JsonpPatchTest.java b/json-p/patch/src/test/java/org/javaee8/jsonp/patch/JsonpPatchTest.java new file mode 100644 index 00000000..3acf085d --- /dev/null +++ b/json-p/patch/src/test/java/org/javaee8/jsonp/patch/JsonpPatchTest.java @@ -0,0 +1,219 @@ +package org.javaee8.jsonp.patch; + +import javax.json.Json; +import javax.json.JsonException; +import javax.json.JsonObject; +import javax.json.JsonPatch; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.Assert; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Class that tests and demonstrates the JSON-P 1.1 Patch Operations. + * @author Andrew Pielage + */ +@RunWith(Arquillian.class) +public class JsonpPatchTest { + + // Create a JsonObject with some values to be used in each test + private static final JsonObject json = Json.createObjectBuilder() + .add("Wibbly", "Wobbly") + .add("Replaced", false) + .add("Lexicon", Json.createArrayBuilder() + .add("Wibbles") + .add("Wobbles") + .build()) + .add("Nested", Json.createObjectBuilder() + .add("Birdie", "Wordie") + .add("Bestiary", Json.createArrayBuilder() + .add("Drowner") + .add("Werewolf") + .add("Chimera") + .build()) + .build()) + .build(); + + @Deployment + public static JavaArchive createDeployment() { + // Create a JavaArchive to deploy + JavaArchive jar = ShrinkWrap.create(JavaArchive.class); + + // Print out directory contents + System.out.println(jar.toString(true)); + + // Return Arquillian Test Archive for application server + return jar; + } + + /** + * Test that the JSON Patch add operation works as intended. + */ + @Test + public void addTest() { + // Create a patch that adds some object members, replaces the value of an already existing object member, and + // adds some extra elements to an array + JsonPatch patch = Json.createPatchBuilder() + .add("/Timey", "Wimey") + .add("/Replaced", true) + .add("/FunnyReference", true) + .add("/FunScore", 100) + .add("/Lexicon/2", "Toddles") + .add("/Lexicon/2", "Tiddles") + .build(); + + // Apply the patch + JsonObject patchedJson = patch.apply(json); + + // Print out to more easily see what we've done + System.out.println("JsonpPatchTest.addTest: Before Patch: " + json); + System.out.println("JsonpPatchTest.addTest: After Patch: " + patchedJson); + + // Test that everything is as it should be + assertTrue("Patched JSON doesn't match!", patchedJson.getString("Wibbly").equals("Wobbly")); + assertTrue("Patched JSON doesn't match!", patchedJson.getString("Timey").equals("Wimey")); + assertTrue("Patched JSON doesn't match!", patchedJson.getBoolean("FunnyReference")); + assertTrue("Patched JSON doesn't match!", patchedJson.getInt("FunScore") == 100); + assertTrue("Patched JSON doesn't match!", patchedJson.getBoolean("Replaced")); + assertTrue("Patched JSON doesn't match!", patchedJson.getJsonArray("Lexicon").getString(0) + .equals("Wibbles")); + assertTrue("Patched JSON doesn't match!", patchedJson.getJsonArray("Lexicon").getString(1) + .equals("Wobbles")); + assertTrue("Patched JSON doesn't match!", patchedJson.getJsonArray("Lexicon").getString(2) + .equals("Tiddles")); + assertTrue("Patched JSON doesn't match!", patchedJson.getJsonArray("Lexicon").getString(3) + .equals("Toddles")); + } + + /** + * Test that the JSON Patch remove operation works as intended. + */ + @Test + public void removeTest() { + // Create a patch that removes an object member and an array element + JsonPatch patch = Json.createPatchBuilder() + .remove("/Replaced") + .remove("/Lexicon/1") + .build(); + + // Apply the patch + JsonObject patchedJson = patch.apply(json); + + // Print out to more easily see what we've done + System.out.println("JsonpPatchTest.removeTest: Before Patch: " + json); + System.out.println("JsonpPatchTest.removeTest: After Patch: " + patchedJson); + + assertTrue("Patched JSON still contains object member!", !patchedJson.containsKey("Replaced")); + assertTrue("Patched JSON still contains object member!", ((patchedJson.getJsonArray("Lexicon").size() == 1) + && (patchedJson.getJsonArray("Lexicon").getString(0).equals("Wibbles")))); + } + + /** + * Test that the JSON Patch replace operation works as intended. + */ + @Test + public void replaceTest() { + // Create a patch that replaces an object member and an array element + JsonPatch patch = Json.createPatchBuilder() + .replace("/Replaced", true) + .replace("/Lexicon/0", "Tiddles") + .replace("/Lexicon/1", "Toddles") + .build(); + + // Apply the patch + JsonObject patchedJson = patch.apply(json); + + // Print out to more easily see what we've done + System.out.println("JsonpPatchTest.replaceTest: Before Patch: " + json); + System.out.println("JsonpPatchTest.replaceTest: After Patch: " + patchedJson); + + assertTrue("Patched JSON still contains original value!", patchedJson.getBoolean("Replaced")); + assertTrue("Patched JSON still contains original values!", + ((patchedJson.getJsonArray("Lexicon").getString(0).equals("Tiddles")) + && (patchedJson.getJsonArray("Lexicon").getString(1).equals("Toddles")))); + } + + /** + * Test that the JSON Patch move operation works as intended. + */ + @Test + public void moveTest() { + // Create a patch that moves an object member, moves an array element, and reorders an array + JsonPatch patch = Json.createPatchBuilder() + .move("/Nested/Tibbly", "/Wibbly") + .move("/Nested/Bestiary/2", "/Lexicon/1") + .move("/Nested/Bestiary/3", "/Nested/Bestiary/0") + .build(); + + // Apply the patch + JsonObject patchedJson = patch.apply(json); + + // Print out to more easily see what we've done + System.out.println("JsonpPatchTest.moveTest: Before Patch: " + json); + System.out.println("JsonpPatchTest.moveTest: After Patch: " + patchedJson); + + assertTrue("Patched JSON hasn't moved value!", (!patchedJson.containsKey("Wibbly") + && patchedJson.getJsonObject("Nested").getString("Tibbly").equals("Wobbly"))); + assertTrue("Patched JSON hasn't moved value!", ((patchedJson.getJsonArray("Lexicon").size() == 1)) + && (patchedJson.getJsonObject("Nested").getJsonArray("Bestiary").getString(2).equals("Chimera")) + && (patchedJson.getJsonObject("Nested").getJsonArray("Bestiary").getString(0).equals("Werewolf"))); + } + + /** + * Test that the JSON Patch copy operation works as intended. + */ + @Test + public void copyTest() { + // Create a patch that copies an object member and an array element + JsonPatch patch = Json.createPatchBuilder() + .copy("/Nested/Tobbly", "/Wibbly") + .copy("/Nested/Bestiary/2", "/Lexicon/0") + .build(); + + // Apply the patch + JsonObject patchedJson = patch.apply(json); + + // Print out to more easily see what we've done + System.out.println("JsonpPatchTest.copyTest: Before Patch: " + json); + System.out.println("JsonpPatchTest.copyTest: After Patch: " + patchedJson); + + assertTrue("Patched JSON hasn't moved value!", (patchedJson.containsKey("Wibbly") + && patchedJson.getJsonObject("Nested").getString("Tobbly").equals("Wobbly"))); + assertTrue("Patched JSON hasn't moved value!", ((patchedJson.getJsonArray("Lexicon").size() == 2)) + && (patchedJson.getJsonObject("Nested").getJsonArray("Bestiary").getString(2).equals("Wibbles"))); + } + + /** + * Test that the JSON Patch test operation works as intended. + */ + @Test + public void testTest() { + // Create a patch that should test positive + JsonPatch patch = Json.createPatchBuilder() + .test("/Wibbly", "Wobbly") + .test("/Lexicon/0", "Wibbles") + .build(); + + // Apply the patch + JsonObject patchedJson = patch.apply(json); + + // Create a patch that should fail + patch = Json.createPatchBuilder() + .test("/Wibbly", "Tobbly") + .build(); + + try { + patchedJson = patch.apply(json); + } catch (JsonException ex) { + return; + } + + Assert.fail("Should have caught a JsonException and exited!"); + } + + // TODO: Ignore unrecognised elements; Adding to a non-existent target; Adding a nested member object; Error Handling +} diff --git a/json-p/pointer/pom.xml b/json-p/pointer/pom.xml new file mode 100644 index 00000000..3c195d55 --- /dev/null +++ b/json-p/pointer/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + + org.javaee8 + json-p + 1.0-SNAPSHOT + + + pointer + Java EE 8 Samples: JSON-P - Pointer + \ No newline at end of file diff --git a/json-p/pointer/src/test/java/org/javaee8/jsonp/pointer/JsonpPointerTest.java b/json-p/pointer/src/test/java/org/javaee8/jsonp/pointer/JsonpPointerTest.java new file mode 100644 index 00000000..20377bf9 --- /dev/null +++ b/json-p/pointer/src/test/java/org/javaee8/jsonp/pointer/JsonpPointerTest.java @@ -0,0 +1,169 @@ +package org.javaee8.jsonp.pointer; + +import java.math.BigDecimal; +import javax.json.Json; +import javax.json.JsonMergePatch; +import javax.json.JsonObject; +import javax.json.JsonPointer; +import javax.json.JsonValue; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Class that tests and demonstrates the JSON-P 1.1 Pointer Operations. + * @author Andrew Pielage + */ +@RunWith(Arquillian.class) +public class JsonpPointerTest { + + // Create a JsonObject with some values to be used in each test + private static final JsonObject json = Json.createObjectBuilder() + .add("Wibbly", "Wobbly") + .add("Replaced", false) + .add("Lexicon", Json.createArrayBuilder() + .add("Wibbles") + .add("Wobbles") + .build()) + .add("Nested", Json.createObjectBuilder() + .add("Birdie", "Wordie") + .add("Bestiary", Json.createArrayBuilder() + .add("Drowner") + .add("Werewolf") + .add("Chimera") + .build()) + .build()) + .build(); + + @Deployment + public static JavaArchive createDeployment() { + // Create a JavaArchive to deploy + JavaArchive jar = ShrinkWrap.create(JavaArchive.class); + + // Print out directory contents + System.out.println(jar.toString(true)); + + // Return Arquillian Test Archive for application server + return jar; + } + + /** + * Test that the JSON Pointers resolve to the correct values as. + */ + @Test + public void resolveTest() { + // Create pointers + JsonPointer objectPointer = Json.createPointer(""); + JsonPointer objectMemberPointer = Json.createPointer("/Wibbly"); + JsonPointer arrayPointer = Json.createPointer("/Lexicon"); + JsonPointer arrayElementPointer = Json.createPointer("/Lexicon/0"); + JsonPointer nestedObjectPointer = Json.createPointer("/Nested"); + JsonPointer nestedObjectMemberPointer = Json.createPointer("/Nested/Birdie"); + JsonPointer nestedArrayPointer = Json.createPointer("/Nested/Bestiary"); + JsonPointer nestedArrayElementPointer = Json.createPointer("/Nested/Bestiary/1"); + + // Check pointers return the correct values + assertTrue("objectPointer doesn't resolve correctly!", objectPointer.getValue(json).equals(json)); + assertTrue("objectMemberPointer doesn't resolve correctly!", + objectMemberPointer.getValue(json).toString().equals("\"Wobbly\"")); + assertTrue("arrayPointer doesn't resolve correctly!", + arrayPointer.getValue(json).equals(json.getJsonArray("Lexicon"))); + assertTrue("arrayElementPointer doesn't resolve correctly!", + arrayElementPointer.getValue(json).toString().equals("\"Wibbles\"")); + assertTrue("nestedObjectPointer doesn't resolve correctly!", + nestedObjectPointer.getValue(json).equals(json.getJsonObject("Nested"))); + assertTrue("nestedObjectMemberPointer doesn't resolve correctly!", + nestedObjectMemberPointer.getValue(json).toString().equals("\"Wordie\"")); + assertTrue("nestedArrayPointer doesn't resolve correctly!", + nestedArrayPointer.getValue(json).equals(json.getJsonObject("Nested").getJsonArray("Bestiary"))); + assertTrue("nestedArrayElementPointer doesn't resolve correctly!", + nestedArrayElementPointer.getValue(json).toString().equals("\"Werewolf\"")); + + // Check alternative notation + assertTrue("objectMemberPointer doesn't resolve correctly!", json.getValue("/Wibbly").toString().equals("\"Wobbly\"")); + assertTrue("objectMemberPointer doesn't resolve correctly!", + json.getValue("/Nested/Bestiary/2").toString().equals("\"Chimera\"")); + } + + /** + * Test that the JSON Pointer add operation works as expected. + */ + @Test + public void addTest() { + // Create pointers + JsonPointer objectMemberPointer = Json.createPointer("/Giggly"); + JsonPointer arrayElementPointer = Json.createPointer("/Lexicon/0"); + JsonPointer nestedObjectMemberPointer = Json.createPointer("/Nested/Purdie"); + JsonPointer nestedArrayElementPointer = Json.createPointer("/Nested/Bestiary/1"); + + // Perform an add operation on each pointer + JsonObject modifiedJson = objectMemberPointer.add(json, Json.createValue("Goggly")); + modifiedJson = arrayElementPointer.add(modifiedJson, Json.createValue("Giggles")); + modifiedJson = nestedObjectMemberPointer.add(modifiedJson, Json.createValue("Furdie")); + modifiedJson = nestedArrayElementPointer.add(modifiedJson, Json.createValue("Wraith")); + + // Check that they've been added + assertTrue("Object member not added!", modifiedJson.containsKey("Giggly") + && modifiedJson.getString("Giggly").equals("Goggly")); + assertTrue("Array element not added!", modifiedJson.getJsonArray("Lexicon").size() == 3 + && modifiedJson.getJsonArray("Lexicon").getString(0).equals("Giggles")); + assertTrue("Nested object member not added!", modifiedJson.getJsonObject("Nested").containsKey("Purdie") + && modifiedJson.getJsonObject("Nested").getString("Purdie").equals("Furdie")); + assertTrue("Nested array element not added!", + modifiedJson.getJsonObject("Nested").getJsonArray("Bestiary").size() == 4 + && modifiedJson.getJsonObject("Nested").getJsonArray("Bestiary").getString(1).equals("Wraith")); + } + + /** + * Test that the JSON Pointer remove operation works as expected. + */ + @Test + public void removeTest() { + // Create pointers + JsonPointer objectMemberPointer = Json.createPointer("/Wibbly"); + JsonPointer arrayElementPointer = Json.createPointer("/Lexicon/0"); + JsonPointer nestedObjectMemberPointer = Json.createPointer("/Nested/Birdie"); + JsonPointer nestedArrayPointer = Json.createPointer("/Nested/Bestiary"); + + // Perform a remove operation using each pointer + JsonObject modifiedJson = objectMemberPointer.remove(json); + modifiedJson = arrayElementPointer.remove(modifiedJson); + modifiedJson = nestedObjectMemberPointer.remove(modifiedJson); + modifiedJson = nestedArrayPointer.remove(modifiedJson); + + // Check that they've been removed + assertTrue("Object member not removed!", !modifiedJson.containsKey("Wibbly")); + assertTrue("Array element not removed!", modifiedJson.getJsonArray("Lexicon").size() == 1 + && !modifiedJson.getJsonArray("Lexicon").getString(0).equals("Drowner")); + assertTrue("Nested object member not removed!", !modifiedJson.getJsonObject("Nested").containsKey("Birdie")); + assertTrue("Nested array not removed!", !modifiedJson.getJsonObject("Nested").containsKey("Bestiary")); + } + + /** + * Test that the JSON Pointer replace operation works as expected. + */ + @Test + public void replaceTest() { + // Create pointers + JsonPointer objectMemberPointer = Json.createPointer("/Wibbly"); + JsonPointer arrayElementPointer = Json.createPointer("/Lexicon/0"); + JsonPointer nestedObjectMemberPointer = Json.createPointer("/Nested/Birdie"); + + // Perform a replace operation using each pointer + JsonObject modifiedJson = objectMemberPointer.replace(json, Json.createValue("Bobbly")); + modifiedJson = arrayElementPointer.replace(modifiedJson, Json.createValue("Tiddles")); + modifiedJson = nestedObjectMemberPointer.replace(modifiedJson, Json.createValue("Bubbly")); + + // Check that they've been replaced + assertTrue("Object member not added!", modifiedJson.containsKey("Wibbly") + && modifiedJson.getString("Wibbly").equals("Bobbly")); + assertTrue("Array element not added!", modifiedJson.getJsonArray("Lexicon").size() == 2 + && modifiedJson.getJsonArray("Lexicon").getString(0).equals("Tiddles")); + assertTrue("Nested object member not added!", modifiedJson.getJsonObject("Nested").containsKey("Birdie") + && modifiedJson.getJsonObject("Nested").getString("Birdie").equals("Bubbly")); + } +} diff --git a/json-p/pom.xml b/json-p/pom.xml new file mode 100644 index 00000000..27cc72e3 --- /dev/null +++ b/json-p/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + json-p + pom + Java EE 8 Samples: JSON-P + + + patch + pointer + merge + + + + + org.javaee8 + test-utils + ${project.version} + + + + \ No newline at end of file diff --git a/jsonb/mapping/pom.xml b/jsonb/mapping/pom.xml new file mode 100644 index 00000000..7904debd --- /dev/null +++ b/jsonb/mapping/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + + + org.javaee8 + jsonb + 1.0-SNAPSHOT + + + mapping + war + Java EE 8 Samples: JSON-B - Mapping + + diff --git a/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/controller/ApplicationConfig.java b/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/controller/ApplicationConfig.java new file mode 100644 index 00000000..b285fee9 --- /dev/null +++ b/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/controller/ApplicationConfig.java @@ -0,0 +1,26 @@ +package org.javaee8.jsonb.mapping.controller; + +import java.util.HashSet; +import java.util.Set; +import javax.ws.rs.core.Application; + +/** + * + * @author Gaurav Gupta + * + */ +@javax.ws.rs.ApplicationPath("resources") +public class ApplicationConfig extends Application { + + @Override + public Set> getClasses() { + Set> resources = new HashSet<>(); + addRestResourceClasses(resources); + return resources; + } + + private void addRestResourceClasses(Set> resources) { + resources.add(org.javaee8.jsonb.mapping.controller.PersonController.class); + } + +} diff --git a/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/controller/PersonController.java b/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/controller/PersonController.java new file mode 100644 index 00000000..c155dda3 --- /dev/null +++ b/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/controller/PersonController.java @@ -0,0 +1,40 @@ +package org.javaee8.jsonb.mapping.controller; + +import org.javaee8.jsonb.mapping.domain.Person; +import java.util.Arrays; +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +/** + * + * @author Gaurav Gupta + */ +@Path("/api/person") +public class PersonController { + + /** + * GET : get all the people. + * + * @return the Response with status 200 (OK) and the list of people in body + * + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getAllPeople() { + Person person1 = new Person(); + person1.setName("Ondrej"); + person1.setAddress("Prague"); + person1.setPin("Mihalyi"); + + Person person2 = new Person(); + person2.setName("Mert"); + person2.setAddress("Turkey"); + person2.setPin("Caliskan"); + + return Arrays.asList(person1, person2); + } + +} diff --git a/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/domain/Person.java b/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/domain/Person.java new file mode 100644 index 00000000..430e27ea --- /dev/null +++ b/jsonb/mapping/src/main/java/org/javaee8/jsonb/mapping/domain/Person.java @@ -0,0 +1,54 @@ +package org.javaee8.jsonb.mapping.domain; + +import java.io.Serializable; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbTransient; + +/** + * + * @author Gaurav Gupta + * + */ +public class Person implements Serializable { + + /** + * JsonbProperty is used to change name of one particular property. + * Property 'name' will be serialized to 'pname' property + */ + @JsonbProperty("pname") + private String name; + + private String address; + + /** + * Property 'pin' will be ignored by JSON Binding engine + */ + @JsonbTransient + private String pin; + + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return this.address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getPin() { + return pin; + } + + public void setPin(String pin) { + this.pin = pin; + } + +} \ No newline at end of file diff --git a/jsonb/mapping/src/test/java/org/javaee8/jsonb/mapping/controller/PersonControllerTest.java b/jsonb/mapping/src/test/java/org/javaee8/jsonb/mapping/controller/PersonControllerTest.java new file mode 100644 index 00000000..8c6df0dc --- /dev/null +++ b/jsonb/mapping/src/test/java/org/javaee8/jsonb/mapping/controller/PersonControllerTest.java @@ -0,0 +1,63 @@ +package org.javaee8.jsonb.mapping.controller; + +import org.javaee8.jsonb.mapping.controller.ApplicationConfig; +import org.javaee8.jsonb.mapping.controller.PersonController; +import java.net.URI; +import java.net.URL; +import java.util.List; +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import org.javaee8.jsonb.mapping.domain.Person; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.junit.Test; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.runner.RunWith; + +/** + * + * @author Gaurav Gupta + * + */ +@RunWith(Arquillian.class) +public class PersonControllerTest { + + private static final String RESOURCE_PATH = "api/person"; + + @ArquillianResource + private URL base; + + private static WebTarget target; + + @Deployment + public static WebArchive createDeployment() { + return ShrinkWrap.create(WebArchive.class) + .addClass(Person.class) + .addClass(PersonController.class) + .addClass(ApplicationConfig.class); + } + + @Before + public void setUpClass() throws Exception { + Client client = ClientBuilder.newClient(); + target = client.target(URI.create(new URL(base, "resources/").toExternalForm())); + } + + @Test + public void testJSONB() throws Exception { + Jsonb jsonb = JsonbBuilder.create(); + // Get all the people + Response response = target.path(RESOURCE_PATH).request().get(); + String val = jsonb.toJson(response.readEntity(List.class)); + assertEquals("[{\"address\":\"Prague\",\"pname\":\"Ondrej\"},{\"address\":\"Turkey\",\"pname\":\"Mert\"}]", val); + } + +} diff --git a/jsonb/pom.xml b/jsonb/pom.xml new file mode 100644 index 00000000..6a00f103 --- /dev/null +++ b/jsonb/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + jsonb + pom + Java EE 8 Samples: JSON-B + + + mapping + + + + + org.javaee8 + test-utils + ${project.version} + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 541a37c9..1c2aa1f5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,28 +1,115 @@ + + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.javaee8 - javaee8-samples + samples-parent 1.0-SNAPSHOT pom - Java EE 8 Samples + + Java EE 8 Samples: root 1.8 3.0.0 UTF-8 - - 1.3.1 + false + false + + ${skipTests} + ${skipTests} + ${skipTests} + ${skipTests} + + + 5.183 + 5.183 + 5.0 + 9.0.12 + 2.3.0.Final - - ${maven.min.version} - + + + central + Central Repository + + https://repo.maven.apache.org/maven2 + + + true + + + false + + + + + payara-milestones + Payara Milestones + https://raw.github.com/payara/Payara_PatchedProjects/master + + true + + + false + + + + + ossrh + Sonatype-snapshot + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + + jvnet-nexus-staging + Java.net Staging Repositories + https://maven.java.net/content/repositories/staging + + true + + + false + + + + + jvnet-nexus-promoted + Java.net Promoted Repositories + https://maven.java.net/content/repositories/promoted/ + + true + + + false + + + + + + test-utils + servlet + jaxrs + security + jsf cdi + jpa + jsonb + validation + json-p @@ -30,26 +117,43 @@ org.jboss.arquillian arquillian-bom - 1.1.5.Final + 1.4.1.Final import pom + + org.jboss.arquillian.container + arquillian-container-test-api + 1.4.1.Final + + + com.h2database + h2 + 1.4.197 + + + fish.payara.arquillian + payara-client-ee8 + 1.0.Beta3 + test + + javax javaee-api - 7.0 + 8.0 provided - + + - org.jboss.weld - weld-api - 3.0.Alpha3 - provided + javax.annotation + javax.annotation-api + 1.3.1 @@ -57,7 +161,19 @@ junit junit - 4.11 + 4.12 + test + + + org.hamcrest + hamcrest-core + 2.1 + test + + + org.hamcrest + hamcrest-library + 2.1 test @@ -65,70 +181,137 @@ arquillian-junit-container test + + org.jboss.arquillian.protocol + arquillian-protocol-servlet + test + org.jboss.shrinkwrap.resolver shrinkwrap-resolver-impl-maven test + jar org.jboss.shrinkwrap.resolver shrinkwrap-resolver-impl-maven-archive test + + xmlunit + xmlunit + 1.6 + test + + + org.skyscreamer + jsonassert + 1.5.0 + test + + + net.sourceforge.htmlunit + htmlunit + 2.33 + test + + + rhino + js + 1.7R2 + test + + + org.json + json + 20180813 + test + + + com.jayway.awaitility + awaitility + 1.7.0 + test + + + jakarta.xml.bind + jakarta.xml.bind-api + 2.3.2 + + + org.glassfish.jaxb + jaxb-runtime + 2.3.2 + + ${project.artifactId} + src/test/resources true + org.apache.maven.plugins maven-compiler-plugin - 3.1 - - - org.apache.maven.plugins - maven-surefire-plugin - 2.17 - - - default-test - test - - test - - - + 3.8.0 + + ${java.min.version} + ${java.min.version} + org.apache.maven.plugins maven-surefire-report-plugin - 2.17 + 3.0.0-M3 true true + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M3 + + + javax.annotation + javax.annotation-api + 1.3.2 + + + org.apache.maven.plugins maven-war-plugin - 2.5 + 3.2.2 + + true + false + + org.apache.maven.plugins maven-enforcer-plugin + 3.0.0-M2 - At least Maven in version ${maven.min.version} is required. + At least Maven in version + ${maven.min.version} is + required. ${maven.min.version} - At least a JDK in version ${java.min.version} is required. + At least a JDK in version + ${java.min.version} is + required. ${java.min.version} @@ -142,84 +325,629 @@ - org.wildfly.plugins - wildfly-maven-plugin - 1.0.2.Final + org.apache.maven.plugins + maven-dependency-plugin + 3.1.1 - - - - - org.apache.maven.plugins - maven-enforcer-plugin - ${plugin.enforcer.version} - - - - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.18 - - true - true - + maven-resources-plugin + 3.1.0 - + + + + + + + + + - wildfly-remote-arquillian + payara-ci-managed + true + + + + fish.payara.arquillian + payara-client-ee8 + + + + + fish.payara.arquillian + arquillian-payara-server-4-managed + 1.0.Beta3-m3 + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack + process-test-classes + + unpack + + + ${session.executionRootDirectory}/target + ${session.executionRootDirectory}/target/dependency-maven-plugin-markers + + + fish.payara.distributions + payara + ${payara.version} + zip + false + ${session.executionRootDirectory}/target + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + javax.annotation + javax.annotation-api + 1.3.1 + + + + ${session.executionRootDirectory}/target/payara5 + + + + ${session.executionRootDirectory}/target/payara5 + payara-remote + payara + + + + + + + + + payara-embedded + + + fish.payara.extras + payara-embedded-all + ${payara.version} + test + + + org.glassfish + javax.json + 1.0.4 + test + + + org.glassfish.tyrus + tyrus-client + 1.13.1 + test + + + org.glassfish.tyrus + tyrus-container-grizzly-client + 1.13.1 + test + + + org.glassfish.jersey.core + jersey-client + 2.27 + test + + + + org.jboss.arquillian.container + arquillian-glassfish-embedded-3.1 + 1.0.0.Final + test + + + + + + src/test/resources + true + + + src/test/resources-glassfish-embedded + true + + + + + + + payara-remote + + + + fish.payara.arquillian + payara-client-ee8 + + + + + fish.payara.arquillian + arquillian-payara-server-4-remote + 1.0.Beta3-m3 + test + + + + + + maven-surefire-plugin + + + payara-remote + + + + + + + + + + payara-micro-managed + + + true + true + + + + + fish.payara.arquillian + payara-client-ee8 + + + + + fish.payara.arquillian + arquillian-payara-micro-5-managed + 1.0.Beta3-m4 + test + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + process-test-classes + + copy + + + + + fish.payara.extras + payara-micro + ${payara.micro.version} + false + ${session.executionRootDirectory}/target/ + payara-micro-${payara.micro.version}.jar + + + + + + + + + maven-surefire-plugin + + + ${session.executionRootDirectory}/target/payara-micro-${payara.micro.version}.jar + + + + + + + + + + + + + + glassfish-embedded + + + org.glassfish.main.extras + glassfish-embedded-all + ${glassfish.version} + test + + + org.glassfish + javax.json + 1.0.4 + test + + + org.glassfish.tyrus + tyrus-client + 1.3 + test + + + org.glassfish.tyrus + tyrus-container-grizzly-client + 1.3 + test + + + org.glassfish.jersey.core + jersey-client + 2.7 + test + + + org.jboss.arquillian.container + arquillian-glassfish-embedded-3.1 + 1.0.2 + test + + + + + + src/test/resources + true + + + src/test/resources-glassfish-embedded + true + + + + + + + glassfish-remote + + + org.glassfish + javax.json + 1.0.4 + test + + + org.glassfish.tyrus + tyrus-client + 1.3 + test + - io.undertow - undertow-websockets-jsr - 1.0.0.Beta33 + org.glassfish.tyrus + tyrus-container-grizzly-client + 1.3 test - + - org.wildfly - wildfly-arquillian-container-remote - 8.2.0.Final + org.jboss.arquillian.container + arquillian-glassfish-remote-3.1 + 1.0.2 test + + + maven-surefire-plugin + + + glassfish-remote + + + + src/test/resources true + + src/test/resources-glassfish-remote + true + + + + + + + + + tomcat-remote + + + true + true + true + true + + + + + org.jboss.arquillian.container + arquillian-tomcat-remote-7 + 1.1.0.Final + test + + + + + + + maven-surefire-plugin + + + tomcat-remote + + + + + + + + + + tomcat-ci-managed + + + true + true + true + true + + + + + + apache.public + https://repository.apache.org/content/repositories/public/ + + true + + + false + + + + + + apache.staging + https://repository.apache.org/content/repositories/staging/ + + true + + + false + + + + + + + org.jboss.arquillian.container + arquillian-tomcat-managed-7 + 1.1.0.Final + test + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack-tomcat + process-test-classes + + unpack + + + + + org.apache.tomcat + tomcat + ${tomcat.version} + zip + false + ${project.build.directory} + + + + + + unpack-tomcat-users + process-test-classes + + unpack + + + + + org.javaee8 + test-utils + ${project.version} + jar + true + ${project.build.directory}/apache-tomcat-${tomcat.version}/conf + tomcat-users.xml + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + tomcat-ci-managed + ${project.build.directory}/apache-tomcat-${tomcat.version} + + + + + + + + + + + thorntail + + + + + io.thorntail + bom + ${thorntail.version} + import + pom + + + + + + + + + fish.payara.arquillian + payara-client-ee8 + + + + io.thorntail + arquillian + test + + + + + + + + maven-surefire-plugin + + + + + arquillian-thorntail.xml + + + + + + + + + + + javadocs + + + javadocs + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.4 + + + sources + process-resources + + sources + resolve + + + javadoc + false + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.19.1 + + true + true + + + + + diff --git a/security/README.md b/security/README.md new file mode 100644 index 00000000..e13fbb0a --- /dev/null +++ b/security/README.md @@ -0,0 +1,10 @@ +# Java EE 8 Samples: Security 1.0# + +The [JSR 375](https://jcp.org/en/jsr/detail?id=375) specifies the first version of the Java EE Security API - Java EE Security 1.0. + +## Samples ## + + - dynamic-rememberme + - jwt + + diff --git a/security/dynamic-rememberme/pom.xml b/security/dynamic-rememberme/pom.xml new file mode 100644 index 00000000..50a7990b --- /dev/null +++ b/security/dynamic-rememberme/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + + + org.javaee8 + security + 1.0-SNAPSHOT + + + dynamic-rememberme + war + Java EE 8 Samples: Security - Dynamic Remember-me + + diff --git a/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/ApplicationInit.java b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/ApplicationInit.java new file mode 100644 index 00000000..4427b0ec --- /dev/null +++ b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/ApplicationInit.java @@ -0,0 +1,82 @@ +package org.javaee8.security.dynamic.rememberme; + +import static java.util.stream.Collectors.toSet; + +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.InterceptionFactory; +import javax.security.enterprise.authentication.mechanism.http.BasicAuthenticationMechanismDefinition; +import javax.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism; + +import org.javaee8.security.dynamic.rememberme.util.HttpAuthenticationMechanismWrapper; +import org.javaee8.security.dynamic.rememberme.util.RememberMeAnnotationLiteral; + +// Configure a basic authentication mechanism. +// In this sample we'll dynamically apply the @RememberMe annotation to this. +@BasicAuthenticationMechanismDefinition( + realmName="foo" +) + +@Alternative +@Priority(500) +@ApplicationScoped +public class ApplicationInit { + + private static final Logger logger = Logger.getLogger(ApplicationInit.class.getName()); + + @Produces + public HttpAuthenticationMechanism produce(InterceptionFactory interceptionFactory, BeanManager beanManager) { + + logger.info("Producing wrapped and dynamic proxied mechanism"); + + // Get a reference (instance) to the HttpAuthenticationMechanism that would have been + // used had we not provided an alternative here. + // + // In this sample that is the mechanism + // the container puts into service following the @BasicAuthenticationMechanismDefinition + // used above. + HttpAuthenticationMechanism mechanism = + createRef( + beanManager.resolve( + beanManager.getBeans(HttpAuthenticationMechanism.class) + .stream() + .filter(e -> !e.getBeanClass().equals(ApplicationInit.class)) + .collect(toSet())), beanManager); + + // We're telling the InterceptionFactory here to dynamically add the @RememberMeAnnotation + // annotation with the supplied values. + interceptionFactory.configure().add( + new RememberMeAnnotationLiteral( + 86400, "", + false, "", + true, "", + "JREMEMBERMEID", + true, "" + ) + ); + + // This will create a proxy as configured above around an instance of + // HttpAuthenticationMechanismWrapper that we provide. + + // Note that we provide an extra wrapper since unfortunately createInterceptedInstance + // says: + // "If the provided instance is an internal container construct (such as client proxy), non-portable behavior results." + return interceptionFactory.createInterceptedInstance( + new HttpAuthenticationMechanismWrapper(mechanism)); + } + + HttpAuthenticationMechanism createRef(Bean bean, BeanManager beanManager) { + return (HttpAuthenticationMechanism) + beanManager.getReference( + bean, + HttpAuthenticationMechanism.class, + beanManager.createCreationalContext(bean)); + } + +} diff --git a/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/Servlet.java b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/Servlet.java new file mode 100644 index 00000000..59775edd --- /dev/null +++ b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/Servlet.java @@ -0,0 +1,34 @@ +package org.javaee8.security.dynamic.rememberme; + +import java.io.IOException; + +import javax.annotation.security.DeclareRoles; +import javax.servlet.ServletException; +import javax.servlet.annotation.HttpConstraint; +import javax.servlet.annotation.ServletSecurity; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@DeclareRoles({ "architect", "admin" }) +@WebServlet("/servlet") +@ServletSecurity(@HttpConstraint(rolesAllowed = "architect")) +public class Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + String webName = null; + if (request.getUserPrincipal() != null) { + webName = request.getUserPrincipal().getName(); + } + + response.getWriter().write("web username: " + webName + "\n"); + response.getWriter().write("web user has role \"architect\": " + request.isUserInRole("architect") + "\n"); + + } + +} diff --git a/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/TestIdentityStore.java b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/TestIdentityStore.java new file mode 100644 index 00000000..56180e13 --- /dev/null +++ b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/TestIdentityStore.java @@ -0,0 +1,25 @@ +package org.javaee8.security.dynamic.rememberme; + +import static java.util.Arrays.asList; +import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT; + +import java.util.HashSet; + +import javax.enterprise.context.RequestScoped; +import javax.security.enterprise.credential.UsernamePasswordCredential; +import javax.security.enterprise.identitystore.CredentialValidationResult; +import javax.security.enterprise.identitystore.IdentityStore; + +@RequestScoped +public class TestIdentityStore implements IdentityStore { + + public CredentialValidationResult validate(UsernamePasswordCredential credential) { + + if (!(credential.getCaller().equals("test") && credential.getPassword().compareTo("pass"))) { + return INVALID_RESULT; + } + + return new CredentialValidationResult("test", new HashSet<>(asList("architect", "admin"))); + } + +} diff --git a/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/TestRememberMeIdentityStore.java b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/TestRememberMeIdentityStore.java new file mode 100644 index 00000000..ba2a2f76 --- /dev/null +++ b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/TestRememberMeIdentityStore.java @@ -0,0 +1,44 @@ +package org.javaee8.security.dynamic.rememberme; + + +import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import javax.enterprise.context.ApplicationScoped; +import javax.security.enterprise.CallerPrincipal; +import javax.security.enterprise.credential.RememberMeCredential; +import javax.security.enterprise.identitystore.CredentialValidationResult; +import javax.security.enterprise.identitystore.RememberMeIdentityStore; + +@ApplicationScoped +public class TestRememberMeIdentityStore implements RememberMeIdentityStore { + + private Map loginTokens = new ConcurrentHashMap<>(); + + @Override + public CredentialValidationResult validate(RememberMeCredential credential) { + if (!loginTokens.containsKey(credential.getToken())) { + return INVALID_RESULT; + } + + return loginTokens.get(credential.getToken()); + } + + @Override + public String generateLoginToken(CallerPrincipal callerPrincipal, Set groups) { + String token = UUID.randomUUID().toString(); + loginTokens.put(token, new CredentialValidationResult(callerPrincipal, groups)); + + return token; + } + + @Override + public void removeLoginToken(String token) { + loginTokens.remove(token); + } + +} diff --git a/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/util/HttpAuthenticationMechanismWrapper.java b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/util/HttpAuthenticationMechanismWrapper.java new file mode 100644 index 00000000..1a122e04 --- /dev/null +++ b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/util/HttpAuthenticationMechanismWrapper.java @@ -0,0 +1,42 @@ +package org.javaee8.security.dynamic.rememberme.util; + +import javax.security.enterprise.AuthenticationException; +import javax.security.enterprise.AuthenticationStatus; +import javax.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism; +import javax.security.enterprise.authentication.mechanism.http.HttpMessageContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class HttpAuthenticationMechanismWrapper implements HttpAuthenticationMechanism { + + private HttpAuthenticationMechanism httpAuthenticationMechanism; + + public HttpAuthenticationMechanismWrapper() { + } + + public HttpAuthenticationMechanismWrapper(HttpAuthenticationMechanism httpAuthenticationMechanism) { + this.httpAuthenticationMechanism = httpAuthenticationMechanism; + } + + HttpAuthenticationMechanism getWrapped() { + return httpAuthenticationMechanism; + } + + @Override + public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, + HttpMessageContext httpMessageContext) throws AuthenticationException { + return getWrapped().validateRequest(request, response, httpMessageContext); + } + + @Override + public AuthenticationStatus secureResponse(HttpServletRequest request, HttpServletResponse response, + HttpMessageContext httpMessageContext) throws AuthenticationException { + return getWrapped().secureResponse(request, response, httpMessageContext); + } + + @Override + public void cleanSubject(HttpServletRequest request, HttpServletResponse response, HttpMessageContext httpMessageContext) { + getWrapped().cleanSubject(request, response, httpMessageContext); + } + +} \ No newline at end of file diff --git a/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/util/RememberMeAnnotationLiteral.java b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/util/RememberMeAnnotationLiteral.java new file mode 100644 index 00000000..f00f6ef2 --- /dev/null +++ b/security/dynamic-rememberme/src/main/java/org/javaee8/security/dynamic/rememberme/util/RememberMeAnnotationLiteral.java @@ -0,0 +1,89 @@ +package org.javaee8.security.dynamic.rememberme.util; + +import javax.enterprise.util.AnnotationLiteral; +import javax.security.enterprise.authentication.mechanism.http.RememberMe; + +@SuppressWarnings("all") +public class RememberMeAnnotationLiteral extends AnnotationLiteral implements RememberMe { + + private static final long serialVersionUID = 1L; + + int cookieMaxAgeSeconds; + String cookieMaxAgeSecondsExpression; + boolean cookieSecureOnly; + String cookieSecureOnlyExpression; + boolean cookieHttpOnly; + String cookieHttpOnlyExpression; + String cookieName; + boolean isRememberMe; + String isRememberMeExpression; + + public RememberMeAnnotationLiteral( + + int cookieMaxAgeSeconds, + String cookieMaxAgeSecondsExpression, + boolean cookieSecureOnly, + String cookieSecureOnlyExpression, + boolean cookieHttpOnly, + String cookieHttpOnlyExpression, + String cookieName, + boolean isRememberMe, + String isRememberMeExpression + + ) { + + this.cookieMaxAgeSeconds = cookieMaxAgeSeconds; + this.cookieMaxAgeSecondsExpression = cookieMaxAgeSecondsExpression; + this.cookieSecureOnly = cookieSecureOnly; + this.cookieSecureOnlyExpression = cookieSecureOnlyExpression; + this.cookieHttpOnly = cookieHttpOnly; + this.cookieHttpOnlyExpression = cookieHttpOnlyExpression; + this.cookieName = cookieName; + this.isRememberMe = isRememberMe; + this.isRememberMeExpression = isRememberMeExpression; + } + + @Override + public boolean cookieHttpOnly() { + return cookieHttpOnly; + } + + @Override + public String cookieHttpOnlyExpression() { + return cookieHttpOnlyExpression; + } + + @Override + public int cookieMaxAgeSeconds() { + return cookieMaxAgeSeconds; + } + + @Override + public String cookieMaxAgeSecondsExpression() { + return cookieMaxAgeSecondsExpression; + } + + @Override + public boolean cookieSecureOnly() { + return cookieSecureOnly; + } + + @Override + public String cookieSecureOnlyExpression() { + return cookieSecureOnlyExpression; + } + + @Override + public String cookieName() { + return cookieName; + } + + public boolean isRememberMe() { + return isRememberMe; + } + + @Override + public String isRememberMeExpression() { + return isRememberMeExpression; + } +} \ No newline at end of file diff --git a/security/dynamic-rememberme/src/test/java/org/javaee8/security/dynamic/rememberme/DynamicRemembermeTest.java b/security/dynamic-rememberme/src/test/java/org/javaee8/security/dynamic/rememberme/DynamicRemembermeTest.java new file mode 100644 index 00000000..6730258e --- /dev/null +++ b/security/dynamic-rememberme/src/test/java/org/javaee8/security/dynamic/rememberme/DynamicRemembermeTest.java @@ -0,0 +1,160 @@ +package org.javaee8.security.dynamic.rememberme; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.URL; + +import org.apache.http.client.CredentialsProvider; +import org.javaee8.security.dynamic.rememberme.util.RememberMeAnnotationLiteral; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.gargoylesoftware.htmlunit.DefaultCredentialsProvider; +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.util.Cookie; + +/** + * @author Arjan Tijms + */ +@RunWith(Arquillian.class) +public class DynamicRemembermeTest { + + @ArquillianResource + private URL base; + + private WebClient webClient; + + @Deployment + public static WebArchive createDeployment() { + return + create(WebArchive.class) + .addClasses( + ApplicationInit.class, + TestIdentityStore.class, + TestRememberMeIdentityStore.class, + Servlet.class + ) + .addPackage( + RememberMeAnnotationLiteral.class.getPackage()) + .addAsWebInfResource("jboss-web.xml"); + } + + @Before + public void setup() { + webClient = new WebClient(); + webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); + } + + @After + public void teardown() { + webClient.close(); + } + + @Test + @RunAsClient + public void testNotAuthenticated() throws IOException { + Page page = webClient.getPage(base + "servlet"); + + assertEquals( + 401, + page.getWebResponse().getStatusCode()); + + assertTrue( + "Response did not contain the \"WWW-Authenticate\" header.", + page.getWebResponse().getResponseHeaderValue("WWW-Authenticate") != null); + } + + @Test + @RunAsClient + public void testAuthenticatedOnce() throws IOException { + + // 1. Request the resource with valid HTTP BASIC credentials + + CredentialsProvider originalCredentialsProvider = webClient.getCredentialsProvider(); + DefaultCredentialsProvider credentialsProvider = new DefaultCredentialsProvider(); + credentialsProvider.addCredentials("test", "pass"); + + webClient.setCredentialsProvider(credentialsProvider); + + Page page = webClient.getPage(base + "servlet"); + + // We should now get a 200 (OK) response + + assertEquals( + 200, + page.getWebResponse().getStatusCode()); + + String content = page.getWebResponse().getContentAsString(); + + assertTrue( + content.contains("web username: test")); + + assertTrue( + content.contains("web user has role \"architect\": true")); + + + // Because remember-me is used, we should have received the JREMEMBERMEID cookie + + Cookie cookie = webClient.getCookieManager().getCookie("JREMEMBERMEID"); + + assertNotNull(cookie); + + System.out.println("JREMEMBERMEID cookie: " + cookie); + + + + // 2. Request the resource without valid HTTP BASIC credentials + + webClient.setCredentialsProvider(originalCredentialsProvider); + + page = webClient.getPage(base + "servlet"); + + // Since our credentials have been exchanged for a token (residing within the cookie) + // we should still be able to get a 200 + + assertEquals(200, page.getWebResponse().getStatusCode()); + + content = page.getWebResponse().getContentAsString(); + + assertTrue( + content.contains("web username: test")); + + assertTrue( + content.contains("web user has role \"architect\": true")); + + System.out.println("\nContent:\n" + content + "\n"); + + + + // 3. Request the resource without valid HTTP BASIC credentials as well as without the cookie + + webClient.getCookieManager().removeCookie(cookie); + + page = webClient.getPage(base + "servlet"); + + // We should not get a 200 okay now but a 401 Unauthorized, and the server should + // ask for our credentials again + + assertEquals( + 401, + page.getWebResponse().getStatusCode()); + + assertTrue( + "Response did not contain the \"WWW-Authenticate\" header.", + page.getWebResponse().getResponseHeaderValue("WWW-Authenticate") != null); + + } + +} diff --git a/security/dynamic-rememberme/src/test/resources/jboss-web.xml b/security/dynamic-rememberme/src/test/resources/jboss-web.xml new file mode 100644 index 00000000..4a9ba91f --- /dev/null +++ b/security/dynamic-rememberme/src/test/resources/jboss-web.xml @@ -0,0 +1,6 @@ + + + + + jaspitest + \ No newline at end of file diff --git a/security/jwt/README.md b/security/jwt/README.md new file mode 100644 index 00000000..153b2220 --- /dev/null +++ b/security/jwt/README.md @@ -0,0 +1,33 @@ +# Java EE Security 1.0 JWT Example +Sample to demonstrate JWT integration with Java EE Security 1.0 (Soteria RI) + +### Sample Users +Username | Password | Roles +--- | --- | --- +payara | fish | ADMIN_ROLE, USER_ROLE +duke | secret | USER_ROLE + +### Login EndPoint +http://localhost:8080/security-jwt-example/api/auth/login?name=duke&password=secret&rememberme=false + +### Protected REST EndPoint + +EndPoint | HTTP Method | Roles Allowed +--- | --- | --- +http://localhost:8080/security-jwt-example/api/sample/read | GET | ANONYMOUS, USER_ROLE, ADMIN_ROLE +http://localhost:8080/security-jwt-example/api/sample/write | POST | USER_ROLE, ADMIN_ROLE +http://localhost:8080/security-jwt-example/api/sample/delete | DELETE | ADMIN_ROLE + + +#### rememberme=false + +Whenever the user wants to access a protected resource, the user agent send the JWT in the Authorization header using the Bearer schema. The content of the header should look like the following: + +`Authorization: Bearer ` + +This is a stateless authentication mechanism as the user state is never saved in server memory. +The server's protected routes will check for a valid JWT in the Authorization header, and if it's present, the user will be allowed to access protected resources. + +#### rememberme=true +Whenever the user wants to access a protected resource, the user agent would automatically include the JWT in the cookie with `JREMEMBERMEID` key. +It does not require state to be stored on the server because the JWT encapsulates everything the server needs to serve the request. \ No newline at end of file diff --git a/security/jwt/pom.xml b/security/jwt/pom.xml new file mode 100644 index 00000000..f45387f1 --- /dev/null +++ b/security/jwt/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + + org.javaee8 + security + 1.0-SNAPSHOT + + + jwt + war + Java EE 8 Samples: Security - JWT + + + 0.6.0 + + + + + io.jsonwebtoken + jjwt + ${version.jsonwebtoken} + + + + diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/AuthenticationIdentityStore.java b/security/jwt/src/main/java/org/javaee8/security/jwt/AuthenticationIdentityStore.java new file mode 100644 index 00000000..a5bc602d --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/AuthenticationIdentityStore.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt; + +import static java.util.Collections.singleton; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.security.enterprise.credential.Credential; +import javax.security.enterprise.credential.UsernamePasswordCredential; +import javax.security.enterprise.identitystore.CredentialValidationResult; +import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT; +import static javax.security.enterprise.identitystore.CredentialValidationResult.NOT_VALIDATED_RESULT; +import javax.security.enterprise.identitystore.IdentityStore; +import javax.security.enterprise.identitystore.IdentityStore.ValidationType; +import static javax.security.enterprise.identitystore.IdentityStore.ValidationType.VALIDATE; + +@RequestScoped +public class AuthenticationIdentityStore implements IdentityStore { + + private Map callerToPassword; + + @PostConstruct + public void init() { + callerToPassword = new HashMap<>(); + callerToPassword.put("payara", "fish"); + callerToPassword.put("duke", "secret"); + } + + @Override + public CredentialValidationResult validate(Credential credential) { + CredentialValidationResult result; + + if (credential instanceof UsernamePasswordCredential) { + UsernamePasswordCredential usernamePassword = (UsernamePasswordCredential) credential; + String expectedPW = callerToPassword.get(usernamePassword.getCaller()); + if (expectedPW != null && expectedPW.equals(usernamePassword.getPasswordAsString())) { + result = new CredentialValidationResult(usernamePassword.getCaller()); + } else { + result = INVALID_RESULT; + } + } else { + result = NOT_VALIDATED_RESULT; + } + return result; + } + + @Override + public Set validationTypes() { + return singleton(VALIDATE); + } +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/AuthorizationIdentityStore.java b/security/jwt/src/main/java/org/javaee8/security/jwt/AuthorizationIdentityStore.java new file mode 100644 index 00000000..8fecbeb0 --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/AuthorizationIdentityStore.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt; + +import static org.javaee8.security.jwt.Constants.ADMIN; +import static org.javaee8.security.jwt.Constants.USER; +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.security.enterprise.identitystore.CredentialValidationResult; +import javax.security.enterprise.identitystore.IdentityStore; +import javax.security.enterprise.identitystore.IdentityStore.ValidationType; +import static javax.security.enterprise.identitystore.IdentityStore.ValidationType.PROVIDE_GROUPS; + +@RequestScoped +public class AuthorizationIdentityStore implements IdentityStore { + + private Map> groupsPerCaller; + + @PostConstruct + public void init() { + groupsPerCaller = new HashMap<>(); + groupsPerCaller.put("payara", new HashSet<>(asList(USER, ADMIN))); + groupsPerCaller.put("duke", singleton(USER)); + } + + @Override + public Set getCallerGroups(CredentialValidationResult validationResult) { + Set result = groupsPerCaller.get(validationResult.getCallerPrincipal().getName()); + if (result == null) { + result = emptySet(); + } + return result; + } + + @Override + public Set validationTypes() { + return singleton(PROVIDE_GROUPS); + } + +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/Constants.java b/security/jwt/src/main/java/org/javaee8/security/jwt/Constants.java new file mode 100644 index 00000000..d7b1947f --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/Constants.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt; + +public final class Constants { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + public static final String BEARER = "Bearer "; + + public static final String ADMIN = "ROLE_ADMIN"; + + public static final String USER = "ROLE_USER"; + + public static final int REMEMBERME_VALIDITY_SECONDS = 24 * 60 * 60; //24 hours + + // Load the TokenProvider configuration[secretKey,tokenValidity] and REMEMBERME_VALIDITY_SECONDS from config +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/JWTAuthenticationMechanism.java b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTAuthenticationMechanism.java new file mode 100644 index 00000000..fbd7bbdc --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTAuthenticationMechanism.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt; + +import static org.javaee8.security.jwt.Constants.AUTHORIZATION_HEADER; +import static org.javaee8.security.jwt.Constants.BEARER; +import static org.javaee8.security.jwt.Constants.REMEMBERME_VALIDITY_SECONDS; +import io.jsonwebtoken.ExpiredJwtException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.security.enterprise.AuthenticationStatus; +import javax.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism; +import javax.security.enterprise.authentication.mechanism.http.HttpMessageContext; +import javax.security.enterprise.authentication.mechanism.http.RememberMe; +import javax.security.enterprise.credential.UsernamePasswordCredential; +import javax.security.enterprise.identitystore.CredentialValidationResult; +import javax.security.enterprise.identitystore.IdentityStoreHandler; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@RememberMe( + cookieMaxAgeSeconds = REMEMBERME_VALIDITY_SECONDS, + isRememberMeExpression = "self.isRememberMe(httpMessageContext)" +) +@RequestScoped +public class JWTAuthenticationMechanism implements HttpAuthenticationMechanism { + + private static final Logger LOGGER = Logger.getLogger(JWTAuthenticationMechanism.class.getName()); + + /** + * Access to the + * IdentityStore(AuthenticationIdentityStore,AuthorizationIdentityStore) is + * abstracted by the IdentityStoreHandler to allow for multiple identity + * stores to logically act as a single IdentityStore + */ + @Inject + private IdentityStoreHandler identityStoreHandler; + + @Inject + private TokenProvider tokenProvider; + + @Override + public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext context) { + + LOGGER.log(Level.INFO, "validateRequest: {0}", request.getRequestURI()); + // Get the (caller) name and password from the request + // NOTE: This is for the smallest possible example only. In practice + // putting the password in a request query parameter is highly insecure + String name = request.getParameter("name"); + String password = request.getParameter("password"); + String token = extractToken(context); + + if (name != null && password != null) { + LOGGER.log(Level.INFO, "credentials : {0}, {1}", new String[]{name, password}); + // validation of the credential using the identity store + CredentialValidationResult result = identityStoreHandler.validate(new UsernamePasswordCredential(name, password)); + if (result.getStatus() == CredentialValidationResult.Status.VALID) { + // Communicate the details of the authenticated user to the container and return SUCCESS. + return createToken(result, context); + } + // if the authentication failed, we return the unauthorized status in the http response + return context.responseUnauthorized(); + } else if (token != null) { + // validation of the jwt credential + return validateToken(token, context); + } else if (context.isProtected()) { + // A protected resource is a resource for which a constraint has been defined. + // if there are no credentials and the resource is protected, we response with unauthorized status + return context.responseUnauthorized(); + } + // there are no credentials AND the resource is not protected, + // SO Instructs the container to "do nothing" + return context.doNothing(); + } + + /** + * To validate the JWT token e.g Signature check, JWT claims + * check(expiration) etc + * + * @param token The JWT access tokens + * @param context + * @return the AuthenticationStatus to notify the container + */ + private AuthenticationStatus validateToken(String token, HttpMessageContext context) { + try { + if (tokenProvider.validateToken(token)) { + JWTCredential credential = tokenProvider.getCredential(token); + return context.notifyContainerAboutLogin(credential.getPrincipal(), credential.getAuthorities()); + } + // if token invalid, response with unauthorized status + return context.responseUnauthorized(); + } catch (ExpiredJwtException eje) { + LOGGER.log(Level.INFO, "Security exception for user {0} - {1}", new String[]{eje.getClaims().getSubject(), eje.getMessage()}); + return context.responseUnauthorized(); + } + } + + /** + * Create the JWT using CredentialValidationResult received from + * IdentityStoreHandler + * + * @param result the result from validation of UsernamePasswordCredential + * @param context + * @return the AuthenticationStatus to notify the container + */ + private AuthenticationStatus createToken(CredentialValidationResult result, HttpMessageContext context) { + if (!isRememberMe(context)) { + String jwt = tokenProvider.createToken(result.getCallerPrincipal().getName(), result.getCallerGroups(), false); + context.getResponse().setHeader(AUTHORIZATION_HEADER, BEARER + jwt); + } + return context.notifyContainerAboutLogin(result.getCallerPrincipal(), result.getCallerGroups()); + } + + /** + * To extract the JWT from Authorization HTTP header + * + * @param context + * @return The JWT access tokens + */ + private String extractToken(HttpMessageContext context) { + String authorizationHeader = context.getRequest().getHeader(AUTHORIZATION_HEADER); + if (authorizationHeader != null && authorizationHeader.startsWith(BEARER)) { + String token = authorizationHeader.substring(BEARER.length(), authorizationHeader.length()); + return token; + } + return null; + } + + /** + * this function invoked using RememberMe.isRememberMeExpression EL + * expression + * + * @param context + * @return The remember me flag + */ + public Boolean isRememberMe(HttpMessageContext context) { + return Boolean.valueOf(context.getRequest().getParameter("rememberme")); + } + +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/JWTCredential.java b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTCredential.java new file mode 100644 index 00000000..260b0517 --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTCredential.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt; + +import java.util.Set; +import javax.security.enterprise.credential.Credential; + +public class JWTCredential implements Credential { + + private final String principal; + private final Set authorities; + + public JWTCredential(String principal, Set authorities) { + this.principal = principal; + this.authorities = authorities; + } + + public String getPrincipal() { + return principal; + } + + public Set getAuthorities() { + return authorities; + } + +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/JWTRememberMeIdentityStore.java b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTRememberMeIdentityStore.java new file mode 100644 index 00000000..fc156c55 --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/JWTRememberMeIdentityStore.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt; + +import io.jsonwebtoken.ExpiredJwtException; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.security.enterprise.CallerPrincipal; +import javax.security.enterprise.credential.RememberMeCredential; +import javax.security.enterprise.identitystore.CredentialValidationResult; +import static javax.security.enterprise.identitystore.CredentialValidationResult.INVALID_RESULT; +import javax.security.enterprise.identitystore.RememberMeIdentityStore; + +@ApplicationScoped +public class JWTRememberMeIdentityStore implements RememberMeIdentityStore { + + private static final Logger LOGGER = Logger.getLogger(JWTRememberMeIdentityStore.class.getName()); + + @Inject + private TokenProvider tokenProvider; + + @Override + public CredentialValidationResult validate(RememberMeCredential rememberMeCredential) { + try { + if (tokenProvider.validateToken(rememberMeCredential.getToken())) { + JWTCredential credential = tokenProvider.getCredential(rememberMeCredential.getToken()); + return new CredentialValidationResult(credential.getPrincipal(), credential.getAuthorities()); + } + // if token invalid, response with invalid result status + return INVALID_RESULT; + } catch (ExpiredJwtException eje) { + LOGGER.log(Level.INFO, "Security exception for user {0} - {1}", new Object[]{eje.getClaims().getSubject(), eje.getMessage()}); + return INVALID_RESULT; + } + } + + @Override + public String generateLoginToken(CallerPrincipal callerPrincipal, Set groups) { + return tokenProvider.createToken(callerPrincipal.getName(), groups, true); + } + + @Override + public void removeLoginToken(String token) { + // Stateless authentication means at server side we don't maintain the state + } + +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/TokenProvider.java b/security/jwt/src/main/java/org/javaee8/security/jwt/TokenProvider.java new file mode 100644 index 00000000..216ee8c0 --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/TokenProvider.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt; + +import static org.javaee8.security.jwt.Constants.REMEMBERME_VALIDITY_SECONDS; +import io.jsonwebtoken.*; +import java.util.Arrays; +import java.util.Date; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import static java.util.stream.Collectors.joining; +import javax.annotation.PostConstruct; + +public class TokenProvider { + + private static final Logger LOGGER = Logger.getLogger(TokenProvider.class.getName()); + + private static final String AUTHORITIES_KEY = "auth"; + + private String secretKey; + + private long tokenValidity; + + private long tokenValidityForRememberMe; + + @PostConstruct + public void init() { + // load from config + this.secretKey = "my-secret-jwt-key"; + this.tokenValidity = TimeUnit.HOURS.toMillis(10); //10 hours + this.tokenValidityForRememberMe = TimeUnit.SECONDS.toMillis(REMEMBERME_VALIDITY_SECONDS); //24 hours + } + + public String createToken(String username, Set authorities, Boolean rememberMe) { + long now = (new Date()).getTime(); + long validity = rememberMe ? tokenValidityForRememberMe : tokenValidity; + + return Jwts.builder() + .setSubject(username) + .claim(AUTHORITIES_KEY, authorities.stream().collect(joining(","))) + .signWith(SignatureAlgorithm.HS512, secretKey) + .setExpiration(new Date(now + validity)) + .compact(); + } + + public JWTCredential getCredential(String token) { + Claims claims = Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + + Set authorities + = Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")) + .stream() + .collect(Collectors.toSet()); + + return new JWTCredential(claims.getSubject(), authorities); + } + + public boolean validateToken(String authToken) { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken); + return true; + } catch (SignatureException e) { + LOGGER.log(Level.INFO, "Invalid JWT signature: {0}", e.getMessage()); + return false; + } + } +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/rest/ApplicationConfig.java b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/ApplicationConfig.java new file mode 100644 index 00000000..bb39382a --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/ApplicationConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt.rest; + +import static org.javaee8.security.jwt.Constants.ADMIN; +import static org.javaee8.security.jwt.Constants.USER; +import javax.annotation.security.DeclareRoles; +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@DeclareRoles({ADMIN, USER}) +@ApplicationPath(value = "api") +public class ApplicationConfig extends Application { +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/rest/AuthController.java b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/AuthController.java new file mode 100644 index 00000000..6f1f2730 --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/AuthController.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt.rest; + +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.security.enterprise.SecurityContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; + +@Path("auth") +public class AuthController { + + private static final Logger LOGGER = Logger.getLogger(AuthController.class.getName()); + + @Inject + private SecurityContext securityContext; + + @GET + @Path("login") + public Response login() { + LOGGER.log(Level.INFO, "login"); + if (securityContext.getCallerPrincipal() != null) { + JsonObject result = Json.createObjectBuilder() + .add("user", securityContext.getCallerPrincipal().getName()) + .build(); + return Response.ok(result).build(); + } + return Response.status(UNAUTHORIZED).build(); + } + +} diff --git a/security/jwt/src/main/java/org/javaee8/security/jwt/rest/SampleController.java b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/SampleController.java new file mode 100644 index 00000000..7be05c97 --- /dev/null +++ b/security/jwt/src/main/java/org/javaee8/security/jwt/rest/SampleController.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt.rest; + +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.security.PermitAll; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.security.enterprise.SecurityContext; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +@Path("sample") +public class SampleController { + + private static final Logger LOGGER = Logger.getLogger(SampleController.class.getName()); + + @Inject + private SecurityContext securityContext; + + @GET + @Path("read") + @PermitAll + public Response read() { + LOGGER.log(Level.INFO, "read"); + JsonObject result = Json.createObjectBuilder() + .add("user", securityContext.getCallerPrincipal() != null + ? securityContext.getCallerPrincipal().getName() : "Anonymous") + .add("message", "Read resource") + .build(); + return Response.ok(result).build(); + } + + @POST + @Path("write") +// @RolesAllowed({USER, ADMIN}) + public Response write() { + LOGGER.log(Level.INFO, "write"); + JsonObject result = Json.createObjectBuilder() + .add("user", securityContext.getCallerPrincipal().getName()) + .add("message", "Write resource") + .build(); + return Response.ok(result).build(); + } + + @DELETE + @Path("delete") +// @RolesAllowed({ADMIN}) + public Response delete() { + LOGGER.log(Level.INFO, "delete"); + JsonObject result = Json.createObjectBuilder() + .add("user", securityContext.getCallerPrincipal().getName()) + .add("message", "Delete resource") + .build(); + return Response.ok(result).build(); + } +} diff --git a/security/jwt/src/test/java/org/javaee8/security/jwt/JwtTest.java b/security/jwt/src/test/java/org/javaee8/security/jwt/JwtTest.java new file mode 100644 index 00000000..4958a3fc --- /dev/null +++ b/security/jwt/src/test/java/org/javaee8/security/jwt/JwtTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2017 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package org.javaee8.security.jwt; + +import java.io.File; +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import java.io.IOException; +import java.net.URL; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import java.net.URISyntaxException; +import javax.ws.rs.client.ClientBuilder; +import static javax.ws.rs.client.Entity.json; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import static org.javaee8.security.jwt.Constants.AUTHORIZATION_HEADER; +import org.javaee8.security.jwt.rest.ApplicationConfig; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; +import org.jboss.shrinkwrap.resolver.api.maven.MavenResolverSystem; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author Gaurav Gupta + */ +@RunWith(Arquillian.class) +public class JwtTest { + + @ArquillianResource + private URL base; + + private WebTarget webTarget; + + @Deployment + public static WebArchive createDeployment() { + MavenResolverSystem RESOLVER = Maven.resolver(); + File[] jjwtFiles = RESOLVER.resolve("io.jsonwebtoken:jjwt:0.6.0").withTransitivity().asFile(); + + return create(WebArchive.class) + .addPackage(ApplicationConfig.class.getPackage()) + .addPackage(JWTCredential.class.getPackage()) + .addAsLibraries(jjwtFiles) + .setWebXML("web.xml") + .addAsWebInfResource("beans.xml") + .addAsWebInfResource("jboss-web.xml"); + } + + @Before + public void setup() throws URISyntaxException { + webTarget = ClientBuilder.newClient().target(base.toURI().toString() + "api/"); + } + + @Test + @RunAsClient + public void testNotAuthenticated() throws IOException { + Response response = webTarget + .path("auth/login") + .queryParam("name", "duke") + .queryParam("password", "invalid") + .queryParam("rememberme", "false") + .request() + .get(); + String authorizationHeader = response.getHeaderString(AUTHORIZATION_HEADER); + assertNull(authorizationHeader); + assertEquals( + 401, + response.getStatus()); + } + + @Test + @RunAsClient + public void testAuthenticatedWithoutRememberme() throws IOException { + Response response = webTarget + .path("auth/login") + .queryParam("name", "duke") + .queryParam("password", "secret") + .queryParam("rememberme", "false") + .request() + .get(); + String authorizationHeader = response.getHeaderString(AUTHORIZATION_HEADER); + assertNotNull(authorizationHeader); + assertEquals( + 200, + response.getStatus()); + + Response protectedResponse = webTarget + .path("sample/write") + .request() + .header(AUTHORIZATION_HEADER, authorizationHeader) + .post(json(null)); + assertEquals( + 200, + protectedResponse.getStatus()); + + Response adminResponse = webTarget + .path("sample/delete") + .request() + .header(AUTHORIZATION_HEADER, authorizationHeader) + .delete(); + //Only ROLE_ADMIN user can access + assertEquals( + 403, + adminResponse.getStatus()); + + } + + @Test + @RunAsClient + public void testAuthenticatedWithRememberme() throws IOException { + Response response = webTarget + .path("auth/login") + .queryParam("name", "payara") + .queryParam("password", "fish") + .queryParam("rememberme", "true") + .request() + .get(); + String token = response.getCookies().get("JREMEMBERMEID").getValue(); + assertNotNull(token); + assertEquals( + 200, + response.getStatus()); + + Response protectedResponse = webTarget + .path("sample/write") + .request() + .cookie("JREMEMBERMEID", token) + .post(json(null)); + assertEquals( + 200, + protectedResponse.getStatus()); + + Response adminResponse = webTarget + .path("sample/delete") + .request() + .cookie("JREMEMBERMEID", token) + .delete(); + assertEquals( + 200, + adminResponse.getStatus()); + + } +} diff --git a/security/jwt/src/test/resources/beans.xml b/security/jwt/src/test/resources/beans.xml new file mode 100644 index 00000000..b09e5921 --- /dev/null +++ b/security/jwt/src/test/resources/beans.xml @@ -0,0 +1,38 @@ + + + + diff --git a/security/jwt/src/test/resources/jboss-web.xml b/security/jwt/src/test/resources/jboss-web.xml new file mode 100644 index 00000000..4a9ba91f --- /dev/null +++ b/security/jwt/src/test/resources/jboss-web.xml @@ -0,0 +1,6 @@ + + + + + jaspitest + \ No newline at end of file diff --git a/security/jwt/src/test/resources/web.xml b/security/jwt/src/test/resources/web.xml new file mode 100644 index 00000000..dbfe954e --- /dev/null +++ b/security/jwt/src/test/resources/web.xml @@ -0,0 +1,56 @@ + + + + + + + Protected resources + /api/sample/write + + + ROLE_USER + ROLE_ADMIN + + + + + + Admin resources + /api/sample/delete + + + ROLE_ADMIN + + + diff --git a/security/pom.xml b/security/pom.xml new file mode 100644 index 00000000..7e7613c9 --- /dev/null +++ b/security/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + security + pom + + Java EE 8 Samples: Security + + + dynamic-rememberme + jwt + + + + + org.javaee8 + test-utils + ${project.version} + + + diff --git a/servlet/README.md b/servlet/README.md new file mode 100644 index 00000000..4d060afc --- /dev/null +++ b/servlet/README.md @@ -0,0 +1,9 @@ +# Java EE 8 Samples: Servlet 4.0# + +The [JSR 369](https://jcp.org/en/jsr/detail?id=369) specifies the next version of Java Servlets - Java Servlets 4.0. + +## Samples ## + + - mapping + + diff --git a/servlet/http2/pom.xml b/servlet/http2/pom.xml new file mode 100644 index 00000000..cc57a2e2 --- /dev/null +++ b/servlet/http2/pom.xml @@ -0,0 +1,508 @@ + + + 4.0.0 + + + org.javaee8 + servlet + 1.0-SNAPSHOT + + + servlet-http2 + war + + Java EE 8 Samples: Servlet - http2 + + + 9.4.28.v20200408 + + + + + + org.eclipse.jetty.http2 + http2-client + ${jetty-version} + test + + + org.eclipse.jetty.http2 + http2-common + ${jetty-version} + test + + + org.eclipse.jetty + jetty-alpn-openjdk8-client + ${jetty-version} + test + + + org.eclipse.jetty + jetty-alpn-java-client + ${jetty-version} + test + + + + + + + + alpn-when-jdk8 + + 1.8 + + + + 8.1.13.v20181017 + + ${settings.localRepository}/org/mortbay/jetty/alpn/alpn-boot/${alpn.version}/alpn-boot-${alpn.version}.jar + + -Xbootclasspath/p:${bootclasspathPrefix} + + + + + maven-surefire-plugin + + ${surefireArgLine} + + + + org.mortbay.jetty.alpn + alpn-boot + ${alpn.version} + + + + + + + + alpn-when-jdk8_05 + + 1.8.0_05 + + + 8.1.0.v20141016 + + + + alpn-when-jdk8_11 + + 1.8.0_11 + + + 8.1.0.v20141016 + + + + alpn-when-jdk8_20 + + 1.8.0_20 + + + 8.1.0.v20141016 + + + + alpn-when-jdk8_25 + + 1.8.0_25 + + + 8.1.2.v20141202 + + + + alpn-when-jdk8_31 + + 1.8.0_31 + + + 8.1.3.v20150130 + + + + alpn-when-jdk8_40 + + 1.8.0_40 + + + 8.1.3.v20150130 + + + + alpn-when-jdk8_45 + + 1.8.0_45 + + + 8.1.3.v20150130 + + + + alpn-when-jdk8_51 + + 1.8.0_51 + + + 8.1.4.v20150727 + + + + alpn-when-jdk8_60 + + 1.8.0_60 + + + 8.1.5.v20150921 + + + + alpn-when-jdk8_65 + + 1.8.0_65 + + + 8.1.6.v20151105 + + + + alpn-when-jdk8_66 + + 1.8.0_66 + + + 8.1.6.v20151105 + + + + alpn-when-jdk8_71 + + 1.8.0_71 + + + 8.1.7.v20160121 + + + + alpn-when-jdk8_72 + + 1.8.0_72 + + + 8.1.7.v20160121 + + + + alpn-when-jdk8_73 + + 1.8.0_73 + + + 8.1.7.v20160121 + + + + alpn-when-jdk8_74 + + 1.8.0_74 + + + 8.1.7.v20160121 + + + + alpn-when-jdk8_77 + + 1.8.0_77 + + + 8.1.7.v20160121 + + + + alpn-when-jdk8_91 + + 1.8.0_91 + + + 8.1.7.v20160121 + + + + alpn-when-jdk8_92 + + 1.8.0_92 + + + 8.1.8.v20160420 + + + + alpn-when-jdk8_101 + + 1.8.0_101 + + + 8.1.8.v20160420 + + + + alpn-when-jdk8_102 + + 1.8.0_102 + + + 8.1.9.v20160720 + + + + alpn-when-jdk8_111 + + 1.8.0_111 + + + 8.1.9.v20160720 + + + + alpn-when-jdk8_112 + + 1.8.0_112 + + + 8.1.9.v20160720 + + + + alpn-when-jdk8_121 + + 1.8.0_121 + + + 8.1.11.v20170118 + + + + alpn-when-jdk8_131 + + 1.8.0_131 + + + 8.1.11.v20170118 + + + + alpn-when-jdk8_141 + + 1.8.0_141 + + + 8.1.11.v20170118 + + + + alpn-when-jdk8_144 + + 1.8.0_144 + + + 8.1.11.v20170118 + + + + alpn-when-jdk8_151 + + 1.8.0_151 + + + 8.1.11.v20170118 + + + + alpn-when-jdk8_152 + + 1.8.0_152 + + + 8.1.11.v20170118 + + + + alpn-when-jdk8_161 + + 1.8.0_161 + + + 8.1.12.v20180117 + + + + alpn-when-jdk8_162 + + 1.8.0_162 + + + 8.1.12.v20180117 + + + + alpn-when-jdk8_171 + + 1.8.0_171 + + + 8.1.12.v20180117 + + + + alpn-when-jdk8_172 + + 1.8.0_172 + + + 8.1.12.v20180117 + + + + alpn-when-jdk8_181 + + 1.8.0_181 + + + 8.1.12.v20180117 + + + + alpn-when-jdk8_191 + + 1.8.0_191 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_192 + + 1.8.0_192 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_201 + + 1.8.0_201 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_202 + + 1.8.0_202 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_211 + + 1.8.0_211 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_212 + + 1.8.0_212 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_221 + + 1.8.0_221 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_222 + + 1.8.0_222 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_231 + + 1.8.0_231 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_232 + + 1.8.0_232 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_241 + + 1.8.0_241 + + + 8.1.13.v20181017 + + + + alpn-when-jdk8_242 + + 1.8.0_242 + + + 8.1.13.v20181017 + + + + + alpn-when-jdk8_251 + + 1.8.0_251 + + + + + + + alpn-when-jdk8_252 + + 1.8.0_252 + + + + + + + + diff --git a/servlet/http2/src/main/java/org/javaee8/servlet/http2/Servlet.java b/servlet/http2/src/main/java/org/javaee8/servlet/http2/Servlet.java new file mode 100644 index 00000000..f8cf4746 --- /dev/null +++ b/servlet/http2/src/main/java/org/javaee8/servlet/http2/Servlet.java @@ -0,0 +1,40 @@ +package org.javaee8.servlet.http2; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.PushBuilder; +import javax.ws.rs.core.MediaType; + +@WebServlet("/test") +public class Servlet extends HttpServlet { + + private static final long serialVersionUID = -3439982021784932020L; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType(MediaType.TEXT_HTML_TYPE.withCharset(UTF_8.name()).toString()); + response.setStatus(200); + + PushBuilder builder = request.newPushBuilder(); + + // If server push isn't supported, return that in the result. + if (builder == null) { + response.addHeader("protocol", "HTTP 1.1"); + response.getWriter().append("

The image below was sent normally using HTTP 1.1.

"); + } else { + response.addHeader("protocol", "HTTP/2"); + response.getWriter().append("

The image below was pushed using HTTP/2.

"); + builder.path("images/payara-logo.jpg").push(); + } + response.getWriter().append(""); + } + +} diff --git a/servlet/http2/src/main/webapp/WEB-INF/web.xml b/servlet/http2/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..ccc69f21 --- /dev/null +++ b/servlet/http2/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,6 @@ + + + + test + + diff --git a/servlet/http2/src/main/webapp/images/payara-logo.jpg b/servlet/http2/src/main/webapp/images/payara-logo.jpg new file mode 100644 index 00000000..7c22eae0 Binary files /dev/null and b/servlet/http2/src/main/webapp/images/payara-logo.jpg differ diff --git a/servlet/http2/src/test/java/org/javaee8/servlet/http2/Http2Test.java b/servlet/http2/src/test/java/org/javaee8/servlet/http2/Http2Test.java new file mode 100644 index 00000000..7c7dc907 --- /dev/null +++ b/servlet/http2/src/test/java/org/javaee8/servlet/http2/Http2Test.java @@ -0,0 +1,94 @@ +package org.javaee8.servlet.http2; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import java.io.File; +import java.net.URI; +import java.net.URL; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import org.glassfish.jersey.client.ClientConfig; +import org.hamcrest.Matchers; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + + +/** + * Test for the HTTP/2 and the JAX-RS client + */ +@RunWith(Arquillian.class) +public class Http2Test { + + @ArquillianResource + private URL basicUrl; + private Client jaxrsClient; + + + @Deployment + public static WebArchive createDeployment() { + final WebArchive war = create(WebArchive.class).addClasses(Servlet.class) + .addAsWebResource(new File("src/main/webapp/images/payara-logo.jpg"), "images/payara-logo.jpg") + .addAsWebInfResource(new File("src/main/webapp/WEB-INF/web.xml")) + .addAsResource("project-defaults.yml"); // only for Thormtail; + System.out.println("War file content: \n" + war.toString(true)); + return war; + } + + @Before + public void setup() throws Exception { + ClientConfig config = new ClientConfig(); + config.connectorProvider(JettyConnector::new); + jaxrsClient = ClientBuilder.newClient(config); + } + + @After + public void cleanUp() throws Exception { + jaxrsClient.close(); + } + + + /** + * This test runs against the public website supporting HTTP/2 + * + * @throws Exception + */ + @Test(timeout = 10000L) + @RunAsClient + public void testHttp2ControlGroup() throws Exception { + Response response = testUri(new URI("https://http2.akamai.com/")); + assertThat("myproto header", response.getHeaderString("myproto"), Matchers.equalTo("h2")); + } + + /** + * This test runs against our private website supporting HTTP/2 + * + * @throws Exception + */ + @Test(timeout = 10000L) + @RunAsClient + public void testServerHttp2() throws Exception { + Response response = testUri(basicUrl.toURI()); + // the header 'protocol' is set in the Servlet class. + assertThat( + "Request wasn't over HTTP/2. Either the wrong servlet was returned, or the server doesn't support HTTP/2.", + response.getHeaderString("protocol"), Matchers.equalTo("HTTP/2")); + } + + private Response testUri(URI uri) { + Response response = jaxrsClient.target(uri).request().get(); + assertNotNull("response", response); + return response; + } +} diff --git a/servlet/http2/src/test/java/org/javaee8/servlet/http2/JettyConnector.java b/servlet/http2/src/test/java/org/javaee8/servlet/http2/JettyConnector.java new file mode 100644 index 00000000..05858a21 --- /dev/null +++ b/servlet/http2/src/test/java/org/javaee8/servlet/http2/JettyConnector.java @@ -0,0 +1,177 @@ +package org.javaee8.servlet.http2; + +import java.io.ByteArrayInputStream; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; + +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Configuration; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpURI; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MetaData.Request; +import org.eclipse.jetty.http2.api.Session; +import org.eclipse.jetty.http2.api.Stream; +import org.eclipse.jetty.http2.api.server.ServerSessionListener; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.frames.DataFrame; +import org.eclipse.jetty.http2.frames.HeadersFrame; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.FuturePromise; +import org.eclipse.jetty.util.Jetty; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.message.internal.Statuses; + +public class JettyConnector implements Connector { + + private HTTP2Client client; + private SslContextFactory sslContextFactory; + + /** + * Needed for the JAX-RS connector creation. + */ + public JettyConnector(final Client jaxrsClient, final Configuration config) { + this(); + } + + public JettyConnector() { + this(Level.INFO); + } + + public JettyConnector(Level logLevel) { + System.setProperty("org.eclipse.jetty.client.LEVEL", logLevel.getName()); + client = new HTTP2Client(); + + // Configure SSL for test. Ignore insecure certificates + sslContextFactory = new SslContextFactory(true); + client.addBean(sslContextFactory); + + // Start client + try { + client.start(); + } catch (Exception e) { + throw new RuntimeException("Unable to start client."); + } + } + + @Override + public ClientResponse apply(ClientRequest request) { + + String host = request.getUri().getHost(); + int port = request.getUri().getPort(); + if (port == -1) { + port = 443; + } + boolean secure = request.getUri().getScheme().equals("https"); + + // Get the session + Session session = createSession(host, port, secure); + + // Create HTTP headers + HttpFields headers = new HttpFields(); + request.getStringHeaders().forEach((key, value) -> { + headers.put(key, value); + }); + headers.put("User-Agent", getName()); + headers.put("Host", host + ":" + port); + + // Create the request + Request jettyRequest = new Request(request.getMethod(), new HttpURI(request.getUri()), HttpVersion.HTTP_2, + headers); + + // Stored metadata + int status = 200; + StringBuilder entityStream = new StringBuilder(); + Map responseHeaders = new HashMap<>(); + + // Response listener + CountDownLatch latch = new CountDownLatch(1); + Stream.Listener responseListener = new Stream.Listener.Adapter() { + @Override + public void onHeaders(Stream stream, HeadersFrame frame) { + frame.getMetaData().getFields().forEach(field -> { + responseHeaders.put(field.getName(), field.getValue()); + }); + } + + @Override + public void onData(Stream stream, DataFrame frame, Callback callback) { + byte[] bytes = new byte[frame.getData().remaining()]; + frame.getData().get(bytes); + entityStream.append(new String(bytes)); + if (frame.isEndStream()) { + latch.countDown(); + callback.succeeded(); + } + } + }; + + // Make the connection + session.newStream(new HeadersFrame(jettyRequest, null, true), new FuturePromise<>(), responseListener); + + // Wait for response + try { + if (!latch.await(5, TimeUnit.SECONDS)) { + throw new RuntimeException("The request timed out. This usually means HTTP/2 isn't supported."); + } + } catch (InterruptedException e) { + throw new RuntimeException("Request interrupted."); + } + + // Build response from metadata + ClientResponse response = new ClientResponse(Statuses.from(status), request); + response.setEntityStream(new ByteArrayInputStream(entityStream.toString().getBytes())); + responseHeaders.forEach((key, value) -> { + response.header(key, value); + }); + return response; + } + + @Override + public Future apply(ClientRequest request, AsyncConnectorCallback callback) { + throw new UnsupportedOperationException("Unimplemented method."); + } + + @Override + public String getName() { + return client.getClass().getName() + "/" + Jetty.VERSION; + } + + @Override + public void close() { + try { + client.stop(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private Session createSession(String host, int port, boolean secure) { + FuturePromise sessionPromise = new FuturePromise<>(); + if (secure) { + client.connect(sslContextFactory, new InetSocketAddress(host, port), new ServerSessionListener.Adapter(), + sessionPromise); + } else { + client.connect(new InetSocketAddress(host, port), new ServerSessionListener.Adapter(), sessionPromise); + } + + try { + return sessionPromise.get(5, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + throw new IllegalStateException("Cloud not get a session.", e); + } + } + +} diff --git a/servlet/http2/src/test/resources/project-defaults.yml b/servlet/http2/src/test/resources/project-defaults.yml new file mode 100644 index 00000000..c6bc400f --- /dev/null +++ b/servlet/http2/src/test/resources/project-defaults.yml @@ -0,0 +1,8 @@ +thorntail: + undertow: + servers: + default-server: + http-listeners: + default: + # in Thorntail, HTTP/2 is by default only enabled for the HTTPS listener + enable-http2: true diff --git a/servlet/mapping/pom.xml b/servlet/mapping/pom.xml new file mode 100644 index 00000000..c298b851 --- /dev/null +++ b/servlet/mapping/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + + + org.javaee8 + servlet + 1.0-SNAPSHOT + + + servlet-mapping + war + Java EE 8 Samples: Servlet - mapping + + diff --git a/servlet/mapping/src/main/java/org/javaee8/servlet/mapping/Servlet.java b/servlet/mapping/src/main/java/org/javaee8/servlet/mapping/Servlet.java new file mode 100644 index 00000000..6eee3789 --- /dev/null +++ b/servlet/mapping/src/main/java/org/javaee8/servlet/mapping/Servlet.java @@ -0,0 +1,40 @@ +package org.javaee8.servlet.mapping; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletMapping; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Arjan Tijms + */ +@WebServlet({"/path/*", "*.ext", "", "/", "/exact"}) +public class Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + HttpServletMapping mapping = request.getHttpServletMapping(); + + System.out.println(mapping); + System.out.println(mapping.getMappingMatch()); + + response.getWriter() + .append("Mapping match:") + .append(mapping.getMappingMatch().name()) + .append("\n") + .append("Match value:'") + .append(mapping.getMatchValue()) + .append("'") + .append("\n") + .append("Pattern:'") + .append(mapping.getPattern()) + .append("'"); + } + +} diff --git a/servlet/mapping/src/test/java/org/javaee8/servlet/mapping/ServletMappingTest.java b/servlet/mapping/src/test/java/org/javaee8/servlet/mapping/ServletMappingTest.java new file mode 100644 index 00000000..923971fa --- /dev/null +++ b/servlet/mapping/src/test/java/org/javaee8/servlet/mapping/ServletMappingTest.java @@ -0,0 +1,128 @@ +package org.javaee8.servlet.mapping; + +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.net.URL; + +import javax.servlet.http.MappingMatch; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.gargoylesoftware.htmlunit.TextPage; +import com.gargoylesoftware.htmlunit.WebClient; + +/** + * @author Arjan Tijms + */ +@RunWith(Arquillian.class) +public class ServletMappingTest { + + @ArquillianResource + private URL base; + + private WebClient webClient; + + @Deployment + public static WebArchive createDeployment() { + return create(WebArchive.class) + .addClass(Servlet.class); + } + + @Before + public void setup() { + webClient = new WebClient(); + } + + @After + public void teardown() { + webClient.close(); + } + + @Test + @RunAsClient + public void testPath() throws IOException { + + // Test Servet is mapped to /path/*, so name after "/path/*" can be anything + TextPage page = webClient.getPage(base + "path/foo"); + String content = page.getContent(); + + System.out.println("\nContent for `"+ base + "path/foo" + "` :\n" + content + "\n"); + + assertTrue(content.contains("Mapping match:" + MappingMatch.PATH.name())); + assertTrue(content.contains("Match value:'foo'")); + assertTrue(content.contains("Pattern:'/path/*'")); + } + + @Test + @RunAsClient + public void testExtension() throws IOException { + + // Test Servet is mapped to *.ext, so name before ".ext" can be anything + TextPage page = webClient.getPage(base + "foo.ext"); + String content = page.getContent(); + + System.out.println("\nContent for `"+ base + "foo.ext" + "` :\n" + content + "\n"); + + assertTrue(content.contains("Mapping match:" + MappingMatch.EXTENSION.name())); + assertTrue(content.contains("Match value:'foo'")); + assertTrue(content.contains("Pattern:'*.ext'")); + } + + @Test + @RunAsClient + public void testRoot() throws IOException { + + // Test Servet is mapped to the root of the web application + TextPage page = webClient.getPage(base); + String content = page.getContent(); + + System.out.println("\nContent for `"+ base + "` :\n" + content + "\n"); + + assertTrue(content.contains("Mapping match:" + MappingMatch.CONTEXT_ROOT.name())); + assertTrue(content.contains("Match value:''")); + assertTrue(content.contains("Pattern:''")); + } + + @Test + @RunAsClient + public void testDefault() throws IOException { + + // Test Servet is mapped to the "default", which is a fallback if nothing else matches + TextPage page = webClient.getPage(base + "doesnotexist"); + String content = page.getContent(); + + System.out.println("\nContent for `"+ base + "doesnotexist" + "` :\n" + content + "\n"); + + assertTrue(content.contains("Mapping match:" + MappingMatch.DEFAULT.name())); + assertTrue(content.contains("Match value:''")); + assertTrue(content.contains("Pattern:'/'")); + } + + @Test + @RunAsClient + public void testExact() throws IOException { + + // Test Servet is mapped to an exact name ("/exact"), which is thus not a wildcard of any kind + TextPage page = webClient.getPage(base + "exact"); + String content = page.getContent(); + + System.out.println("\nContent for `"+ base + "exact" + "` :\n" + content + "\n"); + + assertTrue(content.contains("Mapping match:" + MappingMatch.EXACT.name())); + assertTrue(content.contains("Match value:'exact'")); + assertTrue(content.contains("Pattern:'/exact'")); + } + + + +} diff --git a/servlet/pom.xml b/servlet/pom.xml new file mode 100644 index 00000000..ee512e78 --- /dev/null +++ b/servlet/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + servlet + pom + + Java EE 8 Samples: Servlet + + + http2 + mapping + + + + + org.javaee8 + test-utils + ${project.version} + + + diff --git a/test-utils/pom.xml b/test-utils/pom.xml new file mode 100644 index 00000000..60a50f84 --- /dev/null +++ b/test-utils/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + + samples-parent + org.javaee8 + 1.0-SNAPSHOT + + + test-utils + Java EE 8 Samples: test-utils + + + + junit + junit + 4.12 + + + org.jboss.arquillian.container + arquillian-container-test-api + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-api-maven + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + true + + + + + + diff --git a/test-utils/src/main/java/org/javaee8/CliCommands.java b/test-utils/src/main/java/org/javaee8/CliCommands.java new file mode 100644 index 00000000..5a2dcccc --- /dev/null +++ b/test-utils/src/main/java/org/javaee8/CliCommands.java @@ -0,0 +1,130 @@ +package org.javaee8; + +import static java.lang.Runtime.getRuntime; +import static java.lang.Thread.currentThread; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.logging.Logger; + +/** + * Methods to execute "cli commands" against various servers. + * + * @author Arjan Tijms + * + */ +public class CliCommands { + + private static final Logger logger = Logger.getLogger(CliCommands.class.getName()); + private static final String OS = System.getProperty("os.name").toLowerCase(); + + public static int payaraGlassFish(List cliCommands) { + + String gfHome = System.getProperty("glassfishRemote_gfHome"); + if (gfHome == null) { + logger.info("glassfishRemote_gfHome not specified"); + return -1; + } + + Path gfHomePath = Paths.get(gfHome); + if (!gfHomePath.toFile().exists()) { + logger.severe("glassfishRemote_gfHome at " + gfHome + " does not exists"); + return -1; + } + + if (!gfHomePath.toFile().isDirectory()) { + logger.severe("glassfishRemote_gfHome at " + gfHome + " is not a directory"); + return -1; + } + + Path asadminPath = gfHomePath.resolve(isWindows()? "bin/asadmin.bat" : "bin/asadmin"); + + if (!asadminPath.toFile().exists()) { + logger.severe("asadmin command at " + asadminPath.toAbsolutePath() + " does not exists"); + return -1; + } + + List cmd = new ArrayList<>(); + + cmd.add(asadminPath.toAbsolutePath().toString()); + cmd.addAll(cliCommands); + + ProcessBuilder processBuilder = new ProcessBuilder(cmd); + processBuilder.redirectErrorStream(true); + + try { + return + waitToFinish( + readAllInput( + destroyAtShutDown( + processBuilder.start()))); + } catch (IOException e) { + return -1; + } + } + + public static Process destroyAtShutDown(final Process process) { + getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + if (process != null) { + process.destroy(); + try { + process.waitFor(); + } catch (InterruptedException e) { + currentThread().interrupt(); + throw new RuntimeException(e); + } + } + } + })); + + return process; + } + + public static Process readAllInput(Process process) { + // Read any output from the process + try (Scanner scanner = new Scanner(process.getInputStream())) { + while (scanner.hasNextLine()) { + System.out.println(scanner.nextLine()); + } + } + + return process; + } + + public static int waitToFinish(Process process) { + + // Wait up to 30s for the process to finish + int startupTimeout = 30 * 1000; + while (startupTimeout > 0) { + startupTimeout -= 200; + try { + Thread.sleep(200); + } catch (InterruptedException e1) { + // Ignore + } + + try { + int exitValue = process.exitValue(); + + System.out.println("Asadmin process exited with status " + exitValue); + return exitValue; + + } catch (IllegalThreadStateException e) { + // process is still running + } + } + + throw new IllegalStateException("Asadmin process seems stuck after waiting for 30 seconds"); + } + + public static boolean isWindows() { + return OS.contains("win"); + } + +} diff --git a/test-utils/src/main/java/org/javaee8/Libraries.java b/test-utils/src/main/java/org/javaee8/Libraries.java new file mode 100644 index 00000000..d6e4df82 --- /dev/null +++ b/test-utils/src/main/java/org/javaee8/Libraries.java @@ -0,0 +1,16 @@ +package org.javaee8; + +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.resolver.api.maven.Maven; + +public class Libraries { + + public static JavaArchive[] awaitability() { + return Maven.resolver() + .loadPomFromFile("pom.xml") + .resolve("org.assertj:assertj-core", "com.jayway.awaitility:awaitility") + .withTransitivity() + .as(JavaArchive.class); + } + +} diff --git a/test-utils/src/main/java/org/javaee8/Parameter.java b/test-utils/src/main/java/org/javaee8/Parameter.java new file mode 100644 index 00000000..6492d701 --- /dev/null +++ b/test-utils/src/main/java/org/javaee8/Parameter.java @@ -0,0 +1,12 @@ +package org.javaee8; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +public @interface Parameter { +} + diff --git a/test-utils/src/main/java/org/javaee8/ParameterRule.java b/test-utils/src/main/java/org/javaee8/ParameterRule.java new file mode 100644 index 00000000..ec91d986 --- /dev/null +++ b/test-utils/src/main/java/org/javaee8/ParameterRule.java @@ -0,0 +1,103 @@ +package org.javaee8; + +import org.jboss.arquillian.container.test.api.Deployment; + +import org.junit.rules.MethodRule; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Helper class for Parametrized tests as described here: + * http://blog.schauderhaft.de/2012/12/16/writing-parameterized-tests-with-junit-rules/ + * + * @param + */ +public class ParameterRule implements MethodRule { + private final List params; + + public ParameterRule(List params) { + if (params == null || params.size() == 0) { + throw new IllegalArgumentException("'params' must be specified and have more then zero length!"); + } + this.params = params; + } + + @Override + public Statement apply(final Statement base, final FrameworkMethod method, final Object target) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + boolean runInContainer = getDeploymentMethod(target).getAnnotation(Deployment.class).testable(); + if (runInContainer) { + evaluateParametersInContainer(base, target); + } else { + evaluateParametersInClient(base, target); + } + } + }; + } + + private Method getDeploymentMethod(Object target) throws NoSuchMethodException { + Method[] methods = target.getClass().getDeclaredMethods(); + for (Method method : methods) { + if (method.getAnnotation(Deployment.class) != null) return method; + } + throw new IllegalStateException("No method with @Deployment annotation found!"); + } + + private void evaluateParametersInContainer(Statement base, Object target) throws Throwable { + if (isRunningInContainer()) { + evaluateParamsToTarget(base, target); + } else { + ignoreStatementExecution(base); + } + } + + private void evaluateParametersInClient(Statement base, Object target) throws Throwable { + if (isRunningInContainer()) { + ignoreStatementExecution(base); + } else { + evaluateParamsToTarget(base, target); + } + } + + private boolean isRunningInContainer() { + try { + new InitialContext().lookup("java:comp/env"); + return true; + } catch (NamingException e) { + return false; + } + } + + private void evaluateParamsToTarget(Statement base, Object target) throws Throwable { + for (Object param : params) { + Field targetField = getTargetField(target); + if (!targetField.isAccessible()) { + targetField.setAccessible(true); + } + targetField.set(target, param); + base.evaluate(); + } + } + + private Field getTargetField(Object target) throws NoSuchFieldException { + Field[] allFields = target.getClass().getDeclaredFields(); + for (Field field : allFields) { + if (field.getAnnotation(Parameter.class) != null) return field; + } + throw new IllegalStateException("No field with @Parameter annotation found! Forgot to add it?"); + } + + private void ignoreStatementExecution(Statement base) { + try { + base.evaluate(); + } catch (Throwable ignored) {} + } +} \ No newline at end of file diff --git a/test-utils/src/main/java/org/javaee8/RemoteEJBContextFactory.java b/test-utils/src/main/java/org/javaee8/RemoteEJBContextFactory.java new file mode 100644 index 00000000..5ba2db36 --- /dev/null +++ b/test-utils/src/main/java/org/javaee8/RemoteEJBContextFactory.java @@ -0,0 +1,23 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8; + +import java.util.Iterator; +import java.util.ServiceLoader; + +public class RemoteEJBContextFactory { + + public static RemoteEJBContextProvider getProvider() { + + ServiceLoader loader = ServiceLoader.load(RemoteEJBContextProvider.class); + + Iterator providers = loader.iterator(); + + if (!providers.hasNext()) { + return null; + } + + return providers.next(); + + } + +} diff --git a/test-utils/src/main/java/org/javaee8/RemoteEJBContextProvider.java b/test-utils/src/main/java/org/javaee8/RemoteEJBContextProvider.java new file mode 100644 index 00000000..efcadded --- /dev/null +++ b/test-utils/src/main/java/org/javaee8/RemoteEJBContextProvider.java @@ -0,0 +1,9 @@ +/** Copyright Payara Services Limited **/ +package org.javaee8; + +import javax.naming.Context; + +public interface RemoteEJBContextProvider { + Context getContextWithCredentialsSet(String username, String password); + void releaseContext(); +} diff --git a/test-utils/src/main/java/org/javaee8/ServerOperations.java b/test-utils/src/main/java/org/javaee8/ServerOperations.java new file mode 100644 index 00000000..3ddad43a --- /dev/null +++ b/test-utils/src/main/java/org/javaee8/ServerOperations.java @@ -0,0 +1,55 @@ +package org.javaee8; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * Various high level Java EE 7 samples specific operations to execute against + * the various servers used for running the samples + * + * @author arjan + * + */ +public class ServerOperations { + + /** + * Add the default test user and credentials to the identity store of + * supported containers + */ + public static void addUsersToContainerIdentityStore() { + + // TODO: abstract adding container managed users to utility class + // TODO: consider PR for sending CLI commands to Arquillian + + String javaEEServer = System.getProperty("javaEEServer"); + + if ("glassfish-remote".equals(javaEEServer) || "payara-remote".equals(javaEEServer)) { + + System.out.println("Adding user for glassfish-remote"); + + List cmd = new ArrayList<>(); + + cmd.add("create-file-user"); + cmd.add("--groups"); + cmd.add("g1"); + cmd.add("--passwordfile"); + cmd.add(Paths.get("").toAbsolutePath() + "/src/test/resources/password.txt"); + + cmd.add("u1"); + + CliCommands.payaraGlassFish(cmd); + } else { + if (javaEEServer == null) { + System.out.println("javaEEServer not specified"); + } else { + System.out.println(javaEEServer + " not supported"); + } + } + + // TODO: support other servers than Payara and GlassFish + + // WildFly ./bin/add-user.sh -a -u u1 -p p1 -g g1 + } + +} diff --git a/test-utils/src/main/resources/arquillian-thorntail.xml b/test-utils/src/main/resources/arquillian-thorntail.xml new file mode 100644 index 00000000..a55b0b32 --- /dev/null +++ b/test-utils/src/main/resources/arquillian-thorntail.xml @@ -0,0 +1,18 @@ + + + + + + + + localhost + ${thorntail.arquillian.daemon.port:12345} + + + + diff --git a/test-utils/src/main/resources/arquillian.xml b/test-utils/src/main/resources/arquillian.xml new file mode 100644 index 00000000..bcd5d74e --- /dev/null +++ b/test-utils/src/main/resources/arquillian.xml @@ -0,0 +1,35 @@ + + + + + + + + + production + + + + + + 8089 + localhost + 8080 + tomcat + manager + + + + + + ${arquillian.tomcat.catalinaHome} + ${arquillian.tomcat.catalinaHome} + tomcat + manager + + + + + + diff --git a/test-utils/src/main/resources/tomcat-users.xml b/test-utils/src/main/resources/tomcat-users.xml new file mode 100644 index 00000000..3a8e5081 --- /dev/null +++ b/test-utils/src/main/resources/tomcat-users.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/validation/README.md b/validation/README.md new file mode 100644 index 00000000..0e1c3b4b --- /dev/null +++ b/validation/README.md @@ -0,0 +1,8 @@ +# Java EE 8 Samples: Bean Validation 2# + +The [JSR 380](https://www.jcp.org/en/jsr/detail?id=380) specifies the next version of Bean Validation, version 2. + +## Samples ## + + - constraints: + Testing new features coming with Bean Validation 2 like java.time API validation support, repeatable annotations and type annotations applied on Lists and Maps. \ No newline at end of file diff --git a/validation/constraints/pom.xml b/validation/constraints/pom.xml new file mode 100644 index 00000000..aaf75d95 --- /dev/null +++ b/validation/constraints/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + org.javaee8 + validation + 1.0-SNAPSHOT + + + constraints + Java EE 8 Samples: Bean Validation 2: Support for java.time API + + + + javax.validation + validation-api + 2.0.0.Final + + + \ No newline at end of file diff --git a/validation/constraints/src/main/java/org/javaee8/validation/Address.java b/validation/constraints/src/main/java/org/javaee8/validation/Address.java new file mode 100644 index 00000000..f9142ea9 --- /dev/null +++ b/validation/constraints/src/main/java/org/javaee8/validation/Address.java @@ -0,0 +1,20 @@ +package org.javaee8.validation; + +import javax.validation.constraints.NotEmpty; + +/** + * @author mertcaliskan + */ +public class Address { + + @NotEmpty + private String detail; + + public String getDetail() { + return detail; + } + + public void setDetail(String detail) { + this.detail = detail; + } +} \ No newline at end of file diff --git a/validation/constraints/src/main/java/org/javaee8/validation/Admin.java b/validation/constraints/src/main/java/org/javaee8/validation/Admin.java new file mode 100644 index 00000000..ce75359d --- /dev/null +++ b/validation/constraints/src/main/java/org/javaee8/validation/Admin.java @@ -0,0 +1,7 @@ +package org.javaee8.validation; + +/** + * @author mertcaliskan + */ +public interface Admin { +} \ No newline at end of file diff --git a/validation/constraints/src/main/java/org/javaee8/validation/Country.java b/validation/constraints/src/main/java/org/javaee8/validation/Country.java new file mode 100644 index 00000000..52333474 --- /dev/null +++ b/validation/constraints/src/main/java/org/javaee8/validation/Country.java @@ -0,0 +1,22 @@ +package org.javaee8.validation; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * @author mertcaliskan + */ +public class Country { + + @NotNull + @Size(min = 2, max = 2) + private String countryCode; + + public String getCountryCode() { + return countryCode; + } + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } +} \ No newline at end of file diff --git a/validation/constraints/src/main/java/org/javaee8/validation/Person.java b/validation/constraints/src/main/java/org/javaee8/validation/Person.java new file mode 100644 index 00000000..ce66550a --- /dev/null +++ b/validation/constraints/src/main/java/org/javaee8/validation/Person.java @@ -0,0 +1,74 @@ +package org.javaee8.validation; + +import javax.validation.Valid; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Past; +import javax.validation.constraints.Size; +import javax.validation.groups.Default; +import java.time.LocalDate; +import java.time.Year; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * @author mertcaliskan + */ +class Person { + + @Past(message = "must be a past date") + private Year yearOfBirth; + + private Optional<@Past LocalDate> marriageAnniversary; + + private List<@NotNull @Email String> emails; + + @Size(min = 8, groups = Default.class) + @Size(min = 12, groups = Admin.class) + private String password; + + private Map<@Valid Country, @Valid Address> addressMap = new HashMap<>(); + + + public Year getYearOfBirth() { + return yearOfBirth; + } + + public void setYearOfBirth(Year yearOfBirth) { + this.yearOfBirth = yearOfBirth; + } + + public Optional getMarriageAnniversary() { + return marriageAnniversary; + } + + public void setMarriageAnniversary(Optional marriageAnniversary) { + this.marriageAnniversary = marriageAnniversary; + } + + public List getEmails() { + return emails; + } + + public void setEmails(List emails) { + this.emails = emails; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Map getAddressMap() { + return addressMap; + } + + public void setAddressMap(Map addressMap) { + this.addressMap = addressMap; + } +} diff --git a/validation/constraints/src/test/java/org/javaee8/validation/ConstraintViolationTest.java b/validation/constraints/src/test/java/org/javaee8/validation/ConstraintViolationTest.java new file mode 100644 index 00000000..ba35781a --- /dev/null +++ b/validation/constraints/src/test/java/org/javaee8/validation/ConstraintViolationTest.java @@ -0,0 +1,145 @@ +package org.javaee8.validation; + +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.groups.Default; +import java.time.*; +import java.util.Arrays; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.*; +import static org.jboss.shrinkwrap.api.ShrinkWrap.create; +import static org.junit.Assert.assertThat; + +/** + * @author mertcaliskan + */ +@RunWith(Arquillian.class) +public class ConstraintViolationTest { + + private Validator validator; + + static { + // prevent to use translated messages which we compare in test; + // translation language depends on the locale. + Locale.setDefault(Locale.US); + } + + + @Deployment + public static WebArchive deploy() { + return create(WebArchive.class) + .addAsLibraries( + create(JavaArchive.class).addClasses(Person.class, + Admin.class, + Country.class, + Address.class)); + } + + @Before + public void setUpValidator() { + validator = Validation + .byDefaultProvider() + .configure() + .clockProvider( + () -> Clock.fixed( + Instant.parse("2017-01-01T00:00:00.00Z"), + ZoneId.systemDefault())) + .buildValidatorFactory() + .getValidator(); + } + + @Test + public void validatingBirthDateFailsWithViolation() { + Person person = new Person(); + person.setYearOfBirth(Year.of(2018)); + + Set> violations = validator.validate(person); + + assertThat(violations, is(not(nullValue()))); + assertThat(violations.size(), is(1)); + assertThat(violations.iterator().next().getMessage(), + is("must be a past date")); + } + + public void validatingMarriageAnniversaryDateFailsWithViolation() { + Person person = new Person(); + person.setMarriageAnniversary(Optional.of(LocalDate.MAX)); + + Set> violations = validator.validate(person); + + assertThat(violations, is(not(nullValue()))); + assertThat(violations.size(), is(1)); + assertThat(violations.iterator().next().getMessage(), + is("must be in the past")); + } + + @Test + public void validatingEmailsFailsWithViolation() { + Person person = new Person(); + person.setEmails(Arrays.asList("mert.caliskan@payara.fish", "invalid_mail")); + + Set> violations = validator.validate(person); + + assertThat(violations, is(not(nullValue()))); + assertThat(violations.size(), is(1)); + assertThat(violations.iterator().next().getMessage(), + is("must be a well-formed email address")); + } + + @Test + public void validatingPassswordWithDefaultGroupFailsWithViolation() { + Person person = new Person(); + person.setPassword("1234567"); + + Set> violations = validator.validate(person, Default.class); + + assertThat(violations, is(not(nullValue()))); + assertThat(violations.size(), is(1)); + assertThat(violations.iterator().next().getMessage(), + is("size must be between 8 and 2147483647")); + } + + @Test + public void validatingPassswordWithAdminGroupFailsWithViolation() { + Person person = new Person(); + person.setPassword("1234567"); + + Set> violations = validator.validate(person, Admin.class); + + assertThat(violations, is(not(nullValue()))); + assertThat(violations.size(), is(1)); + assertThat(violations.iterator().next().getMessage(), + is("size must be between 12 and 2147483647")); + } + + @Test + public void validatingAddressFailsWithViolation() { + Person person = new Person(); + Country country = new Country(); + country.setCountryCode("ABC"); + Address address = new Address(); + address.setDetail(""); + person.getAddressMap().put(country, address); + + Set> violations = validator.validate(person); + + assertThat(violations, is(not(nullValue()))); + assertThat(violations.size(), is(2)); + assertThat(violations.iterator().next().getMessage(), + anyOf(is("must not be empty"), is("size must be between 2 and 2"))); + assertThat(violations.iterator().next().getMessage(), + anyOf(is("must not be empty"), is("size must be between 2 and 2"))); + } +} diff --git a/validation/pom.xml b/validation/pom.xml new file mode 100644 index 00000000..e51a2cbf --- /dev/null +++ b/validation/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + + org.javaee8 + samples-parent + 1.0-SNAPSHOT + + + validation + pom + Java EE 8 Samples: Bean Validation 2 + + + constraints + + + + + org.javaee8 + test-utils + ${project.version} + + + \ No newline at end of file