diff --git a/pom.xml b/pom.xml
index 0b5cd16ea0..10e23da0ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1608,6 +1608,7 @@
persistence-modules/spring-data-elasticsearch
core-java-concurrency
core-java-concurrency-collections
+ restx-demo
diff --git a/restx-demo/data/credentials.json b/restx-demo/data/credentials.json
new file mode 100644
index 0000000000..c1a4fcf531
--- /dev/null
+++ b/restx-demo/data/credentials.json
@@ -0,0 +1,12 @@
+{
+ "//": "lines with // keys are just comments (we don't have real comments in json)",
+ "//": "this file stores password passed through md5+bcrypt hash",
+ "//": "you can use `restx hash md5+bcrypt {password}` shell command to get hashed passwords to put here",
+
+ "//": "to help startup with restx, there are comments with clear text passwords,",
+ "//": "which should obviously not be stored here.",
+ "user1": "$2a$10$iZluFUJShbjb1ue68bLrDuGCeJL9EMLHelVIf8u0SUbCseDOvKnoe",
+ "//": "user 1 password is 'user1-pwd'",
+ "user2": "$2a$10$oym3SYMFXf/9gGfDKKHO4eM1vWNqAZMsRZCL.BORCaP4yp5cdiCXu",
+ "//": "user 2 password is 'user2-pwd'"
+}
\ No newline at end of file
diff --git a/restx-demo/data/users.json b/restx-demo/data/users.json
new file mode 100644
index 0000000000..834e03c4b4
--- /dev/null
+++ b/restx-demo/data/users.json
@@ -0,0 +1,4 @@
+[
+ {"name":"user1", "roles": ["hello"]},
+ {"name":"user2", "roles": []}
+]
\ No newline at end of file
diff --git a/restx-demo/md.restx.json b/restx-demo/md.restx.json
new file mode 100644
index 0000000000..c87244001c
--- /dev/null
+++ b/restx-demo/md.restx.json
@@ -0,0 +1,38 @@
+{
+ "module": "restx-demo:restx-demo:0.1-SNAPSHOT",
+ "packaging": "war",
+
+ "properties": {
+ "java.version": "1.8",
+ "restx.version": "0.35-rc4"
+ },
+ "fragments": {
+ "maven": [
+ "classpath:///restx/build/fragments/maven/javadoc-apidoclet.xml" ]
+ },
+ "dependencies": {
+ "compile": [
+ "io.restx:restx-core:${restx.version}",
+ "io.restx:restx-security-basic:${restx.version}",
+ "io.restx:restx-core-annotation-processor:${restx.version}",
+ "io.restx:restx-factory:${restx.version}",
+ "io.restx:restx-factory-admin:${restx.version}",
+ "io.restx:restx-validation:${restx.version}",
+ "io.restx:restx-monitor-codahale:${restx.version}",
+ "io.restx:restx-monitor-admin:${restx.version}",
+ "io.restx:restx-log-admin:${restx.version}",
+ "io.restx:restx-i18n-admin:${restx.version}",
+ "io.restx:restx-stats-admin:${restx.version}",
+ "io.restx:restx-servlet:${restx.version}",
+ "io.restx:restx-server-jetty8:${restx.version}!optional",
+ "io.restx:restx-apidocs:${restx.version}",
+ "io.restx:restx-specs-admin:${restx.version}",
+ "io.restx:restx-admin:${restx.version}",
+ "ch.qos.logback:logback-classic:1.0.13"
+ ],
+ "test": [
+ "io.restx:restx-specs-tests:${restx.version}",
+ "junit:junit:4.11"
+ ]
+ }
+}
diff --git a/restx-demo/pom.xml b/restx-demo/pom.xml
new file mode 100644
index 0000000000..da106b8191
--- /dev/null
+++ b/restx-demo/pom.xml
@@ -0,0 +1,156 @@
+
+
+ 4.0.0
+
+ restx-demo
+ restx-demo
+ 0.1-SNAPSHOT
+ war
+ restx-demo
+
+
+ 1.8
+ 1.8
+ 0.35-rc4
+
+
+
+ com.baeldung
+ parent-modules
+ 1.0.0-SNAPSHOT
+
+
+
+
+ io.restx
+ restx-core
+ ${restx.version}
+
+
+ io.restx
+ restx-security-basic
+ ${restx.version}
+
+
+ io.restx
+ restx-core-annotation-processor
+ ${restx.version}
+
+
+ io.restx
+ restx-factory
+ ${restx.version}
+
+
+ io.restx
+ restx-factory-admin
+ ${restx.version}
+
+
+ io.restx
+ restx-validation
+ ${restx.version}
+
+
+ io.restx
+ restx-monitor-codahale
+ ${restx.version}
+
+
+ io.restx
+ restx-monitor-admin
+ ${restx.version}
+
+
+ io.restx
+ restx-log-admin
+ ${restx.version}
+
+
+ io.restx
+ restx-i18n-admin
+ ${restx.version}
+
+
+ io.restx
+ restx-stats-admin
+ ${restx.version}
+
+
+ io.restx
+ restx-servlet
+ ${restx.version}
+
+
+ io.restx
+ restx-server-jetty8
+ ${restx.version}
+ true
+
+
+ io.restx
+ restx-apidocs
+ ${restx.version}
+
+
+ io.restx
+ restx-specs-admin
+ ${restx.version}
+
+
+ io.restx
+ restx-admin
+ ${restx.version}
+
+
+ ch.qos.logback
+ logback-classic
+ 1.0.13
+
+
+ io.restx
+ restx-specs-tests
+ ${restx.version}
+ test
+
+
+ junit
+ junit
+ 4.11
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+
+
+ attach-docs
+
+ prepare-package
+
+ jar
+
+
+
+
+ ${maven.compiler.source}
+ restx.apidocs.doclet.ApidocsDoclet
+
+ io.restx
+ restx-apidocs-doclet
+ ${restx.version}
+
+ -restx-target-dir ${project.basedir}/target/classes
+
+
+
+
+
diff --git a/restx-demo/src/main/java/restx/demo/AppModule.java b/restx-demo/src/main/java/restx/demo/AppModule.java
new file mode 100644
index 0000000000..26bc681481
--- /dev/null
+++ b/restx-demo/src/main/java/restx/demo/AppModule.java
@@ -0,0 +1,74 @@
+package restx.demo;
+
+import restx.config.ConfigLoader;
+import restx.config.ConfigSupplier;
+import restx.factory.Provides;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableSet;
+import restx.security.*;
+import restx.factory.Module;
+import restx.factory.Provides;
+import javax.inject.Named;
+
+import java.nio.file.Paths;
+
+@Module
+public class AppModule {
+ @Provides
+ public SignatureKey signatureKey() {
+ return new SignatureKey("restx-demo -447494532235718370 restx-demo 801c9eaf-4116-48f2-906b-e979fba72757".getBytes(Charsets.UTF_8));
+ }
+
+ @Provides
+ @Named("restx.admin.password")
+ public String restxAdminPassword() {
+ return "4780";
+ }
+
+ @Provides
+ public ConfigSupplier appConfigSupplier(ConfigLoader configLoader) {
+ // Load settings.properties in restx.demo package as a set of config entries
+ return configLoader.fromResource("restx/demo/settings");
+ }
+
+ @Provides
+ public CredentialsStrategy credentialsStrategy() {
+ return new BCryptCredentialsStrategy();
+ }
+
+ @Provides
+ public BasicPrincipalAuthenticator basicPrincipalAuthenticator(
+ SecuritySettings securitySettings, CredentialsStrategy credentialsStrategy,
+ @Named("restx.admin.passwordHash") String defaultAdminPasswordHash, ObjectMapper mapper) {
+ return new StdBasicPrincipalAuthenticator(new StdUserService<>(
+ // use file based users repository.
+ // Developer's note: prefer another storage mechanism for your users if you need real user management
+ // and better perf
+ new FileBasedUserRepository<>(
+ StdUser.class, // this is the class for the User objects, that you can get in your app code
+ // with RestxSession.current().getPrincipal().get()
+ // it can be a custom user class, it just need to be json deserializable
+ mapper,
+
+ // this is the default restx admin, useful to access the restx admin console.
+ // if one user with restx-admin role is defined in the repository, this default user won't be
+ // available anymore
+ new StdUser("admin", ImmutableSet.of("*")),
+
+ // the path where users are stored
+ Paths.get("data/users.json"),
+
+ // the path where credentials are stored. isolating both is a good practice in terms of security
+ // it is strongly recommended to follow this approach even if you use your own repository
+ Paths.get("data/credentials.json"),
+
+ // tells that we want to reload the files dynamically if they are touched.
+ // this has a performance impact, if you know your users / credentials never change without a
+ // restart you can disable this to get better perfs
+ true),
+ credentialsStrategy, defaultAdminPasswordHash),
+ securitySettings);
+ }
+}
diff --git a/restx-demo/src/main/java/restx/demo/AppServer.java b/restx-demo/src/main/java/restx/demo/AppServer.java
new file mode 100644
index 0000000000..d66aadac68
--- /dev/null
+++ b/restx-demo/src/main/java/restx/demo/AppServer.java
@@ -0,0 +1,32 @@
+package restx.demo;
+
+import com.google.common.base.Optional;
+import restx.server.WebServer;
+import restx.server.Jetty8WebServer;
+
+/**
+ * This class can be used to run the app.
+ *
+ * Alternatively, you can deploy the app as a war in a regular container like tomcat or jetty.
+ *
+ * Reading the port from system env PORT makes it compatible with heroku.
+ */
+public class AppServer {
+ public static final String WEB_INF_LOCATION = "src/main/webapp/WEB-INF/web.xml";
+ public static final String WEB_APP_LOCATION = "src/main/webapp";
+
+ public static void main(String[] args) throws Exception {
+ int port = Integer.valueOf(Optional.fromNullable(System.getenv("PORT")).or("8080"));
+ WebServer server = new Jetty8WebServer(WEB_INF_LOCATION, WEB_APP_LOCATION, port, "0.0.0.0");
+
+ /*
+ * load mode from system property if defined, or default to dev
+ * be careful with that setting, if you use this class to launch your server in production, make sure to launch
+ * it with -Drestx.mode=prod or change the default here
+ */
+ System.setProperty("restx.mode", System.getProperty("restx.mode", "dev"));
+ System.setProperty("restx.app.package", "restx.demo");
+
+ server.startAndAwait();
+ }
+}
diff --git a/restx-demo/src/main/java/restx/demo/Roles.java b/restx-demo/src/main/java/restx/demo/Roles.java
new file mode 100644
index 0000000000..1240da70d1
--- /dev/null
+++ b/restx-demo/src/main/java/restx/demo/Roles.java
@@ -0,0 +1,10 @@
+package restx.demo;
+
+/**
+ * A list of roles for the application.
+ *
+ * We don't use an enum here because it must be used inside an annotation.
+ */
+public final class Roles {
+ public static final String HELLO_ROLE = "hello";
+}
diff --git a/restx-demo/src/main/java/restx/demo/domain/Message.java b/restx-demo/src/main/java/restx/demo/domain/Message.java
new file mode 100644
index 0000000000..733c00deff
--- /dev/null
+++ b/restx-demo/src/main/java/restx/demo/domain/Message.java
@@ -0,0 +1,21 @@
+package restx.demo.domain;
+
+public class Message {
+ private String message;
+
+ public String getMessage() {
+ return message;
+ }
+
+ public Message setMessage(final String message) {
+ this.message = message;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "Message{" +
+ "message='" + message + '\'' +
+ '}';
+ }
+}
diff --git a/restx-demo/src/main/java/restx/demo/rest/HelloResource.java b/restx-demo/src/main/java/restx/demo/rest/HelloResource.java
new file mode 100644
index 0000000000..5cb2c2a5e6
--- /dev/null
+++ b/restx-demo/src/main/java/restx/demo/rest/HelloResource.java
@@ -0,0 +1,62 @@
+package restx.demo.rest;
+
+import restx.demo.domain.Message;
+import restx.demo.Roles;
+import org.joda.time.DateTime;
+import restx.annotations.GET;
+import restx.annotations.POST;
+import restx.annotations.RestxResource;
+import restx.factory.Component;
+import restx.security.PermitAll;
+import restx.security.RolesAllowed;
+import restx.security.RestxSession;
+
+import javax.validation.constraints.NotNull;
+
+@Component @RestxResource
+public class HelloResource {
+
+ /**
+ * Say hello to currently logged in user.
+ *
+ * Authorized only for principals with Roles.HELLO_ROLE role.
+ *
+ * @return a Message to say hello
+ */
+ @GET("/message")
+ @RolesAllowed(Roles.HELLO_ROLE)
+ public Message sayHello() {
+ return new Message().setMessage(String.format(
+ "hello %s, it's %s",
+ RestxSession.current().getPrincipal().get().getName(),
+ DateTime.now().toString("HH:mm:ss")));
+ }
+
+ /**
+ * Say hello to anybody.
+ *
+ * Does not require authentication.
+ *
+ * @return a Message to say hello
+ */
+ @GET("/hello")
+ @PermitAll
+ public Message helloPublic(String who) {
+ return new Message().setMessage(String.format(
+ "hello %s, it's %s",
+ who, DateTime.now().toString("HH:mm:ss")));
+ }
+
+ public static class MyPOJO {
+ @NotNull
+ String value;
+ public String getValue(){ return value; }
+ public void setValue(String value){ this.value = value; }
+ }
+ @POST("/mypojo")
+ @PermitAll
+ public MyPOJO helloPojo(MyPOJO pojo){
+ pojo.setValue("hello "+pojo.getValue());
+ return pojo;
+ }
+}
diff --git a/restx-demo/src/main/resources/logback.xml b/restx-demo/src/main/resources/logback.xml
new file mode 100644
index 0000000000..524bca6b1f
--- /dev/null
+++ b/restx-demo/src/main/resources/logback.xml
@@ -0,0 +1,94 @@
+
+
+ true
+
+
+
+
+ ${LOGS_FOLDER}/errors.log
+
+ ERROR
+
+
+ %d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n
+
+
+ ${LOGS_FOLDER}/errors.%d.log
+ 30
+
+
+
+
+
+
+
+ ${LOGS_FOLDER}/app.log
+
+ INFO
+
+
+ %d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n
+
+
+ ${LOGS_FOLDER}/app.%d.log
+ 10
+
+
+
+ ${LOGS_FOLDER}/debug.log
+
+ %d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n
+
+
+ ${LOGS_FOLDER}/debug.%i.log.zip
+ 1
+ 3
+
+
+
+ 50MB
+
+
+
+
+
+
+
+
+
+
+
+ %d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n
+
+
+
+ ${LOGS_FOLDER}/app.log
+
+ %d [%-16thread] [%-10X{principal}] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/restx-demo/src/main/resources/restx/demo/settings.properties b/restx-demo/src/main/resources/restx/demo/settings.properties
new file mode 100644
index 0000000000..a03c2eea97
--- /dev/null
+++ b/restx-demo/src/main/resources/restx/demo/settings.properties
@@ -0,0 +1 @@
+app.name=restx-demo
\ No newline at end of file
diff --git a/restx-demo/src/main/webapp/WEB-INF/web.xml b/restx-demo/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000..c651794526
--- /dev/null
+++ b/restx-demo/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,15 @@
+
+
+
+ restx
+ restx.servlet.RestxMainRouterServlet
+ 1
+
+
+ restx
+ /api/*
+
+
diff --git a/restx-demo/src/test/java/restx/demo/rest/HelloResourceSpecTest.java b/restx-demo/src/test/java/restx/demo/rest/HelloResourceSpecTest.java
new file mode 100644
index 0000000000..138a22d074
--- /dev/null
+++ b/restx-demo/src/test/java/restx/demo/rest/HelloResourceSpecTest.java
@@ -0,0 +1,23 @@
+package restx.demo.rest;
+
+import restx.demo.AppServer;
+import org.junit.runner.RunWith;
+import restx.tests.RestxSpecTestsRunner;
+import restx.tests.FindSpecsIn;
+
+@RunWith(RestxSpecTestsRunner.class)
+@FindSpecsIn("specs/hello")
+public class HelloResourceSpecTest {
+
+ /**
+ * Useless, thanks to both @RunWith(RestxSpecTestsRunner.class) & @FindSpecsIn()
+ *
+ * @Rule
+ * public RestxSpecRule rule = new RestxSpecRule();
+ *
+ * @Test
+ * public void test_spec() throws Exception {
+ * rule.runTest(specTestPath);
+ * }
+ */
+}
diff --git a/restx-demo/src/test/resources/specs/hello/should_admin_say_hello.spec.yaml b/restx-demo/src/test/resources/specs/hello/should_admin_say_hello.spec.yaml
new file mode 100644
index 0000000000..1b7b8f0f90
--- /dev/null
+++ b/restx-demo/src/test/resources/specs/hello/should_admin_say_hello.spec.yaml
@@ -0,0 +1,10 @@
+title: should admin say hello
+given:
+ - time: 2013-08-28T01:18:00.822+02:00
+ - uuids: [ "e2b4430f-9541-4602-9a3a-413d17c56a6b" ]
+wts:
+ - when: |
+ GET message
+ $RestxSession: {"_expires":"2013-09-27T01:18:00.822+02:00","principal":"admin","sessionKey":"e2b4430f-9541-4602-9a3a-413d17c56a6b"}
+ then: |
+ {"message":"hello admin, it's 01:18:00"}
diff --git a/restx-demo/src/test/resources/specs/hello/should_anyone_say_hello.spec.yaml b/restx-demo/src/test/resources/specs/hello/should_anyone_say_hello.spec.yaml
new file mode 100644
index 0000000000..29b6faca34
--- /dev/null
+++ b/restx-demo/src/test/resources/specs/hello/should_anyone_say_hello.spec.yaml
@@ -0,0 +1,8 @@
+title: should admin say hello
+given:
+ - time: 2013-08-28T01:18:00.822+02:00
+wts:
+ - when: |
+ GET hello?who=xavier
+ then: |
+ {"message":"hello xavier, it's 01:18:00"}
diff --git a/restx-demo/src/test/resources/specs/hello/should_missing_value_triggers_validation_error.spec.yaml b/restx-demo/src/test/resources/specs/hello/should_missing_value_triggers_validation_error.spec.yaml
new file mode 100644
index 0000000000..d0c6323caf
--- /dev/null
+++ b/restx-demo/src/test/resources/specs/hello/should_missing_value_triggers_validation_error.spec.yaml
@@ -0,0 +1,17 @@
+title: should missing post value triggers a validation error
+given:
+ - time: 2013-08-28T01:18:00.822+02:00
+ - uuids: [ "e2b4430f-9541-4602-9a3a-413d17c56a6b" ]
+wts:
+ - when: |
+ POST mypojo
+ $RestxSession: {"_expires":"2013-09-27T01:18:00.822+02:00","principal":"user1","sessionKey":"e2b4430f-9541-4602-9a3a-413d17c56a6b"}
+ {}
+ then: |
+ 400
+ - when: |
+ POST mypojo
+ $RestxSession: {"_expires":"2013-09-27T01:18:00.822+02:00","principal":"user1","sessionKey":"e2b4430f-9541-4602-9a3a-413d17c56a6b"}
+ {"value":"world"}
+ then: |
+ {"value":"hello world"}
diff --git a/restx-demo/src/test/resources/specs/hello/should_user1_say_hello.spec.yaml b/restx-demo/src/test/resources/specs/hello/should_user1_say_hello.spec.yaml
new file mode 100644
index 0000000000..791a3a2776
--- /dev/null
+++ b/restx-demo/src/test/resources/specs/hello/should_user1_say_hello.spec.yaml
@@ -0,0 +1,10 @@
+title: should user1 say hello
+given:
+ - time: 2013-08-28T01:18:00.822+02:00
+ - uuids: [ "e2b4430f-9541-4602-9a3a-413d17c56a6b" ]
+wts:
+ - when: |
+ GET message
+ $RestxSession: {"_expires":"2013-09-27T01:18:00.822+02:00","principal":"user1","sessionKey":"e2b4430f-9541-4602-9a3a-413d17c56a6b"}
+ then: |
+ {"message":"hello user1, it's 01:18:00"}
diff --git a/restx-demo/src/test/resources/specs/hello/should_user2_not_say_hello.spec.yaml b/restx-demo/src/test/resources/specs/hello/should_user2_not_say_hello.spec.yaml
new file mode 100644
index 0000000000..ead5af8d0c
--- /dev/null
+++ b/restx-demo/src/test/resources/specs/hello/should_user2_not_say_hello.spec.yaml
@@ -0,0 +1,10 @@
+title: should user2 not say hello
+given:
+ - time: 2013-08-28T01:19:44.770+02:00
+ - uuids: [ "56f71fcc-42d3-422f-9458-8ad37fc4a0b5" ]
+wts:
+ - when: |
+ GET message
+ $RestxSession: {"_expires":"2013-09-27T01:19:44.770+02:00","principal":"user2","sessionKey":"56f71fcc-42d3-422f-9458-8ad37fc4a0b5"}
+ then: |
+ 403