7676import java .util .Optional ;
7777import java .util .Set ;
7878import java .util .TimeZone ;
79+ import java .util .concurrent .TimeUnit ;
7980import java .util .concurrent .atomic .AtomicBoolean ;
8081import java .util .function .Consumer ;
8182import java .util .function .Function ;
9394import org .jooby .internal .AppPrinter ;
9495import org .jooby .internal .BuiltinParser ;
9596import org .jooby .internal .BuiltinRenderer ;
97+ import org .jooby .internal .CookieSessionManager ;
9698import org .jooby .internal .DefaulErrRenderer ;
9799import org .jooby .internal .HttpHandlerImpl ;
98100import org .jooby .internal .JvmInfo ;
101103import org .jooby .internal .RequestScope ;
102104import org .jooby .internal .RouteMetadata ;
103105import org .jooby .internal .ServerLookup ;
106+ import org .jooby .internal .ServerSessionManager ;
104107import org .jooby .internal .SessionManager ;
105108import org .jooby .internal .TypeConverters ;
106109import org .jooby .internal .handlers .HeadHandler ;
131134import com .google .inject .Guice ;
132135import com .google .inject .Injector ;
133136import com .google .inject .Key ;
137+ import com .google .inject .Provider ;
134138import com .google .inject .Stage ;
135139import com .google .inject .TypeLiteral ;
136140import com .google .inject .multibindings .Multibinder ;
@@ -1101,8 +1105,8 @@ public Route.OneArgHandler promise(final Deferred.Initializer0 initializer) {
11011105 }
11021106
11031107 /**
1104- * Setup a session store to use. Useful if you want/need to persist sessions between shutdowns.
1105- * Sessions are not persisted by defaults .
1108+ * Setup a session store to use. Useful if you want/need to persist sessions between shutdowns, or
1109+ * save data in redis, memcached, mongodb, couchbase, etc. .
11061110 *
11071111 * @param store A session store.
11081112 * @return A session store definition.
@@ -1113,8 +1117,28 @@ public Session.Definition session(final Class<? extends Session.Store> store) {
11131117 }
11141118
11151119 /**
1116- * Setup a session store to use. Useful if you want/need to persist sessions between shutdowns.
1117- * Sessions are not persisted by defaults.
1120+ * Setup a session store that saves data in a the session cookie. It makes the application
1121+ * stateless, which help to scale easily. Keep in mind that a cookie has a limited size (up to
1122+ * 4kb) so you must pay attention to what you put in the session object (don't use as cache).
1123+ *
1124+ * Cookie session signed data using the <code>application.secret</code> property, so you must
1125+ * provide an <code>application.secret</code> value. On dev environment you can set it in your
1126+ * <code>.conf</code> file. In prod is probably better to provide as command line argument and/or
1127+ * environment variable. Just make sure to keep it private.
1128+ *
1129+ * Please note {@link Session#id()}, {@link Session#accessedAt()}, etc.. make no sense for cookie
1130+ * sessions, just the {@link Session#attributes()}.
1131+ *
1132+ * @return A session definition/configuration object.
1133+ */
1134+ public Session .Definition cookieSession () {
1135+ this .session = new Session .Definition ();
1136+ return this .session ;
1137+ }
1138+
1139+ /**
1140+ * Setup a session store to use. Useful if you want/need to persist sessions between shutdowns, or
1141+ * save data in redis, memcached, mongodb, couchbase, etc..
11181142 *
11191143 * @param store A session store.
11201144 * @return A session store definition.
@@ -3830,6 +3854,11 @@ private Injector bootstrap(final Config args,
38303854 finalEnv = env ;
38313855 }
38323856
3857+ boolean cookieSession = session .store () == null ;
3858+ if (cookieSession && !finalConfig .hasPath ("application.secret" )) {
3859+ throw new IllegalStateException ("Required property 'application.secret' is missing" );
3860+ }
3861+
38333862 /** dependency injection */
38343863 @ SuppressWarnings ("unchecked" )
38353864 Injector injector = Guice .createInjector (stage , binder -> {
@@ -3951,14 +3980,21 @@ private Injector bootstrap(final Config args,
39513980 binder .bindScope (RequestScoped .class , requestScope );
39523981
39533982 /** session manager */
3954- binder .bind (SessionManager .class ).asEagerSingleton ();
3955- binder .bind (Session .Definition .class ).toInstance (session );
3983+ binder .bind (Session .Definition .class )
3984+ .toProvider (session (finalConfig .getConfig ("session" ), session ))
3985+ .asEagerSingleton ();
39563986 Object sstore = session .store ();
3957- if (sstore instanceof Class ) {
3958- binder .bind (Session . Store . class ).to (( Class <? extends Store >) sstore )
3987+ if (cookieSession ) {
3988+ binder .bind (SessionManager . class ).to (CookieSessionManager . class )
39593989 .asEagerSingleton ();
39603990 } else {
3961- binder .bind (Session .Store .class ).toInstance ((Store ) sstore );
3991+ binder .bind (SessionManager .class ).to (ServerSessionManager .class ).asEagerSingleton ();
3992+ if (sstore instanceof Class ) {
3993+ binder .bind (Session .Store .class ).to ((Class <? extends Store >) sstore )
3994+ .asEagerSingleton ();
3995+ } else {
3996+ binder .bind (Session .Store .class ).toInstance ((Store ) sstore );
3997+ }
39623998 }
39633999
39644000 binder .bind (Request .class ).toProvider (Providers .outOfScope (Request .class ))
@@ -3986,6 +4022,45 @@ private Injector bootstrap(final Config args,
39864022 return injector ;
39874023 }
39884024
4025+ private static Provider <Session .Definition > session (final Config $session ,
4026+ final Session .Definition session ) {
4027+ return () -> {
4028+ // save interval
4029+ session .saveInterval (session .saveInterval ()
4030+ .orElse ($session .getDuration ("saveInterval" , TimeUnit .MILLISECONDS )));
4031+
4032+ // build cookie
4033+ Cookie .Definition source = session .cookie ();
4034+
4035+ source .name (source .name ()
4036+ .orElse ($session .getString ("cookie.name" )));
4037+
4038+ if (!source .comment ().isPresent () && $session .hasPath ("cookie.comment" )) {
4039+ source .comment ($session .getString ("cookie.comment" ));
4040+ }
4041+ if (!source .domain ().isPresent () && $session .hasPath ("cookie.domain" )) {
4042+ source .domain ($session .getString ("cookie.domain" ));
4043+ }
4044+ source .httpOnly (source .httpOnly ()
4045+ .orElse ($session .getBoolean ("cookie.httpOnly" )));
4046+
4047+ Object maxAge = $session .getAnyRef ("cookie.maxAge" );
4048+ if (maxAge instanceof String ) {
4049+ maxAge = $session .getDuration ("cookie.maxAge" , TimeUnit .SECONDS );
4050+ }
4051+ source .maxAge (source .maxAge ()
4052+ .orElse (((Number ) maxAge ).intValue ()));
4053+
4054+ source .path (source .path ()
4055+ .orElse ($session .getString ("cookie.path" )));
4056+
4057+ source .secure (source .secure ()
4058+ .orElse ($session .getBoolean ("cookie.secure" )));
4059+
4060+ return session ;
4061+ };
4062+ }
4063+
39894064 private static Consumer <? super Object > bindService (final Set <Object > src ,
39904065 final Config conf ,
39914066 final Env env ,
0 commit comments