diff --git a/coverage-report/src/test/java/org/jooby/BeanParserFeature.java b/coverage-report/src/test/java/org/jooby/BeanParserFeature.java index 69a204a5f0..9478e0229b 100644 --- a/coverage-report/src/test/java/org/jooby/BeanParserFeature.java +++ b/coverage-report/src/test/java/org/jooby/BeanParserFeature.java @@ -4,7 +4,6 @@ import java.util.Optional; -import org.jooby.mvc.Body; import org.jooby.mvc.Path; import org.jooby.test.ServerFeature; import org.junit.Test; @@ -72,7 +71,7 @@ public String getibean(final org.jooby.Request req, final IBean bean) throws Exc @org.jooby.mvc.POST @Path("/ibean") - public String postibean(final org.jooby.Request req, final @Body IBean bean) throws Exception { + public String postibean(final org.jooby.Request req, final IBean bean) throws Exception { assertEquals(req.param("name").value(), bean.name()); assertEquals(req.param("valid").booleanValue(), bean.isValid()); assertEquals(req.param("age").intValue(), bean.getAge()); @@ -131,7 +130,9 @@ public String invalidbean(final DefConstBean bean) throws Exception { }); post("/ibean", req -> { - IBean bean = req.body().to(IBean.class); + IBean bean = req.form(IBean.class); + System.out.println(bean.name()); + assertEquals(req.param("name").value(), bean.name()); assertEquals(req.param("valid").booleanValue(), bean.isValid()); assertEquals(req.param("age").intValue(), bean.getAge()); @@ -146,12 +147,12 @@ public String invalidbean(final DefConstBean bean) throws Exception { }); get("/defaultConstructor", req -> { - req.params().to(DefConstBean.class); + req.form(DefConstBean.class); return "OK"; }); post("/beanwithargs", req -> { - BeanWithArgs bean = req.body().to(BeanWithArgs.class); + BeanWithArgs bean = req.form(BeanWithArgs.class); assertEquals(req.param("name").value(), bean.name); assertEquals(req.param("age").intValue(), (int) bean.age.get()); return "OK"; @@ -165,7 +166,7 @@ public String invalidbean(final DefConstBean bean) throws Exception { }); post("/beannoarg", req -> { - BeanNoArg bean = req.body().to(BeanNoArg.class); + BeanNoArg bean = req.form(BeanNoArg.class); assertEquals(req.param("name").value(), bean.getName()); assertEquals(req.param("age").intValue(), (int) bean.getAge().get()); return "OK"; @@ -258,28 +259,6 @@ public void postibean() throws Exception { } - @Test - public void bean415() throws Exception { - - request() - .post("/ibean") - .header("Content-Type", "application/xml") - .multipart() - .add("name", "edgar") - .add("age", 34) - .add("valid", false) - .expect(415); - - request() - .post("/r/ibean") - .header("Content-Type", "application/xml") - .multipart() - .add("name", "edgar") - .add("age", 34) - .add("valid", false) - .expect(415); - } - @Test public void postbeanwithargs() throws Exception { request() diff --git a/coverage-report/src/test/java/org/jooby/EnvConfCallbackFeature.java b/coverage-report/src/test/java/org/jooby/EnvConfCallbackFeature.java new file mode 100644 index 0000000000..af5e89068c --- /dev/null +++ b/coverage-report/src/test/java/org/jooby/EnvConfCallbackFeature.java @@ -0,0 +1,26 @@ +package org.jooby; + +import org.jooby.test.ServerFeature; +import org.junit.Test; + +import com.google.inject.Key; +import com.google.inject.name.Names; + +public class EnvConfCallbackFeature extends ServerFeature { + + { + Key key = Key.get(String.class, Names.named(("envcallback"))); + on("dev", conf -> { + use((env, config, binder) -> { + binder.bind(key).toInstance(env.name()); + }); + }); + + get("/", req -> req.require(key)); + } + + @Test + public void devCallback() throws Exception { + request().get("/").expect("dev"); + } +} diff --git a/coverage-report/src/test/java/org/jooby/RequestTimestampFeature.java b/coverage-report/src/test/java/org/jooby/RequestTimestampFeature.java new file mode 100644 index 0000000000..cb2ef7c8a3 --- /dev/null +++ b/coverage-report/src/test/java/org/jooby/RequestTimestampFeature.java @@ -0,0 +1,22 @@ +package org.jooby; + +import static org.junit.Assert.assertTrue; + +import org.jooby.test.ServerFeature; +import org.junit.Test; + +public class RequestTimestampFeature extends ServerFeature { + + { + get("/ts", req -> req.timestamp()); + } + + @Test + public void timestamp() throws Exception { + request() + .get("/ts") + .expect(v -> { + assertTrue(Long.parseLong(v) > 0); + }); + } +} diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue433.java b/coverage-report/src/test/java/org/jooby/issues/Issue433.java index 6b6c93de3e..0eac194ded 100644 --- a/coverage-report/src/test/java/org/jooby/issues/Issue433.java +++ b/coverage-report/src/test/java/org/jooby/issues/Issue433.java @@ -16,7 +16,7 @@ public static class Bean { }); post("/433", req -> { - Bean bean = req.body(Bean.class); + Bean bean = req.form(Bean.class); return bean.q; }); } diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue444.java b/coverage-report/src/test/java/org/jooby/issues/Issue444.java index d32bc0320b..5b19eb341b 100644 --- a/coverage-report/src/test/java/org/jooby/issues/Issue444.java +++ b/coverage-report/src/test/java/org/jooby/issues/Issue444.java @@ -20,9 +20,13 @@ public class Issue444 extends ServerFeature { }); post("/444", req -> { - ByteBuffer buffer = req.body().to(ByteBuffer.class); + ByteBuffer buffer = req.body(ByteBuffer.class); return buffer.remaining(); }); + + post("/444/raw", req -> { + return req.body(String.class); + }); } @Test @@ -32,4 +36,60 @@ public void shouldAcceptPostWithoutContentType() throws URISyntaxException, Exce .body("abc", null) .expect("3"); } + + @Test + public void shouldFavorCustomParserOnFormUrlEncoded() throws URISyntaxException, Exception { + request() + .post("/444") + .body("abc", "application/x-www-form-urlencoded") + .expect("3"); + } + + @Test + public void shouldFavorCustomParserOnMultipart() throws URISyntaxException, Exception { + request() + .post("/444") + .body("abc", "multipart/form-data") + .expect("3"); + } + + @Test + public void shouldGetRawBody() throws URISyntaxException, Exception { + request() + .post("/444/raw") + .form() + .add("foo", "bar") + .add("bar", "foo") + .expect("foo=bar&bar=foo"); + + request() + .post("/444/raw") + .body("--ylYSWaNWL2lXy3vBYw458nuB9UDehD5o6iHZuLK\n" + + "Content-Disposition: form-data; name=\"foo\"\n" + + "Content-Type: text/plain\n" + + "Content-Transfer-Encoding: 8bit\n" + + "\n" + + "bar\n" + + "--ylYSWaNWL2lXy3vBYw458nuB9UDehD5o6iHZuLK\n" + + "Content-Disposition: form-data; name=\"bar\"; filename=\"foo.txt\"\n" + + "Content-Type: text/plain\n" + + "Content-Transfer-Encoding: binary\n" + + "\n" + + "foo\n" + + "--ylYSWaNWL2lXy3vBYw458nuB9UDehD5o6iHZuLK--", "multipart/form-data") + .expect("--ylYSWaNWL2lXy3vBYw458nuB9UDehD5o6iHZuLK\n" + + "Content-Disposition: form-data; name=\"foo\"\n" + + "Content-Type: text/plain\n" + + "Content-Transfer-Encoding: 8bit\n" + + "\n" + + "bar\n" + + "--ylYSWaNWL2lXy3vBYw458nuB9UDehD5o6iHZuLK\n" + + "Content-Disposition: form-data; name=\"bar\"; filename=\"foo.txt\"\n" + + "Content-Type: text/plain\n" + + "Content-Transfer-Encoding: binary\n" + + "\n" + + "foo\n" + + "--ylYSWaNWL2lXy3vBYw458nuB9UDehD5o6iHZuLK--"); + } + } diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue453.java b/coverage-report/src/test/java/org/jooby/issues/Issue453.java index ac476403bf..09f636e7dd 100644 --- a/coverage-report/src/test/java/org/jooby/issues/Issue453.java +++ b/coverage-report/src/test/java/org/jooby/issues/Issue453.java @@ -18,10 +18,14 @@ public static class Form { return req.header("text", "html").value(); }); - get("/453/escape-form", req -> { + get("/453/escape-params", req -> { return req.params(Form.class, req.param("xss").value("html")).text; }); + get("/453/escape-form", req -> { + return req.form(Form.class, req.param("xss").value("html")).text; + }); + get("/453/to-escape-form", req -> { return req.params(req.param("xss").value("html")).to(Form.class).text; }); @@ -49,6 +53,10 @@ public void escapeForm() throws Exception { .get("/453/escape-form?text=%3Ch1%3EX%3C/h1%3E") .expect("<h1>X</h1>"); + request() + .get("/453/escape-params?text=%3Ch1%3EX%3C/h1%3E") + .expect("<h1>X</h1>"); + request() .get("/453/escape-form?text=%3Ch1%3EX%3C/h1%3E&xss=none") .expect("

X

"); diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue484.java b/coverage-report/src/test/java/org/jooby/issues/Issue488.java similarity index 83% rename from coverage-report/src/test/java/org/jooby/issues/Issue484.java rename to coverage-report/src/test/java/org/jooby/issues/Issue488.java index 59fc9a62d2..1ac763ba52 100644 --- a/coverage-report/src/test/java/org/jooby/issues/Issue484.java +++ b/coverage-report/src/test/java/org/jooby/issues/Issue488.java @@ -6,17 +6,17 @@ import org.jooby.test.ServerFeature; import org.junit.Test; -public class Issue484 extends ServerFeature { +public class Issue488 extends ServerFeature { { - get("/484", req -> { + get("/488", req -> { String t1 = Thread.currentThread().getName(); return new Deferred(deferred -> { deferred.resolve(t1 + ":" + Thread.currentThread().getName()); }); }); - get("/484/promise", promise(deferred -> { + get("/488/promise", promise(deferred -> { String t1 = Thread.currentThread().getName(); deferred.resolve(t1 + ":" + Thread.currentThread().getName()); })); @@ -25,14 +25,14 @@ public class Issue484 extends ServerFeature { @Test public void deferredOnDefaultExecutor() throws Exception { request() - .get("/484") + .get("/488") .expect(rsp -> { String[] threads = rsp.split(":"); assertEquals(threads[0], threads[1]); }); request() - .get("/484/promise") + .get("/488/promise") .expect(rsp -> { String[] threads = rsp.split(":"); assertEquals(threads[0], threads[1]); diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue484b.java b/coverage-report/src/test/java/org/jooby/issues/Issue488b.java similarity index 83% rename from coverage-report/src/test/java/org/jooby/issues/Issue484b.java rename to coverage-report/src/test/java/org/jooby/issues/Issue488b.java index 4276948756..9f6cf41f8e 100644 --- a/coverage-report/src/test/java/org/jooby/issues/Issue484b.java +++ b/coverage-report/src/test/java/org/jooby/issues/Issue488b.java @@ -8,18 +8,18 @@ import org.jooby.test.ServerFeature; import org.junit.Test; -public class Issue484b extends ServerFeature { +public class Issue488b extends ServerFeature { { executor(new ForkJoinPool()); - get("/484", req -> { + get("/488", req -> { return new Deferred(deferred -> { deferred.resolve(deferred.callerThread() + ":" + Thread.currentThread().getName()); }); }); - get("/484/promise", promise(deferred -> { + get("/488/promise", promise(deferred -> { deferred.resolve(deferred.callerThread() + ":" + Thread.currentThread().getName()); })); } @@ -27,14 +27,14 @@ public class Issue484b extends ServerFeature { @Test public void deferredWithExecutorInstance() throws Exception { request() - .get("/484") + .get("/488") .expect(rsp -> { String[] threads = rsp.split(":"); assertNotEquals(threads[0], threads[1]); }); request() - .get("/484/promise") + .get("/488/promise") .expect(rsp -> { String[] threads = rsp.split(":"); assertNotEquals(threads[0], threads[1]); diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue484c.java b/coverage-report/src/test/java/org/jooby/issues/Issue488c.java similarity index 86% rename from coverage-report/src/test/java/org/jooby/issues/Issue484c.java rename to coverage-report/src/test/java/org/jooby/issues/Issue488c.java index d79ff22112..06d6314fdf 100644 --- a/coverage-report/src/test/java/org/jooby/issues/Issue484c.java +++ b/coverage-report/src/test/java/org/jooby/issues/Issue488c.java @@ -12,7 +12,7 @@ import com.google.inject.Key; import com.google.inject.name.Names; -public class Issue484c extends ServerFeature { +public class Issue488c extends ServerFeature { { executor("ste"); @@ -22,13 +22,13 @@ public class Issue484c extends ServerFeature { .toInstance(Executors.newSingleThreadExecutor()); }); - get("/484", req -> { + get("/488", req -> { return new Deferred(deferred -> { deferred.resolve(deferred.callerThread() + ":" + Thread.currentThread().getName()); }); }); - get("/484/promise", promise((req, deferred) -> { + get("/488/promise", promise((req, deferred) -> { deferred.resolve(deferred.callerThread() + ":" + Thread.currentThread().getName()); })); } @@ -36,14 +36,14 @@ public class Issue484c extends ServerFeature { @Test public void deferredWithExecutorReference() throws Exception { request() - .get("/484") + .get("/488") .expect(rsp -> { String[] threads = rsp.split(":"); assertNotEquals(threads[0], threads[1]); }); request() - .get("/484/promise") + .get("/488/promise") .expect(rsp -> { String[] threads = rsp.split(":"); assertNotEquals(threads[0], threads[1]); diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue484d.java b/coverage-report/src/test/java/org/jooby/issues/Issue488d.java similarity index 76% rename from coverage-report/src/test/java/org/jooby/issues/Issue484d.java rename to coverage-report/src/test/java/org/jooby/issues/Issue488d.java index 426f4f1808..f689ba3a53 100644 --- a/coverage-report/src/test/java/org/jooby/issues/Issue484d.java +++ b/coverage-report/src/test/java/org/jooby/issues/Issue488d.java @@ -10,7 +10,7 @@ import com.typesafe.config.ConfigFactory; import com.typesafe.config.ConfigValueFactory; -public class Issue484d extends ServerFeature { +public class Issue488d extends ServerFeature { { use(ConfigFactory.empty() @@ -21,23 +21,23 @@ public class Issue484d extends ServerFeature { use(new Exec()); - get("/484", req -> new Deferred(deferred -> { + get("/488", req -> new Deferred(deferred -> { deferred.resolve(Thread.currentThread().getName()); })); - get("/484/cached", req -> new Deferred("cached", deferred -> { + get("/488/cached", req -> new Deferred("cached", deferred -> { deferred.resolve(Thread.currentThread().getName()); })); - get("/484/fj", promise(deferred -> { + get("/488/fj", promise(deferred -> { deferred.resolve(Thread.currentThread().getName()); })); - get("/484/local/cached", promise("cached", (req, deferred) -> { + get("/488/local/cached", promise("cached", (req, deferred) -> { deferred.resolve(Thread.currentThread().getName()); })); - get("/484/local/fj", promise("fj", deferred -> { + get("/488/local/fj", promise("fj", deferred -> { deferred.resolve(Thread.currentThread().getName()); })); } @@ -45,31 +45,31 @@ public class Issue484d extends ServerFeature { @Test public void deferredOnGloablOrLocalExecutor() throws Exception { request() - .get("/484") + .get("/488") .expect(rsp -> { assertTrue(rsp.startsWith("forkjoin")); }); request() - .get("/484/cached") + .get("/488/cached") .expect(rsp -> { assertTrue(rsp.startsWith("cached")); }); request() - .get("/484/fj") + .get("/488/fj") .expect(rsp -> { assertTrue(rsp.startsWith("forkjoin")); }); request() - .get("/484/local/cached") + .get("/488/local/cached") .expect(rsp -> { assertTrue(rsp.startsWith("cached")); }); request() - .get("/484/local/fj") + .get("/488/local/fj") .expect(rsp -> { assertTrue(rsp.startsWith("forkjoin")); }); diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue485.java b/coverage-report/src/test/java/org/jooby/issues/Issue489.java similarity index 79% rename from coverage-report/src/test/java/org/jooby/issues/Issue485.java rename to coverage-report/src/test/java/org/jooby/issues/Issue489.java index a84cc629b5..9cec6694d6 100644 --- a/coverage-report/src/test/java/org/jooby/issues/Issue485.java +++ b/coverage-report/src/test/java/org/jooby/issues/Issue489.java @@ -8,17 +8,17 @@ import org.jooby.test.ServerFeature; import org.junit.Test; -public class Issue485 extends ServerFeature { +public class Issue489 extends ServerFeature { { executor(new ForkJoinPool()); executor("cached", Executors.newCachedThreadPool()); - get("/485/fj", promise(deferred -> { + get("/489/fj", promise(deferred -> { deferred.resolve(Thread.currentThread().getName()); })); - get("/485/cached", promise("cached", deferred -> { + get("/489/cached", promise("cached", deferred -> { deferred.resolve(Thread.currentThread().getName()); })); @@ -27,13 +27,13 @@ public class Issue485 extends ServerFeature { @Test public void globalOrLocalExecutor() throws Exception { request() - .get("/485/fj") + .get("/489/fj") .expect(rsp -> { assertTrue(rsp.toLowerCase().startsWith("forkjoinpool")); }); request() - .get("/485/cached") + .get("/489/cached") .expect(rsp -> { assertTrue(rsp.toLowerCase().startsWith("pool")); }); diff --git a/coverage-report/src/test/java/org/jooby/issues/Issue498.java b/coverage-report/src/test/java/org/jooby/issues/Issue498.java new file mode 100644 index 0000000000..488ea583d7 --- /dev/null +++ b/coverage-report/src/test/java/org/jooby/issues/Issue498.java @@ -0,0 +1,53 @@ +package org.jooby.issues; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.jooby.test.ServerFeature; +import org.junit.Test; + +import com.couchbase.client.deps.io.netty.util.internal.chmv8.ForkJoinPool; + +public class Issue498 extends ServerFeature { + + { + executor(new ForkJoinPool()); + + get("/498", deferred(req -> { + assertNotNull(req); + return Thread.currentThread().getName(); + })); + + get("/498/0", deferred(() -> { + return Thread.currentThread().getName(); + })); + + get("/498/x", deferred(() -> { + throw new IllegalStateException("intentional err"); + })); + + err((req, rsp, x) -> { + rsp.send(x.getCause().getMessage()); + }); + } + + @Test + public void functionalDeferred() throws Exception { + request() + .get("/498") + .expect(v -> { + assertTrue(v.toLowerCase().contains("fork")); + }); + + request() + .get("/498/0") + .expect(v -> { + assertTrue(v.toLowerCase().contains("fork")); + }); + + request() + .get("/498/x") + .expect("intentional err"); + } + +} diff --git a/coverage-report/src/test/java/org/jooby/ws/OnByteArrayMessageFeature.java b/coverage-report/src/test/java/org/jooby/ws/OnByteArrayMessageFeature.java index 223c5d2420..91930840c3 100644 --- a/coverage-report/src/test/java/org/jooby/ws/OnByteArrayMessageFeature.java +++ b/coverage-report/src/test/java/org/jooby/ws/OnByteArrayMessageFeature.java @@ -6,6 +6,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.jooby.MediaType; import org.jooby.test.ServerFeature; import org.junit.After; import org.junit.Before; @@ -29,7 +30,7 @@ public class OnByteArrayMessageFeature extends ServerFeature { ws.close(); }); }); - }); + }).produces(MediaType.octetstream); } diff --git a/coverage-report/src/test/java/org/jooby/ws/OnByteBufferMessageFeature.java b/coverage-report/src/test/java/org/jooby/ws/OnByteBufferMessageFeature.java index 18d356b3fa..7cf87abf43 100644 --- a/coverage-report/src/test/java/org/jooby/ws/OnByteBufferMessageFeature.java +++ b/coverage-report/src/test/java/org/jooby/ws/OnByteBufferMessageFeature.java @@ -7,6 +7,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.jooby.MediaType; import org.jooby.test.ServerFeature; import org.junit.After; import org.junit.Before; @@ -30,8 +31,7 @@ public class OnByteBufferMessageFeature extends ServerFeature { ws.close(); }); }); - - }); + }).produces(MediaType.octetstream); } diff --git a/jooby-banner/src/main/java/org/jooby/banner/Banner.java b/jooby-banner/src/main/java/org/jooby/banner/Banner.java index 249a231508..c27534abd5 100644 --- a/jooby-banner/src/main/java/org/jooby/banner/Banner.java +++ b/jooby-banner/src/main/java/org/jooby/banner/Banner.java @@ -23,14 +23,20 @@ import java.util.Optional; +import javax.inject.Provider; + import org.jooby.Env; import org.jooby.Jooby.Module; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.name.Names; import com.typesafe.config.Config; +import javaslang.control.Try; + /** *

banner

*

@@ -105,8 +111,15 @@ public void configure(final Env env, final Config conf, final Binder binder) { String v = conf.getString("application.version"); String text = this.text.orElse(name); - env.onStart( - () -> log.info("\n{} v{}\n", convertOneLine(String.format(FONT, font), text).trim(), v)); + Provider ascii = () -> Try + .of(() -> convertOneLine(String.format(FONT, font), text).trim()) + .getOrElse(text); + + binder.bind(Key.get(String.class, Names.named("application.banner"))).toProvider(ascii); + + env.onStart(() -> { + log.info("\n{} v{}\n", ascii.get(), v); + }); } public Banner font(final String font) { diff --git a/jooby-banner/src/test/java/org/jooby/banner/BannerTest.java b/jooby-banner/src/test/java/org/jooby/banner/BannerTest.java index e49efd9a74..7b4e77dddc 100644 --- a/jooby-banner/src/test/java/org/jooby/banner/BannerTest.java +++ b/jooby-banner/src/test/java/org/jooby/banner/BannerTest.java @@ -1,6 +1,9 @@ package org.jooby.banner; import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; + +import javax.inject.Provider; import org.jooby.Env; import org.jooby.test.MockUnit; @@ -14,6 +17,9 @@ import com.github.lalyos.jfiglet.FigletFont; import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.binder.LinkedBindingBuilder; +import com.google.inject.name.Names; import com.typesafe.config.Config; import javaslang.control.Try.CheckedRunnable; @@ -34,6 +40,7 @@ public void configure() throws Exception { new MockUnit(Env.class, Config.class, Binder.class, Logger.class) .expect(conf("app", "1.0.0")) .expect(log("app")) + .expect(banner()) .expect(onStart) .run(unit -> { new Banner(banner) @@ -50,6 +57,7 @@ public void print() throws Exception { .expect(onStart) .expect(convertOnLine(banner, "speed")) .expect(print(banner, "1.0.0")) + .expect(banner()) .run(unit -> { new Banner(banner) .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class)); @@ -67,6 +75,7 @@ public void font() throws Exception { .expect(onStart) .expect(convertOnLine(banner, "myfont")) .expect(print(banner, "1.0.0")) + .expect(banner()) .run(unit -> { new Banner(banner) .font("myfont") @@ -85,6 +94,7 @@ public void defprint() throws Exception { .expect(onStart) .expect(convertOnLine(banner, "speed")) .expect(print(banner, "1.0.0")) + .expect(banner()) .run(unit -> { new Banner() .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class)); @@ -121,4 +131,16 @@ private Block conf(final String name, final String v) { expect(conf.getString("application.version")).andReturn(v); }; } + + @SuppressWarnings("unchecked") + private Block banner() { + return unit -> { + + LinkedBindingBuilder lbb = unit.mock(LinkedBindingBuilder.class); + expect(lbb.toProvider(isA(Provider.class))).andReturn(lbb); + + Binder binder = unit.get(Binder.class); + expect(binder.bind(Key.get(String.class, Names.named("application.banner")))).andReturn(lbb); + }; + } } diff --git a/jooby-crash/pom.xml b/jooby-crash/pom.xml new file mode 100644 index 0000000000..10ba135920 --- /dev/null +++ b/jooby-crash/pom.xml @@ -0,0 +1,183 @@ + + + + + org.jooby + jooby-project + 1.0.0-SNAPSHOT + + + 4.0.0 + jooby-crash + + crash shell module + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Test.java + **/*Feature.java + **/Issue*.java + + + + + + org.basepom.maven + duplicate-finder-maven-plugin + 1.2.1 + + + duplicate-dependencies + validate + + check + + + + crash/crash.properties + + + + + + + + + + + + + org.jooby + jooby + ${project.version} + + + + org.slf4j + jul-to-slf4j + ${slf4j-api.version} + + + + org.slf4j + log4j-over-slf4j + ${slf4j-api.version} + + + + org.slf4j + jcl-over-slf4j + ${slf4j-api.version} + + + + org.codehaus.groovy + groovy + + + + org.crashub + crash.shell + + + + + org.jooby + jooby + ${project.version} + test + tests + + + + junit + junit + test + + + + org.easymock + easymock + test + + + + org.powermock + powermock-api-easymock + test + + + + org.powermock + powermock-module-junit4 + test + + + + org.jacoco + org.jacoco.agent + runtime + test + + + + org.jooby + jooby-netty + ${project.version} + test + + + + org.jooby + jooby-jackson + ${project.version} + test + + + + org.jooby + jooby-banner + ${project.version} + test + + + + org.crashub + crash.connectors.telnet + test + + + + org.crashub + crash.connectors.web + test + + + + org.crashub + crash.connectors.ssh + test + + + + org.crashub + crash.plugins.mail + test + + + + org.crashub + crash.plugins.cron + test + + + + + diff --git a/jooby-crash/src/main/java/org/jooby/crash/Crash.java b/jooby-crash/src/main/java/org/jooby/crash/Crash.java new file mode 100644 index 0000000000..43fd4df154 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/Crash.java @@ -0,0 +1,328 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jooby.crash; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.Set; + +import org.crsh.plugin.CRaSHPlugin; +import org.crsh.plugin.PluginContext; +import org.jooby.Env; +import org.jooby.Jooby.Module; +import org.jooby.Registry; +import org.jooby.Route; +import org.jooby.WebSocket; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import com.google.common.collect.Sets; +import com.google.inject.Binder; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import com.google.inject.multibindings.Multibinder; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; + +import javaslang.concurrent.Promise; + +/** + *

crash

+ *

+ * Connect, monitor or use virtual machine resources via SSH, telnet or HTTP with + * CRaSH remote shell. + *

+ * + *

usage

+ * + *
{@code
+ * import org.jooby.crash;
+ *
+ * {
+ *   use(new Crash());
+ * }
+ * }
+ * + *

+ * That's all you need to get CRaSH up and running!!! + *

+ *

+ * Now is time to see how to connect and interact with the CRaSH + * shell + *

+ * + *

commands

+ *

+ * You can write additional shell commands using Groovy or Java, see the + * CRaSH documentation for + * details. CRaSH search for commands in the cmd folder. + *

+ *

+ * Here is a simple ‘hello’ command that could be loaded from cmd/hello.groovy folder: + *

+ *
{@code
+ * package commands
+ *
+ * import org.crsh.cli.Command
+ * import org.crsh.cli.Usage
+ * import org.crsh.command.InvocationContext
+ *
+ * class hello {
+ *
+ *  @Usage("Say Hello")
+ *  @Command
+ *  def main(InvocationContext context) {
+ *      return "Hello"
+ *  }
+ *
+ * }
+ * }
+ * + *

+ * Jooby adds some additional attributes and commands to InvocationContext that you can access from + * your command: + *

+ * + * + * + *

routes command

+ *

+ * The routes print all the application routes. + *

+ * + *

conf command

+ *

+ * The conf tree print the application configuration tree (configuration precedence). + *

+ * + *

+ * The conf props [path] print all the application properties, sub-tree or a single + * property if path argument is present. + *

+ * + *

connectors

+ * + *

HTTP connector

+ *

+ * The HTTP connector is a simple yet powerful collection of HTTP endpoints where you can + * run + * CRaSH + * command: + *

+ * + *
{@code
+ *
+ * {
+ *   use(new Crash()
+ *      .plugin(HttpShellPlugin.class)
+ *   );
+ * }
+ * }
+ * + *

+ * Try it: + *

+ * + *
+ * GET /api/shell/thread/ls
+ * 
+ * + *

+ * OR: + *

+ * + *
+ * GET /api/shell/thread ls
+ * 
+ * + *

+ * The connector listen at /api/shell. If you want to mount the connector some + * where + * else just set the property: crash.httpshell.path. + *

+ * + *

SSH connector

+ *

+ * Just add the crash.connectors.ssh + * dependency to your project. + *

+ * + *

+ * Try it: + *

+ * + *
+ * ssh -p 2000 admin@localhost
+ * 
+ * + *

+ * Default user and password is: admin. See how to provide a custom + * authentication + * plugin. + *

+ * + *

telnet connector

+ *

+ * Just add the crash.connectors.telnet + * dependency to your project. + *

+ * + *

+ * Try it: + *

+ * + *
+ * telnet localhost 5000
+ * 
+ * + *

+ * Checkout complete + * telnet + * connector configuration. + *

+ * + *

web connector

+ *

+ * Just add the crash.connectors.web + * dependency to your project. + *

+ * + *

+ * Try it: + *

+ * + *
+ * GET /shell
+ * 
+ * + *

+ * A web shell console will be ready to go at /shell. If you want to mount the + * connector some where else just set the property: crash.webshell.path. + *

+ * + * @author edgar + * @since 1.0.0 + */ +@SuppressWarnings({"unchecked", "rawtypes" }) +public class Crash implements Module { + + static { + if (!SLF4JBridgeHandler.isInstalled()) { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + } + } + + private static final Key> PLUGINS = Key + .get(new TypeLiteral>() { + }); + + private final Set plugins = new HashSet<>(); + + private final ClassLoader loader; + + /** + * Creates a new {@link Crash} module. + * + * @param loader Class loader to use or null. + */ + public Crash(final ClassLoader loader) { + this.loader = Optional.ofNullable(loader).orElse(getClass().getClassLoader()); + } + + /** + * Creates a new {@link Crash} module. + */ + public Crash() { + this(null); + } + + /** + * Add a custom plugin to CRaSH. + * + * @param plugin Plugin class. + * @return This module. + */ + public Crash plugin(final Class plugin) { + plugins.add(plugin); + return this; + } + + @Override + public void configure(final Env env, final Config conf, final Binder binder) { + Properties props = new Properties(); + if (conf.hasPath("crash")) { + conf.getConfig("crash").entrySet().forEach( + e -> props.setProperty("crash." + e.getKey(), e.getValue().unwrapped().toString())); + } + + Map attributes = new HashMap<>(); + attributes.put("conf", conf); + + // connectors.web on classpath? + boolean webShell = loader.getResource("META-INF/resources/js/crash.js") != null; + if (webShell) { + plugins.add(WebShellPlugin.class); + WebShellPlugin.install(env, conf); + } + + if (plugins.contains(HttpShellPlugin.class)) { + HttpShellPlugin.install(env, conf); + } + + Multibinder mb = Multibinder.newSetBinder(binder, CRaSHPlugin.class); + plugins.forEach(it -> mb.addBinding().to(it)); + + CrashBootstrap crash = new CrashBootstrap(); + + Promise promise = Promise.make(); + binder.bind(PluginContext.class).toProvider(promise.future()::get); + + env.onStart(r -> { + Set routes = r.require(Route.KEY); + Set sockets = r.require(WebSocket.KEY); + attributes.put("registry", r); + attributes.put("routes", routes); + attributes.put("websockets", sockets); + attributes.put("env", env); + + Set plugins = Sets.newHashSet(r.require(PLUGINS)); + ServiceLoader.load(CRaSHPlugin.class, this.loader).forEach(plugins::add); + + promise.success(crash.start(loader, props, attributes, plugins)); + }); + + env.onStop(crash::stop); + } + + @Override + public Config config() { + return ConfigFactory.parseResources(getClass(), "crash.conf"); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/CrashBootstrap.java b/jooby-crash/src/main/java/org/jooby/crash/CrashBootstrap.java new file mode 100644 index 0000000000..01ab724207 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/CrashBootstrap.java @@ -0,0 +1,109 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jooby.crash; + +import static java.util.Arrays.asList; +import static javaslang.Tuple.of; +import static org.jooby.crash.CrashFSDriver.endsWith; +import static org.jooby.crash.CrashFSDriver.noneOf; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +import org.crsh.plugin.CRaSHPlugin; +import org.crsh.plugin.PluginContext; +import org.crsh.plugin.PluginLifeCycle; +import org.crsh.vfs.FS; + +import javaslang.control.Try; + +class CrashBootstrap extends PluginLifeCycle { + + private static final Predicate ACCEPT = endsWith(".class").negate(); + + private List drivers = new ArrayList<>(); + + public PluginContext start(final ClassLoader loader, final Properties props, + final Map attributes, final Set> plugins) throws IOException { + FS conffs = newFS(CrashFSDriver.parse(loader, asList( + of("crash", ACCEPT)))); + + FS cmdfs = newFS(CrashFSDriver.parse(loader, asList( + of("cmd", ACCEPT), + of("org/jooby/crash", ACCEPT), + of("crash/commands", noneOf("jndi.groovy", "jdbc.groovy", "jpa.groovy", "jul.groovy"))))); + + setConfig(props); + + PluginContext ctx = new PluginContext(executor("crash"), scanner("crash-scanner"), + () -> plugins, attributes, cmdfs, conffs, loader); + + ctx.refresh(); + + start(ctx); + + return ctx; + } + + public void shutdown() { + drivers.forEach(it -> Try.run(it::close)); + super.stop(); + } + + private FS newFS(final List drivers) throws IOException { + FS fs = new FS(); + for (CrashFSDriver driver : drivers) { + fs.mount(driver); + } + this.drivers.addAll(drivers); + return fs; + } + + private static ScheduledExecutorService scanner(final String name) { + return Executors.newScheduledThreadPool(1, r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setName(name); + return thread; + }); + } + + private static ExecutorService executor(final String name) { + AtomicInteger next = new AtomicInteger(0); + return new ThreadPoolExecutor(0, 10, 60L, TimeUnit.SECONDS, new SynchronousQueue(), + r -> { + Thread thread = Executors.defaultThreadFactory().newThread(r); + thread.setName(name + "-" + next.incrementAndGet()); + return thread; + }); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/CrashFSDriver.java b/jooby-crash/src/main/java/org/jooby/crash/CrashFSDriver.java new file mode 100644 index 0000000000..276bca351c --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/CrashFSDriver.java @@ -0,0 +1,207 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jooby.crash; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.crsh.vfs.spi.AbstractFSDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableList; + +import javaslang.Predicates; +import javaslang.Tuple2; +import javaslang.control.Try; + +class CrashFSDriver extends AbstractFSDriver implements AutoCloseable { + + private static final String LOGIN = "login.groovy"; + + private static Predicate FLOGIN = fname(LOGIN); + + private static final String JAR = ".jar!"; + + /** The logging system. */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + private Path root; + + private FileSystem fs; + + private Predicate filter; + + private URI src; + + private CrashFSDriver(final URI src, final FileSystem fs, final Predicate filter) { + this.src = src; + this.fs = fs; + String path = src.toString(); + int jar = path.indexOf(JAR); + path = path.substring(jar + JAR.length()); + this.root = fs.getPath(path); + this.filter = filter; + } + + private CrashFSDriver(final URI src, final Path root, final Predicate filter) { + this.src = src; + this.root = root; + this.filter = filter; + } + + @Override + public Path root() throws IOException { + return root; + } + + @Override + public String name(final Path handle) throws IOException { + return Optional.ofNullable(handle.getFileName()).orElse(handle).toString().replaceAll("/", ""); + } + + @Override + public boolean isDir(final Path handle) throws IOException { + return Files.isDirectory(handle); + } + + @Override + public Iterable children(final Path handle) throws IOException { + try (Stream walk = Files.walk(handle)) { + List children = walk + .skip(1) + .filter(filter) + .collect(Collectors.toList()); + return children; + } + } + + public boolean exists(final Predicate filter) { + try (Stream walk = Try.of(() -> Files.walk(root)).getOrElse(Stream.empty())) { + return walk + .skip(1) + .filter(filter) + .findFirst() + .isPresent(); + } + } + + @Override + public long getLastModified(final Path handle) throws IOException { + return Try.of(() -> Files.getLastModifiedTime(handle).toMillis()).getOrElse(-1L); + } + + @Override + public Iterator open(final Path handle) throws IOException { + return ImmutableList.of(Files.newInputStream(handle)).iterator(); + } + + @Override + public void close() throws Exception { + if (fs != null) { + fs.close(); + } + } + + @Override + public String toString() { + return root + " -> " + src; + } + + public static List parse(final ClassLoader loader, + final List>> paths) { + List drivers = new ArrayList<>(); + boolean loginFound = false; + + for (Tuple2> path : paths) { + for (URI uri : expandPath(loader, path._1)) { + // classpath first + CrashFSDriver driver = Try.of(() -> FileSystems.newFileSystem(uri, Collections.emptyMap())) + .map(it -> Try.of(() -> new CrashFSDriver(uri, it, path._2)).get()) + // file system fallback + .recoverWith(x -> Try.of(() -> new CrashFSDriver(uri, Paths.get(uri), path._2))) + .get(); + // HACK to make sure login.groovy takes precedence over default one + driver.log.debug("driver created: {}", driver); + if (loginFound) { + driver.filter = FLOGIN.negate().and(driver.filter); + } else if (driver.exists(FLOGIN)) { + driver.log.debug(" login.groovy found: {}", driver); + loginFound = true; + } + drivers.add(driver); + } + } + return drivers; + } + + private static List expandPath(final ClassLoader loader, final String pattern) { + List result = new ArrayList<>(); + File file = new File(pattern); + if (file.exists()) { + result.add(file.toURI()); + } + Try.run(() -> { + Collections.list(loader.getResources(pattern)) + .stream() + .map(it -> Try.of(() -> it.toURI()).get()) + .forEach(result::add); + }); + return result; + } + + @SuppressWarnings({"rawtypes", "unchecked" }) + public static Predicate noneOf(final String... names) { + Predicate[] predicates = new Predicate[names.length]; + for (int i = 0; i < predicates.length; i++) { + predicates[i] = fname(names[i]); + } + return Predicates.noneOf(predicates); + } + + public static Predicate fname(final String name) { + return it -> Optional.ofNullable(it.getFileName()) + .map(Object::toString) + .orElse(it.toString()) + .equals(name); + } + + public static Predicate endsWith(final String ext) { + return it -> Optional.ofNullable(it.getFileName()) + .map(Object::toString) + .orElse(it.toString()) + .endsWith(ext); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/HttpShellPlugin.java b/jooby-crash/src/main/java/org/jooby/crash/HttpShellPlugin.java new file mode 100644 index 0000000000..cc7777f6b8 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/HttpShellPlugin.java @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jooby.crash; + +import org.crsh.plugin.CRaSHPlugin; +import org.crsh.plugin.PluginContext; +import org.crsh.shell.Shell; +import org.crsh.shell.ShellFactory; +import org.crsh.shell.ShellProcess; +import org.crsh.shell.ShellProcessContext; +import org.jooby.Deferred; +import org.jooby.Env; +import org.jooby.MediaType; + +import com.typesafe.config.Config; + +public class HttpShellPlugin extends CRaSHPlugin { + + @Override + public HttpShellPlugin getImplementation() { + return this; + } + + static void install(final Env env, final Config conf) { + String path = conf.getString("crash.httpshell.path"); + env.routes().get(path + "/{cmd:.*}", req -> { + MediaType type = req.accepts(MediaType.json).map(it -> MediaType.json) + .orElse(MediaType.plain); + + return new Deferred(deferred -> { + PluginContext ctx = req.require(PluginContext.class); + ShellFactory shellFactory = ctx.getPlugin(ShellFactory.class); + Shell shell = shellFactory.create(null); + String cmd = req.param("cmd").value().replaceAll("/", " "); + ShellProcess process = shell.createProcess(cmd); + ShellProcessContext spc = new SimpleProcessContext( + result -> deferred.resolve(result.type(type))); + process.execute(spc); + }); + }); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/SimpleProcessContext.java b/jooby-crash/src/main/java/org/jooby/crash/SimpleProcessContext.java new file mode 100644 index 0000000000..9d25fc4cca --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/SimpleProcessContext.java @@ -0,0 +1,135 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jooby.crash; + +import static javaslang.API.$; +import static javaslang.API.Case; +import static javaslang.API.Match; +import static javaslang.Predicates.instanceOf; + +import java.io.IOException; +import java.util.function.Consumer; + +import org.crsh.shell.ShellProcessContext; +import org.crsh.shell.ShellResponse; +import org.crsh.text.Screenable; +import org.crsh.text.Style; +import org.jooby.Result; +import org.jooby.Results; + +class SimpleProcessContext implements ShellProcessContext { + + private StringBuilder buff = new StringBuilder(); + + private Consumer deferred; + + private int width; + + private int height; + + public SimpleProcessContext(final Consumer deferred) { + this(deferred, 204, 48); + } + + public SimpleProcessContext(final Consumer deferred, final int width, + final int height) { + this.deferred = deferred; + this.width = width; + this.height = height; + } + + @Override + public boolean takeAlternateBuffer() throws IOException { + return false; + } + + @Override + public boolean releaseAlternateBuffer() throws IOException { + return false; + } + + @Override + public String getProperty(final String propertyName) { + return null; + } + + @Override + public String readLine(final String msg, final boolean echo) + throws IOException, InterruptedException, IllegalStateException { + return null; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public void flush() throws IOException { + } + + @Override + public Screenable append(final Style style) throws IOException { + return this; + } + + @Override + public Screenable cls() throws IOException { + return this; + } + + @Override + public Appendable append(final CharSequence csq) throws IOException { + buff.append(csq); + return this; + } + + @Override + public Appendable append(final CharSequence csq, final int start, final int end) + throws IOException { + buff.append(csq, start, end); + return this; + } + + @Override + public Appendable append(final char c) throws IOException { + buff.append(c); + return this; + } + + @Override + public void end(final ShellResponse response) { + org.jooby.Status status = Match(response).of( + Case(instanceOf(ShellResponse.Ok.class), org.jooby.Status.OK), + Case(instanceOf(ShellResponse.UnknownCommand.class), org.jooby.Status.BAD_REQUEST), + Case($(), org.jooby.Status.SERVER_ERROR)); + + deferred.accept(Results.with(buff.length() == 0 ? response.getMessage() : buff, status)); + } + + @Override + public String toString() { + return buff.toString(); + } +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/WebShellHandler.java b/jooby-crash/src/main/java/org/jooby/crash/WebShellHandler.java new file mode 100644 index 0000000000..d06bf4c968 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/WebShellHandler.java @@ -0,0 +1,142 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jooby.crash; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.crsh.cli.impl.Delimiter; +import org.crsh.cli.impl.completion.CompletionMatch; +import org.crsh.cli.spi.Completion; +import org.crsh.plugin.PluginContext; +import org.crsh.shell.Shell; +import org.crsh.shell.ShellFactory; +import org.crsh.shell.ShellProcess; +import org.crsh.util.Utils; +import org.jooby.Request; +import org.jooby.WebSocket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableMap; + +import javaslang.control.Try; + +class WebShellHandler implements WebSocket.FullHandler { + + /** The logging system. */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + @SuppressWarnings("rawtypes") + @Override + public void connect(final Request req, final WebSocket ws) throws Exception { + PluginContext ctx = req.require(PluginContext.class); + ShellFactory factory = ctx.getPlugin(ShellFactory.class); + Shell shell = factory.create(null); + + AtomicReference process = new AtomicReference(); + + ws.onMessage(msg -> { + Map event = msg.to(Map.class); + String type = (String) event.get("type"); + if (type.equals("welcome")) { + log.debug("sending welcome + prompt"); + ws.send(event("print", shell.getWelcome())); + ws.send(event("prompt", shell.getPrompt())); + } else if (type.equals("execute")) { + String command = (String) event.get("command"); + Integer width = (Integer) event.get("width"); + Integer height = (Integer) event.get("height"); + process.set(shell.createProcess(command)); + SimpleProcessContext context = new SimpleProcessContext(r -> { + Try.run(() -> { + // reset process + process.set(null); + + ws.send(event("print", r.get())); + ws.send(event("prompt", shell.getPrompt())); + ws.send(event("end")); + }).onFailure(x -> log.error("error found while sending output", x)); + + if ("bye".equals(command)) { + ws.close(WebSocket.NORMAL); + } + }, width, height); + log.debug("executing {}", command); + process.get().execute(context); + } else if (type.equals("cancel")) { + ShellProcess p = process.get(); + if (p != null) { + log.info("cancelling {}", p); + p.cancel(); + } + } else if (type.equals("complete")) { + String prefix = (String) event.get("prefix"); + CompletionMatch completion = shell.complete(prefix); + Completion completions = completion.getValue(); + Delimiter delimiter = completion.getDelimiter(); + StringBuilder sb = new StringBuilder(); + List values = new ArrayList(); + if (completions.getSize() == 1) { + String value = completions.getValues().iterator().next(); + delimiter.escape(value, sb); + if (completions.get(value)) { + sb.append(delimiter.getValue()); + } + values.add(sb.toString()); + } else { + String commonCompletion = Utils.findLongestCommonPrefix(completions.getValues()); + if (commonCompletion.length() > 0) { + delimiter.escape(commonCompletion, sb); + values.add(sb.toString()); + } else { + for (Map.Entry entry : completions) { + delimiter.escape(entry.getKey(), sb); + values.add(sb.toString()); + sb.setLength(0); + } + } + } + log.debug("completing {} with {}", prefix, values); + ws.send(event("complete", values)); + } + }); + + // clean up on close + ws.onClose(status -> { + log.info("closing web-socket"); + ShellProcess sp = process.get(); + if (sp != null) { + sp.cancel(); + } + shell.close(); + }); + } + + private Object event(final String type, final Object data) { + return ImmutableMap.of("type", type, "data", data); + } + + private Object event(final String type) { + return ImmutableMap.of("type", type); + } + +} diff --git a/jooby-crash/src/main/java/org/jooby/crash/WebShellPlugin.java b/jooby-crash/src/main/java/org/jooby/crash/WebShellPlugin.java new file mode 100644 index 0000000000..14f170ab20 --- /dev/null +++ b/jooby-crash/src/main/java/org/jooby/crash/WebShellPlugin.java @@ -0,0 +1,84 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jooby.crash; + +import org.crsh.plugin.CRaSHPlugin; +import org.jooby.Env; +import org.jooby.MediaType; +import org.jooby.Results; +import org.jooby.Route; +import org.jooby.Routes; + +import com.typesafe.config.Config; + +class WebShellPlugin extends CRaSHPlugin { + + @Override + public WebShellPlugin getImplementation() { + return this; + } + + static void install(final Env env, final Config conf) { + Routes routes = env.routes(); + String path = conf.getString("crash.webshell.path"); + String title = conf.getString("application.name") + " shell"; + routes.assets(path + "/css/**", "META-INF/resources/css/{0}"); + routes.assets(path + "/js/**", "META-INF/resources/js/{0}"); + String rootpath = Route.normalize(conf.getString("application.path") + path); + + routes.get(path, req -> Results.ok("\n" + + "\n" + + "\n" + + " \n" + + " " + title + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
\n" + + "\n" + + "\n" + + "").type(MediaType.html)); + + routes.ws(path, new WebShellHandler()).consumes(MediaType.json).produces(MediaType.json); + } + +} diff --git a/jooby-crash/src/main/resources/org/jooby/crash/conf.groovy b/jooby-crash/src/main/resources/org/jooby/crash/conf.groovy new file mode 100644 index 0000000000..0ce426e51d --- /dev/null +++ b/jooby-crash/src/main/resources/org/jooby/crash/conf.groovy @@ -0,0 +1,78 @@ +package org.jooby.crash + +import org.crsh.cli.Usage +import org.crsh.cli.Command +import org.crsh.cli.completers.SystemPropertyNameCompleter +import org.crsh.command.InvocationContext +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import org.crsh.cli.Man +import org.crsh.cli.Argument +import org.crsh.cli.Option + +import org.crsh.cli.completers.EnumCompleter +import java.util.regex.Pattern +import org.crsh.cli.Required + +import com.google.common.base.Strings; + +@Usage("application properties commands") +class conf +{ + + @Usage("print configuration tree") + @Command + Object tree() { + return configTree(context.attributes.conf.origin().description()) + } + + @Usage("print properties") + @Command + void props(InvocationContext context, + @Usage("Apply a path filter, either a property name or config sub-tree") + @Argument + String path) { + + def conf = context.attributes.conf + try { + def local = path == null ? conf : conf.getConfig(path) + def prefix = path == null ? "" : path + "." + + local.entrySet().each { + context.provide([name: prefix + it.key, value: it.value.unwrapped()]) + } + } catch (Exception x) { + context.provide([name: path, value: conf.getAnyRef(path)]) + } + } + + private String configTree(final String description) { + return configTree(description.split(":\\s+\\d+,|,"), 0); + } + + private String configTree(final String[] sources, final int i) { + if (i < sources.length) { + return new StringBuilder() + .append(Strings.padStart("", i, (char)' ')) + .append("└── ") + .append(sources[i]) + .append("\n") + .append(configTree(sources, i + 1)) + .toString(); + } + return ""; + } + +} + +@Retention(RetentionPolicy.RUNTIME) +@Usage("the property name") +@Man("The name of the property") +@Argument(name = "name", completer = SystemPropertyNameCompleter.class) +@interface PropertyName { } + +@Retention(RetentionPolicy.RUNTIME) +@Usage("the property value") +@Man("The value of the property") +@Argument(name = "value") +@interface PropertyValue { } diff --git a/jooby-crash/src/main/resources/org/jooby/crash/crash.conf b/jooby-crash/src/main/resources/org/jooby/crash/crash.conf new file mode 100644 index 0000000000..61ba6fbac5 --- /dev/null +++ b/jooby-crash/src/main/resources/org/jooby/crash/crash.conf @@ -0,0 +1,2 @@ +crash.webshell.path = /shell +crash.httpshell.path = /api/shell diff --git a/jooby-crash/src/main/resources/org/jooby/crash/login.groovy b/jooby-crash/src/main/resources/org/jooby/crash/login.groovy new file mode 100644 index 0000000000..49a07c3cbe --- /dev/null +++ b/jooby-crash/src/main/resources/org/jooby/crash/login.groovy @@ -0,0 +1,19 @@ +welcome = { -> + def registry = crash.context.attributes.registry + def conf = crash.context.attributes.conf + def appname = conf.getString("application.name") + def version = conf.getString("application.version") + + try { + return registry.require("application.banner", String.class) + " v" + version + "\n" + } catch (Exception x) { + return """ +Welcome to $appname v$version +""" + } +} + +prompt = { -> + def env = crash.context.attributes.conf.getString("application.env") + return env + "> "; +} \ No newline at end of file diff --git a/jooby-crash/src/main/resources/org/jooby/crash/routes.groovy b/jooby-crash/src/main/resources/org/jooby/crash/routes.groovy new file mode 100644 index 0000000000..aca1836ef2 --- /dev/null +++ b/jooby-crash/src/main/resources/org/jooby/crash/routes.groovy @@ -0,0 +1,43 @@ +package org.jooby.crash + +import org.crsh.cli.Usage +import org.crsh.cli.Command + +import static javaslang.API.$; +import static javaslang.API.Case; +import static javaslang.API.Match; +import static javaslang.Predicates.instanceOf; + +import org.jooby.Route; + +class routes +{ + @Usage("print application routes") + @Command + void main(InvocationContext context) { + def routes = context.attributes.routes + + routes.each { + def order = 0; + def filter = it.filter() + def pattern = it.pattern() + + if (filter instanceof Route.Before) { + pattern = "{before}" + pattern + } else if (filter instanceof Route.After) { + pattern = "{after}" + pattern + } else if (filter instanceof Route.Complete) { + pattern = "{complete}" + pattern + } + + context.provide([order:order, method: it.method(), pattern: pattern, consumes: it.consumes(), produces: it.produces(), name: it.name(), source: it.source()]) + order += 1 + } + + context.provide('\n\n') + def sockets = context.attributes.websockets + sockets.each { + context.provide([method: 'WS', pattern: it.pattern, consumes: it.consumes(), produces: it.produces()]) + } + } +} diff --git a/jooby-crash/src/test/java/app/AuthPlugin.java b/jooby-crash/src/test/java/app/AuthPlugin.java new file mode 100644 index 0000000000..7db7039007 --- /dev/null +++ b/jooby-crash/src/test/java/app/AuthPlugin.java @@ -0,0 +1,35 @@ +package app; + +import org.crsh.auth.AuthenticationPlugin; +import org.crsh.plugin.CRaSHPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AuthPlugin extends CRaSHPlugin + implements AuthenticationPlugin { + + /** The logging system. */ + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Override + public AuthPlugin getImplementation() { + return this; + } + + @Override + public String getName() { + return "auth"; + } + + @Override + public Class getCredentialType() { + return String.class; + } + + @Override + public boolean authenticate(final String username, final String credential) throws Exception { + log.info("{} pass {}", username, credential); + return true; + } + +} diff --git a/jooby-crash/src/test/java/app/CrashApp.java b/jooby-crash/src/test/java/app/CrashApp.java new file mode 100644 index 0000000000..73e16d18fa --- /dev/null +++ b/jooby-crash/src/test/java/app/CrashApp.java @@ -0,0 +1,38 @@ +package app; + +import org.jooby.Jooby; +import org.jooby.banner.Banner; +import org.jooby.crash.HttpShellPlugin; +import org.jooby.crash.Crash; +import org.jooby.json.Jackson; + +public class CrashApp extends Jooby { + { + conf("crash.conf"); + + use(new Jackson()); + + use(new Banner("crash me")); + + use(new Crash() + .plugin(HttpShellPlugin.class) + .plugin(AuthPlugin.class)); + + before("/path", (req, rsp) -> { + }); + + after("/path", (req, rsp, v) -> { + return v; + }); + + complete("/path", (req, rsp, v) -> { + }); + + get("/", () -> "OK"); + + } + + public static void main(final String[] args) { + run(CrashApp::new, args); + } +} diff --git a/jooby-crash/src/test/resources/crash.conf b/jooby-crash/src/test/resources/crash.conf new file mode 100644 index 0000000000..188edbcf8a --- /dev/null +++ b/jooby-crash/src/test/resources/crash.conf @@ -0,0 +1,8 @@ +crash.auth=auth +crash.mail.smtp.host=smtp.gmail.com +crash.mail.smtp.port=587 +crash.mail.smtp.secure=tls +crash.mail.smtp.username="espina.edgar@gmail.com" +crash.mail.smtp.password=th3w0rld1sm1n3 +crash.mail.smtp.from="espina.edgar@gmail.com" +crash.mail.debug=true diff --git a/jooby-dist/src/main/resources/assemblies/jooby.flexible.zip.xml b/jooby-dist/src/main/resources/assemblies/jooby.flexible.zip.xml index f1938901c6..d5d3a7cb63 100644 --- a/jooby-dist/src/main/resources/assemblies/jooby.flexible.zip.xml +++ b/jooby-dist/src/main/resources/assemblies/jooby.flexible.zip.xml @@ -83,6 +83,16 @@ + + + ${project.basedir}${file.separator}cmd + + cmd + + **/* + + + \ No newline at end of file diff --git a/jooby-dist/src/main/resources/assemblies/jooby.stork.xml b/jooby-dist/src/main/resources/assemblies/jooby.stork.xml index ccd5631a49..7ca9e824ba 100644 --- a/jooby-dist/src/main/resources/assemblies/jooby.stork.xml +++ b/jooby-dist/src/main/resources/assemblies/jooby.stork.xml @@ -55,6 +55,17 @@ + + + ${project.basedir}${file.separator}cmd + + cmd + true + + **/* + + + ${project.basedir}${file.separator}conf @@ -92,16 +103,6 @@ lib runtime - - *:* - - - - lib - compile - - *:* - diff --git a/jooby-dist/src/main/resources/assemblies/jooby.war.xml b/jooby-dist/src/main/resources/assemblies/jooby.war.xml index 6a7c0a5a03..feb6229c5e 100644 --- a/jooby-dist/src/main/resources/assemblies/jooby.war.xml +++ b/jooby-dist/src/main/resources/assemblies/jooby.war.xml @@ -141,6 +141,17 @@ + + + + ${project.build.outputDirectory} + + WEB-INF${file.separator}cmd + + **/* + + + diff --git a/jooby-dist/src/main/resources/assemblies/jooby.zip.xml b/jooby-dist/src/main/resources/assemblies/jooby.zip.xml index 8bc810d3f3..b4be06585e 100644 --- a/jooby-dist/src/main/resources/assemblies/jooby.zip.xml +++ b/jooby-dist/src/main/resources/assemblies/jooby.zip.xml @@ -58,6 +58,16 @@ + + + ${project.basedir}${file.separator}cmd + + cmd + + **/* + + + ${project.basedir}${file.separator}public diff --git a/jooby-undertow/src/main/java/org/jooby/internal/undertow/UndertowRequest.java b/jooby-undertow/src/main/java/org/jooby/internal/undertow/UndertowRequest.java index c7f60fbd50..95736db12b 100644 --- a/jooby-undertow/src/main/java/org/jooby/internal/undertow/UndertowRequest.java +++ b/jooby-undertow/src/main/java/org/jooby/internal/undertow/UndertowRequest.java @@ -53,6 +53,8 @@ import io.undertow.util.AttachmentKey; import io.undertow.util.HeaderValues; import io.undertow.util.HttpString; +import javaslang.Lazy; +import javaslang.control.Try; public class UndertowRequest implements NativeRequest { @@ -63,7 +65,7 @@ public class UndertowRequest implements NativeRequest { private Config config; - private final FormData form; + private final Lazy form; private final String path; @@ -74,8 +76,8 @@ public UndertowRequest(final HttpServerExchange exchange, final Config conf) this.exchange = exchange; this.blocking = Suppliers.memoize(() -> this.exchange.startBlocking()); this.config = conf; - this.form = parseForm(exchange, conf.getString("application.tmpdir"), - conf.getString("application.charset")); + this.form = Lazy.of(() -> Try.of(() -> parseForm(exchange, conf.getString("application.tmpdir"), + conf.getString("application.charset"))).get()); this.path = URLDecoder.decode(exchange.getRequestPath(), "UTF-8"); } @@ -93,9 +95,10 @@ public String path() { public List paramNames() { ImmutableList.Builder builder = ImmutableList. builder(); builder.addAll(exchange.getQueryParameters().keySet()); - form.forEach(v -> { + FormData formdata = this.form.get(); + formdata.forEach(v -> { // excludes upload from param names. - if (!form.getFirst(v).isFile()) { + if (!formdata.getFirst(v).isFile()) { builder.add(v); } }); @@ -111,7 +114,7 @@ public List params(final String name) { query.stream().forEach(builder::add); } // form params - Optional.ofNullable(form.get(name)).ifPresent(values -> { + Optional.ofNullable(form.get().get(name)).ifPresent(values -> { values.stream().forEach(value -> { if (!value.isFile()) { builder.add(value.getValue()); @@ -150,7 +153,7 @@ public List cookies() { @Override public List files(final String name) { Builder builder = ImmutableList.builder(); - Deque values = form.get(name); + Deque values = form.get().get(name); if (values != null) { values.forEach(value -> { if (value.isFile()) { diff --git a/jooby-undertow/src/test/java/org/jooby/undertow/UndertowRequestTest.java b/jooby-undertow/src/test/java/org/jooby/undertow/UndertowRequestTest.java index 22d2c84692..d43a6aa57a 100644 --- a/jooby-undertow/src/test/java/org/jooby/undertow/UndertowRequestTest.java +++ b/jooby-undertow/src/test/java/org/jooby/undertow/UndertowRequestTest.java @@ -20,15 +20,7 @@ public class UndertowRequestTest { private Block form = unit -> { - Config conf = unit.get(Config.class); - expect(conf.getString("application.tmpdir")).andReturn("target"); - expect(conf.getString("application.charset")).andReturn("UTF-8"); - - HeaderMap headers = unit.mock(HeaderMap.class); - expect(headers.getFirst("Content-Type")).andReturn(null); - HttpServerExchange exchange = unit.get(HttpServerExchange.class); - expect(exchange.getRequestHeaders()).andReturn(headers); expect(exchange.getRequestPath()).andReturn("/"); }; diff --git a/jooby/src/main/java/org/jooby/Jooby.java b/jooby/src/main/java/org/jooby/Jooby.java index 7e59fee406..601fdf86ec 100644 --- a/jooby/src/main/java/org/jooby/Jooby.java +++ b/jooby/src/main/java/org/jooby/Jooby.java @@ -92,6 +92,7 @@ import org.jooby.Route.Definition; import org.jooby.Route.Mapper; +import org.jooby.Route.OneArgHandler; import org.jooby.Session.Store; import org.jooby.handlers.AssetHandler; import org.jooby.internal.AppPrinter; @@ -1018,188 +1019,45 @@ public T require(final Key type) { return injector.getInstance(type); } - /** - * Produces a deferred response, useful for async request processing. - * - *

usage

- * - *
-   * {
-   *    ExecutorService executor = ...;
-   *
-   *    get("/async", promise(deferred {@literal ->} {
-   *      executor.execute(() {@literal ->} {
-   *        try {
-   *          deferred.resolve(...); // success value
-   *        } catch (Exception ex) {
-   *          deferred.reject(ex); // error value
-   *        }
-   *      });
-   *    }));
-   *  }
-   * 
- * - *

- * Or with automatic error handler: - *

- * - *
-   * {
-   *    ExecutorService executor = ...;
-   *
-   *    get("/async", promise(deferred {@literal ->} {
-   *      executor.execute(() {@literal ->} {
-   *        deferred.resolve(() {@literal ->} {
-   *          Object value = ...
-   *          return value;
-   *        }); // success value
-   *      });
-   *    }));
-   *  }
-   * 
- * - *

- * Or as {@link Runnable} with automatic error handler: - *

- * - *
-   * {
-   *    ExecutorService executor = ...;
-   *
-   *    get("/async", promise(deferred {@literal ->} {
-   *      executor.execute(deferred.run(() {@literal ->} {
-   *        Object value = ...
-   *        return value;
-   *      }); // success value
-   *    }));
-   *  }
-   * 
- * - * @param initializer Deferred initializer. - * @return A new deferred handler. - * @see Deferred - */ + @Override public Route.OneArgHandler promise(final Deferred.Initializer initializer) { return req -> { return new Deferred(initializer); }; } - /** - * Produces a deferred response, useful for async request processing. - * - *

usage

- * - *
-   * {
-   *    get("/async", promise("myexec", deferred {@literal ->} {
-   *      // resolve a success value
-   *      deferred.resolve(...);
-   *    }));
-   *  }
-   * 
- * - * @param executor Executor to run the deferred. - * @param initializer Deferred initializer. - * @return A new deferred handler. - * @see Deferred - */ + @Override public Route.OneArgHandler promise(final String executor, final Deferred.Initializer initializer) { return req -> new Deferred(executor, initializer); } - /** - * Produces a deferred response, useful for async request processing. - * - *

usage

- * - *
-   * {
-   *    ExecutorService executor = ...;
-   *
-   *    get("/async", promise(deferred {@literal ->} {
-   *      executor.execute(() {@literal ->} {
-   *        try {
-   *          deferred.resolve(...); // success value
-   *        } catch (Exception ex) {
-   *          deferred.reject(ex); // error value
-   *        }
-   *      });
-   *    }));
-   *  }
-   * 
- * - *

- * Or with automatic error handler: - *

- * - *
-   * {
-   *    ExecutorService executor = ...;
-   *
-   *    get("/async", promise(deferred {@literal ->} {
-   *      executor.execute(() {@literal ->} {
-   *        deferred.resolve(() {@literal ->} {
-   *          Object value = ...
-   *          return value;
-   *        }); // success value
-   *      });
-   *    }));
-   *  }
-   * 
- * - *

- * Or as {@link Runnable} with automatic error handler: - *

- * - *
-   * {
-   *    ExecutorService executor = ...;
-   *
-   *    get("/async", promise(deferred {@literal ->} {
-   *      executor.execute(deferred.run(() {@literal ->} {
-   *        Object value = ...
-   *        return value;
-   *      }); // success value
-   *    }));
-   *  }
-   * 
- * - * @param initializer Deferred initializer. - * @return A new deferred handler. - * @see Deferred - */ + @Override public Route.OneArgHandler promise(final Deferred.Initializer0 initializer) { return req -> { return new Deferred(initializer); }; } - /** - * Produces a deferred response, useful for async request processing. - * - *

usage

- * - *
-   * {
-   *    get("/async", promise("myexec", deferred {@literal ->} {
-   *      // resolve a success value
-   *      deferred.resolve(...);
-   *    }));
-   *  }
-   * 
- * - * @param executor Executor to run the deferred. - * @param initializer Deferred initializer. - * @return A new deferred handler. - * @see Deferred - */ + @Override public Route.OneArgHandler promise(final String executor, final Deferred.Initializer0 initializer) { return req -> new Deferred(executor, initializer); } + @Override + public OneArgHandler deferred(final String executor, final OneArgHandler handler) { + return req -> { + return new Deferred(executor, deferred -> { + try { + deferred.resolve(handler.handle(req)); + } catch (Throwable x) { + deferred.reject(x); + } + }); + }; + } + /** * Setup a session store to use. Useful if you want/need to persist sessions between shutdowns, or * save data in redis, memcached, mongodb, couchbase, etc.. @@ -3271,7 +3129,8 @@ public Route.Definition assets(final String path) { @Override public Route.Definition assets(final String path, final String location) { AssetHandler handler = new AssetHandler(location); - on("*", conf -> { + onStart(r -> { + Config conf = r.require(Config.class); handler .cdn(conf.getString("assets.cdn")) .lastModified(conf.getBoolean("assets.lastModified")) diff --git a/jooby/src/main/java/org/jooby/Request.java b/jooby/src/main/java/org/jooby/Request.java index 9f22873f9d..f8badc2afe 100644 --- a/jooby/src/main/java/org/jooby/Request.java +++ b/jooby/src/main/java/org/jooby/Request.java @@ -648,6 +648,17 @@ default T params(final Class type) { return params().to(type); } + /** + * Short version of params().to(type). + * + * @param type Object type. + * @param Value type. + * @return Instance of object. + */ + default T form(final Class type) { + return params().to(type); + } + /** * Short version of params(xss).to(type). * @@ -660,6 +671,18 @@ default T params(final Class type, final String... xss) { return params(xss).to(type); } + /** + * Short version of params(xss).to(type). + * + * @param type Object type. + * @param xss Xss filter to apply. + * @param Value type. + * @return Instance of object. + */ + default T form(final Class type, final String... xss) { + return params(xss).to(type); + } + /** * Get a HTTP request parameter under the given name. A HTTP parameter can be provided in any of * these forms: @@ -777,7 +800,8 @@ default List files(final String name) { List cookies(); /** - * HTTP body. + * HTTP body. Please don't use this method for form submits. This method is used for getting + * raw data or a data like json, xml, etc... * * @return The HTTP body. * @throws Exception If body can't be converted or there is no HTTP body. @@ -787,6 +811,9 @@ default List files(final String name) { /** * Short version of body().to(type). * + * HTTP body. Please don't use this method for form submits. This method is used for getting + * raw or a parsed data like json, xml, etc... + * * @param type Object type. * @param Value type. * @return Instance of object. diff --git a/jooby/src/main/java/org/jooby/Route.java b/jooby/src/main/java/org/jooby/Route.java index ba9f51b385..db8092aa4d 100644 --- a/jooby/src/main/java/org/jooby/Route.java +++ b/jooby/src/main/java/org/jooby/Route.java @@ -1008,7 +1008,8 @@ public Collection map(final Mapper mapper) { class Definition implements Props { private static final SourceProvider SRC = SourceProvider.DEFAULT_INSTANCE - .plusSkippedClasses(Definition.class, Jooby.class, Collection.class, Group.class); + .plusSkippedClasses(Definition.class, Jooby.class, Collection.class, Group.class, + javaslang.collection.List.class, Routes.class); /** * Route's name. @@ -1173,6 +1174,15 @@ public boolean glob() { return cpattern.glob(); } + /** + * Source information (where the route was defined). + * + * @return Source information (where the route was defined). + */ + public Route.Source source() { + return new RouteSourceImpl(declaringClass, line); + } + /** * Recreate a route path and apply the given variables. * @@ -2229,7 +2239,9 @@ static String normalize(final String path) { } /** - * @return Source information. + * Source information (where the route was defined). + * + * @return Source information (where the route was defined). */ Route.Source source(); diff --git a/jooby/src/main/java/org/jooby/Routes.java b/jooby/src/main/java/org/jooby/Routes.java index 9487b08439..6a3a4cb325 100644 --- a/jooby/src/main/java/org/jooby/Routes.java +++ b/jooby/src/main/java/org/jooby/Routes.java @@ -2567,4 +2567,185 @@ default Routes err(final int statusCode, final Err.Handler handler) { }); } + /** + * Produces a deferred response, useful for async request processing. + * + *

usage

+ * + *
+   * {
+   *    ExecutorService executor = ...;
+   *
+   *    get("/async", promise(deferred {@literal ->} {
+   *      executor.execute(() {@literal ->} {
+   *        try {
+   *          deferred.resolve(...); // success value
+   *        } catch (Exception ex) {
+   *          deferred.reject(ex); // error value
+   *        }
+   *      });
+   *    }));
+   *  }
+   * 
+ * + *

+ * Or with automatic error handler: + *

+ * + *
+   * {
+   *    ExecutorService executor = ...;
+   *
+   *    get("/async", promise(deferred {@literal ->} {
+   *      executor.execute(() {@literal ->} {
+   *        deferred.resolve(() {@literal ->} {
+   *          Object value = ...
+   *          return value;
+   *        }); // success value
+   *      });
+   *    }));
+   *  }
+   * 
+ * + *

+ * Or as {@link Runnable} with automatic error handler: + *

+ * + *
+   * {
+   *    ExecutorService executor = ...;
+   *
+   *    get("/async", promise(deferred {@literal ->} {
+   *      executor.execute(deferred.run(() {@literal ->} {
+   *        Object value = ...
+   *        return value;
+   *      }); // success value
+   *    }));
+   *  }
+   * 
+ * + * @param initializer Deferred initializer. + * @return A new deferred handler. + * @see Deferred + */ + Route.OneArgHandler promise(Deferred.Initializer initializer); + + /** + * Produces a deferred response, useful for async request processing. + * + *

usage

+ * + *
+   * {
+   *    get("/async", promise("myexec", deferred {@literal ->} {
+   *      // resolve a success value
+   *      deferred.resolve(...);
+   *    }));
+   *  }
+   * 
+ * + * @param executor Executor to run the deferred. + * @param initializer Deferred initializer. + * @return A new deferred handler. + * @see Deferred + */ + Route.OneArgHandler promise(String executor, Deferred.Initializer initializer); + + /** + * Produces a deferred response, useful for async request processing. + * + *

usage

+ * + *
+   * {
+   *    ExecutorService executor = ...;
+   *
+   *    get("/async", promise(deferred {@literal ->} {
+   *      executor.execute(() {@literal ->} {
+   *        try {
+   *          deferred.resolve(...); // success value
+   *        } catch (Exception ex) {
+   *          deferred.reject(ex); // error value
+   *        }
+   *      });
+   *    }));
+   *  }
+   * 
+ * + *

+ * Or with automatic error handler: + *

+ * + *
+   * {
+   *    ExecutorService executor = ...;
+   *
+   *    get("/async", promise(deferred {@literal ->} {
+   *      executor.execute(() {@literal ->} {
+   *        deferred.resolve(() {@literal ->} {
+   *          Object value = ...
+   *          return value;
+   *        }); // success value
+   *      });
+   *    }));
+   *  }
+   * 
+ * + *

+ * Or as {@link Runnable} with automatic error handler: + *

+ * + *
+   * {
+   *    ExecutorService executor = ...;
+   *
+   *    get("/async", promise(deferred {@literal ->} {
+   *      executor.execute(deferred.run(() {@literal ->} {
+   *        Object value = ...
+   *        return value;
+   *      }); // success value
+   *    }));
+   *  }
+   * 
+ * + * @param initializer Deferred initializer. + * @return A new deferred handler. + * @see Deferred + */ + Route.OneArgHandler promise(Deferred.Initializer0 initializer); + + /** + * Produces a deferred response, useful for async request processing. + * + *

usage

+ * + *
+   * {
+   *    get("/async", promise("myexec", deferred {@literal ->} {
+   *      // resolve a success value
+   *      deferred.resolve(...);
+   *    }));
+   *  }
+   * 
+ * + * @param executor Executor to run the deferred. + * @param initializer Deferred initializer. + * @return A new deferred handler. + * @see Deferred + */ + Route.OneArgHandler promise(String executor, Deferred.Initializer0 initializer); + + Route.OneArgHandler deferred(String executor, Route.OneArgHandler handler); + + default Route.OneArgHandler deferred(final Route.OneArgHandler handler) { + return deferred(null, handler); + } + + default Route.OneArgHandler deferred(final String executor, final Route.ZeroArgHandler handler) { + return deferred(executor, req -> handler.handle()); + } + + default Route.OneArgHandler deferred(final Route.ZeroArgHandler handler) { + return deferred(null, handler); + } } diff --git a/jooby/src/main/java/org/jooby/WebSocket.java b/jooby/src/main/java/org/jooby/WebSocket.java index 628972bb35..e2b7711da2 100644 --- a/jooby/src/main/java/org/jooby/WebSocket.java +++ b/jooby/src/main/java/org/jooby/WebSocket.java @@ -279,13 +279,13 @@ class Definition { * Defines the media types that the methods of a resource class or can consumes. Default is: * {@literal *}/{@literal *}. */ - private MediaType consumes = MediaType.all; + private MediaType consumes = MediaType.plain; /** * Defines the media types that the methods of a resource class or can produces. Default is: * {@literal *}/{@literal *}. */ - private MediaType produces = MediaType.all; + private MediaType produces = MediaType.plain; /** A path pattern. */ private String pattern; diff --git a/jooby/src/main/java/org/jooby/handlers/AssetHandler.java b/jooby/src/main/java/org/jooby/handlers/AssetHandler.java index 846a395aba..7a2e0885b7 100644 --- a/jooby/src/main/java/org/jooby/handlers/AssetHandler.java +++ b/jooby/src/main/java/org/jooby/handlers/AssetHandler.java @@ -70,7 +70,7 @@ *

* *
- * assets.cdn = "http://http://d7471vfo50fqt.cloudfront.net"
+ * assets.cdn = "http://d7471vfo50fqt.cloudfront.net"
  * 
* *

diff --git a/jooby/src/main/java/org/jooby/internal/HttpHandlerImpl.java b/jooby/src/main/java/org/jooby/internal/HttpHandlerImpl.java index b6813b2cf3..ffdd258928 100644 --- a/jooby/src/main/java/org/jooby/internal/HttpHandlerImpl.java +++ b/jooby/src/main/java/org/jooby/internal/HttpHandlerImpl.java @@ -354,7 +354,6 @@ private void onDeferred(final Map scope, final NativeRequest req Throwable cause = failure.orElse(null); if (cause != null) { close = true; - handleErr(req, rsp, cause); } cleanup(req, rsp, close, cause, true); } diff --git a/jooby/src/main/java/org/jooby/internal/RequestImpl.java b/jooby/src/main/java/org/jooby/internal/RequestImpl.java index 260823b050..7764033112 100644 --- a/jooby/src/main/java/org/jooby/internal/RequestImpl.java +++ b/jooby/src/main/java/org/jooby/internal/RequestImpl.java @@ -278,9 +278,6 @@ public Mutant body() throws Exception { long length = length(); if (length > 0) { MediaType type = type(); - if (!type.isAny() && (MediaType.form.matches(type) || MediaType.multipart.matches(type))) { - return params(); - } Config conf = require(Config.class); File fbody = new File(conf.getString("application.tmpdir"), @@ -289,9 +286,9 @@ public Mutant body() throws Exception { int bufferSize = conf.getBytes("server.http.RequestBufferSize").intValue(); Parser.BodyReference body = new BodyReferenceImpl(length, charset(), fbody, req.in(), bufferSize); - return new MutantImpl(require(ParserExecutor.class), type(), body); + return new MutantImpl(require(ParserExecutor.class), type, body); } - return new MutantImpl(require(ParserExecutor.class), type(), new EmptyBodyReference()); + return new MutantImpl(require(ParserExecutor.class), type, new EmptyBodyReference()); } @Override diff --git a/jooby/src/main/java/org/jooby/internal/WebSocketImpl.java b/jooby/src/main/java/org/jooby/internal/WebSocketImpl.java index 3b90209314..e3a5474df4 100644 --- a/jooby/src/main/java/org/jooby/internal/WebSocketImpl.java +++ b/jooby/src/main/java/org/jooby/internal/WebSocketImpl.java @@ -151,7 +151,7 @@ public void connect(final Injector injector, final Request req, final NativeWebS ws.onTextMessage(message -> Try .run(() -> messageCallback.invoke( - new MutantImpl(injector.getInstance(ParserExecutor.class), + new MutantImpl(injector.getInstance(ParserExecutor.class), consumes, new StrParamReferenceImpl("body", "message", ImmutableList.of(message))))) .onFailure(this::handleErr)); diff --git a/jooby/src/test/java/org/jooby/JoobyTest.java b/jooby/src/test/java/org/jooby/JoobyTest.java index a88b2b81e8..fed9799dcf 100644 --- a/jooby/src/test/java/org/jooby/JoobyTest.java +++ b/jooby/src/test/java/org/jooby/JoobyTest.java @@ -524,6 +524,7 @@ public Object m1() { expect(injector.getInstance(Route.KEY)).andReturn(Collections.emptySet()); expect(injector.getInstance(WebSocket.KEY)).andReturn(Collections.emptySet()); injector.injectMembers(isA(Jooby.class)); + unit.registerMock(Injector.class, injector); AppPrinter printer = unit.constructor(AppPrinter.class) .args(Set.class, Set.class, Config.class) @@ -657,8 +658,12 @@ public void defaults() throws Exception { Jooby jooby = new Jooby(); + assertEquals(false, jooby.isStarted()); + jooby.start(); + assertEquals(true, jooby.isStarted()); + }, boot); } @@ -2058,7 +2063,8 @@ public void assets() throws Exception { .expect(requestScope) .expect(webSockets) .expect(tmpdir) - .expect(err).expect(executor("deferred")) + .expect(err) + .expect(executor("deferred")) .expect(unit -> { Mutant ifModifiedSince = unit.mock(Mutant.class); expect(ifModifiedSince.toOptional(Long.class)).andReturn(Optional.empty()); @@ -2080,6 +2086,15 @@ public void assets() throws Exception { Route.Chain chain = unit.get(Route.Chain.class); chain.next(req, rsp); }) + .expect(unit -> { + Config conf = unit.get(Config.class); + expect(conf.getString("assets.cdn")).andReturn("").times(2); + expect(conf.getBoolean("assets.lastModified")).andReturn(true).times(2); + expect(conf.getBoolean("assets.etag")).andReturn(true).times(2); + + Injector injector = unit.get(Injector.class); + expect(injector.getInstance(Key.get(Config.class))).andReturn(conf).times(2); + }) .run(unit -> { Jooby jooby = new Jooby(); @@ -2255,23 +2270,23 @@ public void ws() throws Exception { .expect(routeHandler) .expect(params) .expect(requestScope) - .expect( - unit -> { - Multibinder multibinder = unit.mock(Multibinder.class); + .expect(unit -> { + Multibinder multibinder = unit.mock(Multibinder.class); - LinkedBindingBuilder binding = unit - .mock(LinkedBindingBuilder.class); - binding.toInstance(unit.capture(WebSocket.Definition.class)); + LinkedBindingBuilder binding = unit + .mock(LinkedBindingBuilder.class); + binding.toInstance(unit.capture(WebSocket.Definition.class)); - expect(multibinder.addBinding()).andReturn(binding); + expect(multibinder.addBinding()).andReturn(binding); - Binder binder = unit.get(Binder.class); + Binder binder = unit.get(Binder.class); - expect(Multibinder.newSetBinder(binder, WebSocket.Definition.class)).andReturn( - multibinder); - }) + expect(Multibinder.newSetBinder(binder, WebSocket.Definition.class)).andReturn( + multibinder); + }) .expect(tmpdir) - .expect(err).expect(executor("deferred")) + .expect(err) + .expect(executor("deferred")) .run(unit -> { Jooby jooby = new Jooby(); @@ -2279,8 +2294,8 @@ public void ws() throws Exception { WebSocket.Definition ws = jooby.ws("/", (socket) -> { }); assertEquals("/", ws.pattern()); - assertEquals(MediaType.all, ws.consumes()); - assertEquals(MediaType.all, ws.produces()); + assertEquals(MediaType.plain, ws.consumes()); + assertEquals(MediaType.plain, ws.produces()); defs.add(ws); jooby.start(); diff --git a/jooby/src/test/java/org/jooby/RouteDefinitionTest.java b/jooby/src/test/java/org/jooby/RouteDefinitionTest.java index 7a9a08c904..72dc0eaf9e 100644 --- a/jooby/src/test/java/org/jooby/RouteDefinitionTest.java +++ b/jooby/src/test/java/org/jooby/RouteDefinitionTest.java @@ -316,6 +316,14 @@ public void attrs() throws Exception { assertEquals(Route.class, r.attr("type")); } + @Test + public void src() throws Exception { + Function route = path -> new Route.Definition("*", path, () -> null); + Route.Definition r = route.apply("/"); + + assertEquals("org.jooby.RouteDefinitionTest:321", r.source().toString()); + } + @Test public void glob() throws Exception { Function route = path -> new Route.Definition("*", path, () -> null); diff --git a/jooby/src/test/java/org/jooby/WebSocketDefinitionTest.java b/jooby/src/test/java/org/jooby/WebSocketDefinitionTest.java index dbf58c22a3..d04f2d7c5f 100644 --- a/jooby/src/test/java/org/jooby/WebSocketDefinitionTest.java +++ b/jooby/src/test/java/org/jooby/WebSocketDefinitionTest.java @@ -13,8 +13,8 @@ public void toStr() { }); assertEquals("WS /pattern\n" + - " consume: */*\n" + - " produces: */*\n", def.toString()); + " consume: text/plain\n" + + " produces: text/plain\n", def.toString()); } @Test diff --git a/jooby/src/test/java/org/jooby/internal/AppPrinterTest.java b/jooby/src/test/java/org/jooby/internal/AppPrinterTest.java index 3eb40f26c1..8308535d15 100644 --- a/jooby/src/test/java/org/jooby/internal/AppPrinterTest.java +++ b/jooby/src/test/java/org/jooby/internal/AppPrinterTest.java @@ -32,7 +32,7 @@ public void print() { " GET {complete}/ [*/*] [*/*] (/anonymous)\n" + " GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + "listening on:\n" + " http://localhost:8080/", setup); @@ -58,11 +58,11 @@ public void printHttps() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + - "listening on:" + - "\n http://localhost:8080/" + - "\n https://localhost:8443/", setup); + "listening on:\n" + + " http://localhost:8080/\n" + + " https://localhost:8443/", setup); } @Test @@ -76,11 +76,11 @@ public void printHttp2() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + - "listening on:" + - "\n http://localhost:8080/ +h2" + - "\n https://localhost:8443/ +h2", setup); + "listening on:\n" + + " http://localhost:8080/ +h2\n" + + " https://localhost:8443/ +h2", setup); } @Test @@ -95,11 +95,11 @@ public void printHttp2Https() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + - "listening on:" + - "\n http://localhost:8080/" + - "\n https://localhost:8443/ +h2", setup); + "listening on:\n" + + " http://localhost:8080/\n" + + " https://localhost:8443/ +h2", setup); } @Test @@ -113,10 +113,10 @@ public void printHttp2ClearText() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + - "listening on:" + - "\n http://localhost:8080/ +h2", setup); + "listening on:\n" + + " http://localhost:8080/ +h2", setup); } private Config config(final String path) { @@ -136,7 +136,7 @@ public void printWithPath() { .toString(); assertEquals(" GET / [*/*] [*/*] (/anonymous)\n" + " GET /home [*/*] [*/*] (/anonymous)\n" + - " WS /ws [*/*] [*/*]\n" + + " WS /ws [text/plain] [text/plain]\n" + "\n" + "listening on:\n" + " http://localhost:8080/app", setup); diff --git a/md/doc/crash/crash.md b/md/doc/crash/crash.md new file mode 100644 index 0000000000..9d239218c0 --- /dev/null +++ b/md/doc/crash/crash.md @@ -0,0 +1,135 @@ +# crash + +CRaSH remote shell: connect and monitor JVM resources via HTTP, SSH or telnet. + +## dependency + +```xml + + org.jooby + jooby-crash + {{version}} + +``` + +## usage + +```java + +import org.jooby.crash; + +{ + use(new Crash()); +} +``` + +Just drop your commands in the `cmd` folder and CRaSH will pick up all them. + +Now let's see how to connect and interact with the CRaSH shell. + +## connectors + +### HTTP connector + +The HTTP connector is a simple yet powerful collection of HTTP endpoints where you can run a CRaSH command: + +```java +{ + use(new Crash() + .plugin(HttpShellPlugin.class) + ); +} +``` + +Try it: + +``` +GET /api/shell/thread/ls +``` + +OR: + +``` +GET /api/shell/thread ls +``` + +The connector is listening at ```/api/shell```. If you want to mount the connector some where else just set the property: ```crash.httpshell.path```. + +### SSH connector + +Just add the crash.connectors.ssh dependency to your project. + +Try it: + +``` +ssh -p 2000 admin@localhost +``` + +Default user and password is: ```admin```. See how to provide a custom authentication plugin. + +### telnet connector + +Just add the crash.connectors.telnet dependency to your project. + +Try it: + +``` +telnet localhost 5000 +``` + +Checkout complete telnet connector configuration. + +### web connector + +Just add the crash.connectors.web dependency to your project. + +Try it: + +``` +GET /shell +``` + +A web shell console will be ready to go at ```/shell```. If you want to mount the connector some where else just set the property: ```crash.webshell.path```. + +## commands + +You can write additional shell commands using Groovy or Java, see the CRaSH documentation for details. CRaSH search for commands in the ```cmd``` folder. + +Here is a simple ‘hello’ command that could be loaded from ```cmd/hello.groovy``` folder: + +```java +package commands + +import org.crsh.cli.Command +import org.crsh.cli.Usage +import org.crsh.command.InvocationContext + +class hello { + + @Usage("Say Hello") + @Command + def main(InvocationContext context) { + return "Hello" + } + +} +``` + +Jooby adds some additional attributes and commands to `InvocationContext` that you can access from your command: + +* registry: Access to [Registry]({{defdocs}}/Registry.html). +* conf: Access to `Config`. + +### routes command + +The ```routes``` print all the application routes. + +### conf command + +The ```conf tree``` print the application configuration tree (configuration precedence). + +The ```conf props [path]``` print all the application properties, sub-tree or a single property if ```path``` argument is present. + +## fancy banner + +Just add the [jooby-banner](/doc/banner) to your project and all the `CRaSH` shell will use it. Simple and easy!! diff --git a/md/doc/deployment/deployment.md b/md/doc/deployment/deployment.md index a67b0d1401..929ea4eeae 100644 --- a/md/doc/deployment/deployment.md +++ b/md/doc/deployment/deployment.md @@ -94,6 +94,8 @@ It works for ```logback.xml``` too, if ```logback.[env].xml``` is present, then {{war.md}} +{{doc/crash/crash.md}} + # conclusion * **jar/capsule deployment** makes perfect sense for PaaS like Heroku, AppEngine, etc... diff --git a/md/doc/more/more.md b/md/doc/more/more.md index 98a46b0df6..51fd3ea2fb 100644 --- a/md/doc/more/more.md +++ b/md/doc/more/more.md @@ -1,5 +1,6 @@ -# metrics +# monitor +* [crash](/doc/crash): [CRaSH](http://www.crashub.org/) remote shell integration. * [metrics](/doc/metrics): application metrics from the excellent [metrics](http://metrics.dropwizard.io) library. # utils diff --git a/md/routes.md b/md/routes.md index 09cb4706df..b8b8d7c931 100644 --- a/md/routes.md +++ b/md/routes.md @@ -183,7 +183,7 @@ All you have to do is to define a ```assets.cdn``` property: ```properties # application.prod.properties -assets.cdn = "http://http://d7471vfo50fqt.cloudfront.net" +assets.cdn = "http://d7471vfo50fqt.cloudfront.net" ``` ```java @@ -192,7 +192,7 @@ assets.cdn = "http://http://d7471vfo50fqt.cloudfront.net" } ``` -A ```GET``` to ```/assets/js/index.js``` will be redirected to: ```http://http://d7471vfo50fqt.cloudfront.net/assets/js/index.js``` +A ```GET``` to ```/assets/js/index.js``` will be redirected to: ```http://d7471vfo50fqt.cloudfront.net/assets/js/index.js``` Of course, you usually set a ```cdn``` in your ```application.prod.conf``` file only. diff --git a/pom.xml b/pom.xml index 6f269031e7..916842f839 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,7 @@ jooby-scanner jooby-csl jooby-unbescape + jooby-crash coverage-report @@ -566,6 +567,12 @@ ${jooby.version} + + org.jooby + jooby-crash + ${jooby.version} + + com.coverity.security @@ -1388,6 +1395,82 @@ ${jfiglet.version} + + + org.codehaus.groovy + groovy + ${groovy.version} + + + + + org.crashub + crash.shell + ${crash.version} + + + org.codehaus.groovy + groovy-all + + + + + + org.crashub + crash.connectors.telnet + ${crash.version} + + + org.codehaus.groovy + groovy-all + + + log4j + log4j + + + commons-logging + commons-logging + + + + + + org.crashub + crash.connectors.web + ${crash.version} + + + com.google.code.gson + gson + + + + + + org.crashub + crash.connectors.ssh + ${crash.version} + + + org.codehaus.groovy + groovy-all + + + + + + org.crashub + crash.plugins.mail + ${crash.version} + + + + org.crashub + crash.plugins.cron + ${crash.version} + + junit @@ -2450,7 +2533,7 @@ org.eclipse.jdt.apt.processorOptions/defaultOverwrite=true 5.2.1.Final 4.3.9.Final 3.0.0 - 4.3.6 + 4.5.2 1.9.36 3.2.0 5.0.3 @@ -2470,13 +2553,13 @@ org.eclipse.jdt.apt.processorOptions/defaultOverwrite=true 2.2.6 2.6.2 2.10.0 - 1.9.2 + 1.9.4 1.5.8 2.1.8-M1 3.4 1.15 2.12.0 - 1.10.66 + 1.11.43 1.4 1.2 2.5.0 @@ -2500,6 +2583,8 @@ org.eclipse.jdt.apt.processorOptions/defaultOverwrite=true 1.0.0 0.7 2.5.0.M3 + 1.3.1 + 2.4.7 **