Skip to content

Commit 74239b7

Browse files
committed
Flash attributes
1 parent 723d7cd commit 74239b7

14 files changed

Lines changed: 443 additions & 6 deletions

File tree

docs/asciidoc/body.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Response body is generated from `handler` function:
128128
----
129129
{
130130
get("/", ctx -> {
131-
ctx.setResponseConde(200); // <1>
131+
ctx.setResponseCode(200); // <1>
132132
133133
ctx.setResponseType(MediaType.text); // <2>
134134

docs/asciidoc/context.adoc

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,57 @@ You may prefer to use one of the utility methods:
453453
454454
====
455455

456+
==== Flash
457+
458+
Flash parameters are designed to transport success/error messages between requests. It is similar to
459+
a javadoc:Session[] but the lifecycle is shorter: *data is kept for only one request*.
460+
461+
.Java
462+
[source,java,role="primary"]
463+
----
464+
get("/", ctx -> {
465+
return ctx.flash("success").value("Welcome!"); <3>
466+
});
467+
468+
post("/save", ctx -> {
469+
ctx.flash("success", "Item created"); <1>
470+
return ctx.sendRedirect("/"); <2>
471+
});
472+
----
473+
474+
.Kotlin
475+
[source,kotlin,role="secondary"]
476+
----
477+
get("/") {ctx ->
478+
ctx.flash("success").value("Welcome!") <3>
479+
}
480+
481+
post("/save") { ctx ->
482+
ctx.flash("success", "Item created") <1>
483+
ctx.sendRedirect("/") <2>
484+
}
485+
----
486+
487+
<1> Set a flash attribute: `success`
488+
<2> Redirect to home page
489+
<3> Display an existing flash attribute `success` or shows `Welcome!`
490+
491+
Flash attributes are implemented using an `HTTP Cookie`. To customize the cookie name
492+
(defaults to `jooby.flash`) use the javadoc:FlashScope extension:
493+
494+
.Java
495+
[source,java,role="primary"]
496+
----
497+
install(new FlashScope(new Cookie("myflash").setHttpOnly(true)));
498+
----
499+
500+
.Kotlin
501+
[source,kotlin,role="secondary"]
502+
----
503+
install(FlashScope(Cookie("myflash").setHttpOnly(true)))
504+
----
505+
506+
456507
include::value-api.adoc[]
457508

458509
include::body.adoc[]

jooby/src/main/java/io/jooby/Context.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ public interface Context extends Registry {
119119
* **********************************************************************************************
120120
*/
121121

122+
default @Nonnull FlashMap flashMap() {
123+
return (FlashMap) getAttributes()
124+
.computeIfAbsent(FlashMap.NAME, key -> FlashMap.create(this, FlashScope.cookie()));
125+
}
126+
127+
default @Nonnull Value flash(@Nonnull String name) {
128+
return Value.create(name, flashMap().get(name));
129+
}
130+
131+
default @Nonnull Context flash(@Nonnull String name, @Nonnull String value) {
132+
flashMap().put(name, value);
133+
return this;
134+
}
135+
122136
/**
123137
* Find a session or creates a new session.
124138
*
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.jooby;
2+
3+
import io.jooby.internal.FlashMapImpl;
4+
5+
import javax.annotation.Nonnull;
6+
import javax.annotation.Nullable;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
10+
/**
11+
* Flash scope.
12+
*
13+
* @author edgar
14+
* @since 2.0.0
15+
*/
16+
public interface FlashMap extends Map<String, String> {
17+
18+
String NAME = "flash";
19+
20+
/**
21+
* Creates a new flash-scope using the given cookie.
22+
*
23+
* @param ctx
24+
* @param template
25+
* @return
26+
*/
27+
static @Nonnull FlashMap create(Context ctx, Cookie template) {
28+
return new FlashMapImpl(ctx, template);
29+
}
30+
31+
/**
32+
* Keep flash cookie for next request.
33+
*/
34+
FlashMap keep();
35+
36+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.jooby;
2+
3+
import javax.annotation.Nonnull;
4+
5+
import static io.jooby.FlashMap.NAME;
6+
import static java.util.Objects.requireNonNull;
7+
8+
public class FlashScope implements Extension {
9+
10+
private final Cookie template;
11+
12+
public FlashScope(@Nonnull Cookie template) {
13+
this.template = requireNonNull(template, "Flash cookie is required.");
14+
}
15+
16+
public FlashScope() {
17+
this(cookie());
18+
}
19+
20+
@Override public void install(@Nonnull Jooby application) {
21+
application.before(ctx -> ctx.attribute(NAME, FlashMap.create(ctx, template)));
22+
}
23+
24+
public static Cookie cookie() {
25+
return new Cookie("jooby.flash").setHttpOnly(true);
26+
}
27+
}

jooby/src/main/java/io/jooby/Value.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,23 @@ default int size() {
688688
.add(values);
689689
}
690690

691+
/**
692+
* Creates a value that fits better with the given values.
693+
*
694+
* - For null/empty values. It produces a missing value.
695+
* - For single element (size==1). It produces a single value
696+
*
697+
* @param name Field name.
698+
* @param value Field values.
699+
* @return A value.
700+
*/
701+
static @Nonnull Value create(@Nonnull String name, @Nullable String value) {
702+
if (value == null) {
703+
return missing(name);
704+
}
705+
return value(name, value);
706+
}
707+
691708
/**
692709
* Create a hash/object value using the map values.
693710
*
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package io.jooby.internal;
2+
3+
import io.jooby.Context;
4+
import io.jooby.Cookie;
5+
import io.jooby.FlashMap;
6+
import io.jooby.Value;
7+
8+
import javax.annotation.Nullable;
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
import java.util.function.BiFunction;
12+
import java.util.function.Function;
13+
14+
import static io.jooby.Cookie.decode;
15+
import static java.util.Collections.emptyMap;
16+
17+
public class FlashMapImpl extends HashMap<String, String> implements FlashMap {
18+
19+
private Context ctx;
20+
21+
private boolean keep;
22+
23+
private Cookie template;
24+
25+
private Map<String, String> initialScope;
26+
27+
public FlashMapImpl(Context ctx, Cookie template) {
28+
Value cookie = ctx.cookie(template.getName());
29+
Map<String, String> seed = cookie.isMissing() ? emptyMap() : decode(cookie.value());
30+
super.putAll(seed);
31+
this.ctx = ctx;
32+
this.template = template;
33+
this.initialScope = seed;
34+
if (seed.size() > 0) {
35+
Cookie sync = toCookie();
36+
if (sync != null) {
37+
ctx.setResponseCookie(sync);
38+
}
39+
}
40+
}
41+
42+
@Override public FlashMap keep() {
43+
if (size() > 0) {
44+
Cookie cookie = this.template.clone().setValue(Cookie.encode(this));
45+
ctx.setResponseCookie(cookie);
46+
}
47+
return this;
48+
}
49+
50+
private Cookie toCookie() {
51+
// 1. no change detect
52+
if (this.equals(initialScope)) {
53+
// 1.a. existing data available, discard
54+
if (this.size() > 0) {
55+
return template.clone().setMaxAge(0);
56+
}
57+
} else {
58+
// 2. change detected
59+
if (this.size() == 0) {
60+
// 2.a everything was removed from app logic
61+
return template.clone().setMaxAge(0);
62+
} else {
63+
// 2.b there is something to see in the next request
64+
return template.clone().setValue(Cookie.encode(this));
65+
}
66+
}
67+
return null;
68+
}
69+
70+
private void syncCookie() {
71+
Cookie cookie = toCookie();
72+
if (cookie != null) {
73+
ctx.setResponseCookie(cookie);
74+
}
75+
}
76+
77+
@Override public String compute(String key,
78+
BiFunction<? super String, ? super String, ? extends String> remappingFunction) {
79+
String result = super.compute(key, remappingFunction);
80+
syncCookie();
81+
return result;
82+
}
83+
84+
@Override public String computeIfAbsent(String key,
85+
Function<? super String, ? extends String> mappingFunction) {
86+
String result = super.computeIfAbsent(key, mappingFunction);
87+
syncCookie();
88+
return result;
89+
}
90+
91+
@Override public String computeIfPresent(String key,
92+
BiFunction<? super String, ? super String, ? extends String> remappingFunction) {
93+
String result = super.computeIfPresent(key, remappingFunction);
94+
syncCookie();
95+
return result;
96+
}
97+
98+
@Override public String merge(String key, String value,
99+
BiFunction<? super String, ? super String, ? extends String> remappingFunction) {
100+
String result = super.merge(key, value, remappingFunction);
101+
syncCookie();
102+
return result;
103+
}
104+
105+
@Override public String put(String key, String value) {
106+
String result = super.put(key, value);
107+
syncCookie();
108+
return result;
109+
}
110+
111+
@Override public String putIfAbsent(String key, String value) {
112+
String result = super.putIfAbsent(key, value);
113+
syncCookie();
114+
return result;
115+
}
116+
117+
@Override public void putAll(Map<? extends String, ? extends String> m) {
118+
super.putAll(m);
119+
syncCookie();
120+
}
121+
122+
@Override public boolean remove(Object key, Object value) {
123+
boolean result = super.remove(key, value);
124+
syncCookie();
125+
return result;
126+
}
127+
128+
@Override public String remove(Object key) {
129+
String result = super.remove(key);
130+
syncCookie();
131+
return result;
132+
}
133+
134+
@Override public boolean replace(String key, String oldValue, String newValue) {
135+
boolean result = super.replace(key, oldValue, newValue);
136+
syncCookie();
137+
return result;
138+
}
139+
140+
@Override public String replace(String key, String value) {
141+
String result = super.replace(key, value);
142+
syncCookie();
143+
return result;
144+
}
145+
146+
@Override
147+
public void replaceAll(BiFunction<? super String, ? super String, ? extends String> function) {
148+
super.replaceAll(function);
149+
syncCookie();
150+
}
151+
152+
}
153+

modules/server/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class JettyContext implements Callback, Context {
8080
private Route route;
8181
private MediaType responseType;
8282
private Map<String, String> cookies;
83+
private HashMap<String, String> responseCookies;
8384

8485
public JettyContext(Request request, Router router, int bufferSize, long maxRequestSize) {
8586
this.request = request;
@@ -297,8 +298,15 @@ public Context setResponseType(@Nonnull MediaType contentType, @Nullable Charset
297298
}
298299

299300
@Nonnull public Context setResponseCookie(@Nonnull Cookie cookie) {
301+
if (responseCookies == null) {
302+
responseCookies = new HashMap<>();
303+
}
300304
cookie.setPath(cookie.getPath(getContextPath()));
301-
response.addHeader(SET_COOKIE.asString(), cookie.toCookieString());
305+
responseCookies.put(cookie.getName(), cookie.toCookieString());
306+
response.setHeader(SET_COOKIE, null);
307+
for (String cookieString : responseCookies.values()) {
308+
response.addHeader(SET_COOKIE.asString(), cookieString);
309+
}
302310
return this;
303311
}
304312

modules/server/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ public class NettyContext implements Context, ChannelFutureListener {
109109
private long contentLength = -1;
110110
private boolean needsFlush;
111111
private Map<String, String> cookies;
112+
private Map<String, String> responseCookies;
112113

113114
public NettyContext(ChannelHandlerContext ctx, HttpRequest req, Router router, String path,
114115
int bufferSize) {
@@ -312,8 +313,15 @@ public NettyContext(ChannelHandlerContext ctx, HttpRequest req, Router router, S
312313
}
313314

314315
@Nonnull public Context setResponseCookie(@Nonnull Cookie cookie) {
316+
if (responseCookies == null) {
317+
responseCookies = new HashMap<>();
318+
}
315319
cookie.setPath(cookie.getPath(getContextPath()));
316-
setHeaders.add(SET_COOKIE, cookie.toCookieString());
320+
responseCookies.put(cookie.getName(), cookie.toCookieString());
321+
setHeaders.remove(SET_COOKIE);
322+
for (String cookieString : responseCookies.values()) {
323+
setHeaders.add(SET_COOKIE, cookieString);
324+
}
317325
return this;
318326
}
319327

0 commit comments

Comments
 (0)