Skip to content

Commit 4fe264a

Browse files
committed
Flash scope fix jooby-project#397
1 parent ec9d2e9 commit 4fe264a

15 files changed

Lines changed: 839 additions & 1 deletion

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package apps.flashscope;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
5+
import org.jooby.FlashScope;
6+
import org.jooby.Jooby;
7+
import org.jooby.Results;
8+
import org.jooby.hbs.Hbs;
9+
10+
import com.typesafe.config.ConfigFactory;
11+
import com.typesafe.config.ConfigValueFactory;
12+
13+
public class FlashScopeApp extends Jooby {
14+
15+
{
16+
use(ConfigFactory.empty().withValue("server.module",
17+
ConfigValueFactory.fromAnyRef("org.jooby.undertow.Undertow")));
18+
19+
use(new Hbs("/apps/flashscope"));
20+
21+
use(new FlashScope());
22+
23+
get("/", () -> Results.html("flash"));
24+
25+
post("/", req -> {
26+
req.flash("success", req.param("message").value());
27+
return Results.redirect("/");
28+
});
29+
30+
AtomicInteger inc = new AtomicInteger(100);
31+
get("/toggle", req -> {
32+
return req.ifFlash("n").orElseGet(() -> {
33+
String v = Integer.toString(inc.incrementAndGet());
34+
req.flash("n", v);
35+
return v;
36+
});
37+
});
38+
}
39+
40+
public static void main(final String[] args) throws Throwable {
41+
run(FlashScopeApp::new, args);
42+
}
43+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package org.jooby.issues;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import org.jooby.FlashScope;
7+
import org.jooby.Results;
8+
import org.jooby.test.ServerFeature;
9+
import org.junit.Test;
10+
11+
public class Issue397 extends ServerFeature {
12+
13+
{
14+
get("/397/noflash", req -> req.flash());
15+
16+
use(new FlashScope());
17+
18+
get("/397/flash", req -> req.flash());
19+
20+
get("/397/discard", req -> req.flash().remove("success"));
21+
22+
post("/397/reset", req -> {
23+
req.flash("foo", "bar");
24+
return Results.redirect("/397");
25+
});
26+
27+
post("/397/flash", req -> {
28+
req.flash("success", "Thank you!");
29+
return Results.redirect("/397");
30+
});
31+
32+
get("/397/untouch", req -> "untouch");
33+
34+
err((req, rsp, err) -> {
35+
rsp.send(err.getMessage());
36+
});
37+
}
38+
39+
@Test
40+
public void noFlashScope() throws Exception {
41+
request()
42+
.get("/397/noflash")
43+
.expect("Bad Request(400): Flash scope isn't available. Install via: use(new FlashScope());");
44+
}
45+
46+
@Test
47+
public void shouldCreateAndDestroyFlashCookie() throws Exception {
48+
request()
49+
.post("/397/flash")
50+
.expect(302)
51+
.header("Set-Cookie", setCookie -> {
52+
assertEquals("flash=success=Thank+you%21;Version=1", setCookie);
53+
request()
54+
.get("/397/flash")
55+
.header("Cookie", setCookie)
56+
.expect("{success=Thank you!}")
57+
.header("Set-Cookie", clearCookie -> {
58+
assertTrue(clearCookie.startsWith("flash=;Version=1;Max-Age=0;"));
59+
});
60+
});
61+
}
62+
63+
@Test
64+
public void shouldNotCreateCookieWhenFlashStateDontChange() throws Exception {
65+
request()
66+
.get("/397/untouch")
67+
.expect(200)
68+
.header("Set-Cookie", setCookie -> {
69+
assertEquals(null, setCookie);
70+
});
71+
}
72+
73+
@Test
74+
public void shouldRecreateCookieOnReset() throws Exception {
75+
request()
76+
.post("/397/flash")
77+
.expect(302)
78+
.header("Set-Cookie", setCookie1 -> {
79+
assertEquals("flash=success=Thank+you%21;Version=1", setCookie1);
80+
request()
81+
.post("/397/reset")
82+
.header("Cookie", setCookie1)
83+
.expect(302)
84+
.header("Set-Cookie", setCookie2 -> {
85+
assertEquals("flash=success=Thank+you%21&foo=bar;Version=1", setCookie2);
86+
request()
87+
.get("/397/flash")
88+
.header("Cookie", setCookie2)
89+
.expect("{success=Thank you!, foo=bar}")
90+
.header("Set-Cookie", clearCookie -> {
91+
assertTrue(clearCookie.startsWith("flash=;Version=1;Max-Age=0;"));
92+
});
93+
});
94+
});
95+
}
96+
97+
@Test
98+
public void shouldClearFlashCookieWhenEmpty() throws Exception {
99+
request()
100+
.get("/397/discard")
101+
.header("Cookie", "flash=success=OK;Version=1")
102+
.expect(200)
103+
.header("Set-Cookie", setCookie -> {
104+
assertTrue(setCookie.startsWith("flash=;Version=1;Max-Age=0;"));
105+
});
106+
}
107+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.jooby.issues;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import java.util.Map;
7+
import java.util.Optional;
8+
9+
import org.jooby.FlashScope;
10+
import org.jooby.mvc.Flash;
11+
import org.jooby.mvc.GET;
12+
import org.jooby.mvc.Path;
13+
import org.jooby.test.ServerFeature;
14+
import org.junit.Test;
15+
16+
public class Issue397b extends ServerFeature {
17+
18+
@Path("/397")
19+
public static class Resource {
20+
21+
@GET
22+
@Path("/flash")
23+
public Object flash(@Flash final Map<String, String> flash) {
24+
flash.put("foo", "bar");
25+
return flash;
26+
}
27+
28+
@GET
29+
@Path("/flash/attr")
30+
public Object flash(@Flash final String foo) {
31+
return foo;
32+
}
33+
34+
@GET
35+
@Path("/flash/attr/optional")
36+
public Object flash(@Flash final Optional<String> foo) {
37+
return foo.orElse("empty");
38+
}
39+
}
40+
41+
{
42+
use(new FlashScope());
43+
44+
use(Resource.class);
45+
46+
err((req, rsp, err) -> {
47+
err.printStackTrace();
48+
rsp.send(err.statusCode() + ": " + err.getCause().getMessage());
49+
});
50+
}
51+
52+
@Test
53+
public void flashScopeOnMvc() throws Exception {
54+
request()
55+
.get("/397/flash")
56+
.expect("{foo=bar}")
57+
.header("Set-Cookie", setCookie -> {
58+
assertEquals("flash=foo=bar;Version=1", setCookie);
59+
request()
60+
.get("/397/flash/attr")
61+
.expect("bar")
62+
.header("Set-Cookie", clearCookie -> {
63+
assertTrue(clearCookie.startsWith("flash=;Version=1;Max-Age=0;"));
64+
});
65+
});
66+
}
67+
68+
@Test
69+
public void optionalFlashScope() throws Exception {
70+
request()
71+
.get("/397/flash/attr/optional")
72+
.header("Cookie", "")
73+
.expect("empty");
74+
}
75+
76+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<html>
2+
<body>
3+
4+
{{#if flash.success}}
5+
{{flash.success}}!!
6+
{{else}}
7+
Welcome
8+
<form action="/" method="post">
9+
<input name="message">
10+
</form>
11+
{{/if}}
12+
</body>
13+
</html>

jooby-undertow/src/main/resources/org/jooby/spi/server.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ undertow {
1212
awaitShutdown = 1000
1313

1414
server {
15+
ALLOW_EQUALS_IN_COOKIE_VALUE = true
16+
1517
ALWAYS_SET_KEEP_ALIVE = true
1618

1719
# The maximum size in bytes of a http request header.

jooby/src/main/java/org/jooby/Cookie.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,29 @@
2020

2121
import static java.util.Objects.requireNonNull;
2222

23+
import java.net.URLDecoder;
24+
import java.net.URLEncoder;
25+
import java.nio.charset.StandardCharsets;
26+
import java.util.Iterator;
27+
import java.util.Map;
2328
import java.util.Optional;
29+
import java.util.function.Function;
2430
import java.util.regex.Pattern;
31+
import java.util.stream.Collectors;
2532

2633
import javax.crypto.Mac;
2734
import javax.crypto.spec.SecretKeySpec;
2835

2936
import org.jooby.internal.CookieImpl;
3037

38+
import com.google.common.base.Splitter;
3139
import com.google.common.base.Strings;
3240
import com.google.common.io.BaseEncoding;
3341

42+
import javaslang.Tuple;
43+
import javaslang.Tuple2;
44+
import javaslang.control.Try;
45+
3446
/**
3547
* Creates a cookie, a small amount of information sent by a server to
3648
* a Web browser, saved by the browser, and later sent back to the server.
@@ -66,6 +78,42 @@
6678
*/
6779
public interface Cookie {
6880

81+
/**
82+
* Decode a cookie value using, like: <code>k=v</code>, multiple <code>k=v</code> pair are
83+
* separated by <code>&amp;</code>. Also, <code>k</code> and <code>v</code> are decoded using
84+
* {@link URLDecoder}.
85+
*/
86+
public static final Function<String, Map<String, String>> URL_DECODER = value -> {
87+
Function<String, String> decode = v -> Try
88+
.of(() -> URLDecoder.decode(v, StandardCharsets.UTF_8.name())).get();
89+
return Splitter.on('&').trimResults().omitEmptyStrings()
90+
.splitToList(value)
91+
.stream()
92+
.map(v -> {
93+
Iterator<String> it = Splitter.on('=').trimResults().omitEmptyStrings()
94+
.split(v)
95+
.iterator();
96+
Tuple2<String, String> t2 = Tuple.of(decode.apply(it.next()), decode.apply(it.next()));
97+
return t2;
98+
}).collect(Collectors.toMap(it -> it._1, it -> it._2));
99+
};
100+
101+
/**
102+
* Encode a hash into cookie value, like: <code>k1=v1&amp;...&amp;kn=vn</code>. Also,
103+
* <code>key</code> and <code>value</code> are encoded using {@link URLEncoder}.
104+
*/
105+
public static final Function<Map<String, String>, String> URL_ENCODER = value -> {
106+
Function<String, String> encode = v -> Try
107+
.of(() -> URLEncoder.encode(v, StandardCharsets.UTF_8.name())).get();
108+
return value.entrySet().stream()
109+
.map(e -> new StringBuilder()
110+
.append(encode.apply(e.getKey()))
111+
.append('=')
112+
.append(encode.apply(e.getValue())))
113+
.collect(Collectors.joining("&"))
114+
.toString();
115+
};
116+
69117
/**
70118
* Build a {@link Cookie}.
71119
*
@@ -313,6 +361,7 @@ public Definition maxAge(final int maxAge) {
313361
* A negative value means that the cookie is not stored persistently and will be deleted when
314362
* the Web browser exits. A zero value causes the cookie to be deleted.
315363
* </p>
364+
*
316365
* @return Cookie's max age in seconds.
317366
*/
318367
public Optional<Integer> maxAge() {

0 commit comments

Comments
 (0)