Skip to content

Commit 219f3c3

Browse files
committed
SPAs support using asset handler #1339
1 parent a464f77 commit 219f3c3

File tree

5 files changed

+109
-4
lines changed

5 files changed

+109
-4
lines changed

docs/asciidoc/static-files.adoc

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,36 @@ The `assets` route works for static sites too. Just need to use a special path m
9494
The key difference is the `/?*` mapping. This mapping add support for base root mapping:
9595

9696
- GET `/docs` => `/docs/index.html`
97+
- GET `/docs/index.html` => `/docs/index.html`
9798
- GET `/docs/about.html` => `/docs/about.html`
98-
- GET `/docs/note` => `/docs/index.html`
99+
- GET `/docs/note` => `/docs/note/index.html`
100+
101+
=== SPAs
102+
103+
The `assets` route works for single page applications (SPAs) too. Just need to use a special path mapping plus a fallback asset:
104+
105+
.Classpath resources:
106+
[source, java, role="primary"]
107+
----
108+
{
109+
AssetSource docs = AssetSource.create(Paths.get("docs")); <1>
110+
assets("/docs/?*", new AssetHandler("index.html", docs)); <2>
111+
}
112+
----
113+
114+
.Kotlin
115+
[source, kotlin, role="secondary"]
116+
----
117+
{
118+
val docs = AssetSource.create(Paths.get("docs")) <1>
119+
assets("/docs/?*", AssetHandler("index.html", docs)) <2>
120+
}
121+
----
122+
123+
<1> Serve from `docs` directory
124+
<2> Use the `/?*` mapping and uses `index.html` as fallback asset
125+
126+
SPAs mode never generates a `NOT FOUND (404)` response, unresolved assets fallback to `index.html`
99127

100128
=== Options
101129

jooby/src/main/java/io/jooby/AssetHandler.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
/**
1414
* Handler for static resources represented by the {@link Asset} contract.
1515
*
16+
* It has built-in support for static-static as well as SPAs (single page applications).
17+
*
1618
* @author edgar
1719
* @since 2.0.0
1820
*/
@@ -29,6 +31,28 @@ public class AssetHandler implements Route.Handler {
2931

3032
private String filekey;
3133

34+
private String fallback;
35+
36+
/**
37+
* Creates a new asset handler that fallback to the given fallback asset when the asset
38+
* is not found. Instead of produces a <code>404</code> its fallback to the given asset.
39+
*
40+
* <pre>{@code
41+
* {
42+
* assets("/?*", new AssetHandler("index.html", AssetSource.create(Paths.get("...")));
43+
* }
44+
* }</pre>
45+
*
46+
* The fallback option makes the asset handler to work like a SPA (Single-Application-Page).
47+
*
48+
* @param fallback Fallback asset.
49+
* @param sources Asset sources.
50+
*/
51+
public AssetHandler(@Nonnull String fallback, AssetSource... sources) {
52+
this.fallback = fallback;
53+
this.sources = sources;
54+
}
55+
3256
/**
3357
* Creates a new asset handler.
3458
*
@@ -42,8 +66,14 @@ public AssetHandler(AssetSource... sources) {
4266
String filepath = ctx.pathMap().getOrDefault(filekey, "index.html");
4367
Asset asset = resolve(filepath);
4468
if (asset == null) {
45-
ctx.sendError(new StatusCodeException(StatusCode.NOT_FOUND));
46-
return ctx;
69+
if (fallback != null) {
70+
asset = resolve(fallback);
71+
}
72+
// Still null?
73+
if (asset == null) {
74+
ctx.sendError(new StatusCodeException(StatusCode.NOT_FOUND));
75+
return ctx;
76+
}
4777
}
4878

4979
// handle If-None-Match

jooby/src/main/java/io/jooby/Jooby.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ public Jooby() {
144144
return env;
145145
}
146146

147+
/**
148+
* Application class loader.
149+
*
150+
* @return Application class loader.
151+
*/
152+
public @Nonnull ClassLoader getClassLoader() {
153+
return env == null ? environmentOptions.getClassLoader() : env.getClassLoader();
154+
}
155+
147156
/**
148157
* Application configuration. It is a shortcut for {@link Environment#getConfig()}.
149158
*
@@ -451,7 +460,7 @@ public Jooby errorCode(@Nonnull Class<? extends Throwable> type,
451460
return require(ServiceKey.key(type));
452461
}
453462

454-
@Override public @Nonnull <T> T require(@Nonnull ServiceKey<T> key) {
463+
@Override public @Nonnull <T> T require(@Nonnull ServiceKey<T> key) {
455464
ServiceRegistry services = getServices();
456465
T service = services.getOrNull(key);
457466
if (service == null) {

tests/src/test/java/io/jooby/FeaturedTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,6 +1982,43 @@ public void varOnCatchAll() {
19821982
});
19831983
}
19841984

1985+
@Test
1986+
public void singlePageApp() {
1987+
new JoobyRunner(app -> {
1988+
1989+
app.assets("/?*", new AssetHandler("fallback.html", AssetSource.create(app.getClassLoader(), "/www")));
1990+
1991+
}).ready(client -> {
1992+
client.get("/docs", rsp -> {
1993+
assertEquals("fallback.html", rsp.body().string().trim());
1994+
});
1995+
1996+
client.get("/docs/index.html", rsp -> {
1997+
assertEquals("fallback.html", rsp.body().string().trim());
1998+
});
1999+
2000+
client.get("/docs/v1", rsp -> {
2001+
assertEquals("fallback.html", rsp.body().string().trim());
2002+
});
2003+
2004+
client.get("/", rsp -> {
2005+
assertEquals("index.html", rsp.body().string().trim());
2006+
});
2007+
2008+
client.get("/index.html", rsp -> {
2009+
assertEquals("index.html", rsp.body().string().trim());
2010+
});
2011+
2012+
client.get("/note", rsp -> {
2013+
assertEquals("note.html", rsp.body().string().trim());
2014+
});
2015+
2016+
client.get("/note/index.html", rsp -> {
2017+
assertEquals("note.html", rsp.body().string().trim());
2018+
});
2019+
});
2020+
}
2021+
19852022
@Test
19862023
public void staticSiteFromCp() {
19872024
new JoobyRunner(app -> {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fallback.html

0 commit comments

Comments
 (0)