Skip to content

Commit 7ff089b

Browse files
committed
better assets route + doc
1 parent 9ebae15 commit 7ff089b

File tree

16 files changed

+352
-33
lines changed

16 files changed

+352
-33
lines changed

docs/asciidoc/index.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ include::routing.adoc[]
216216

217217
include::context.adoc[]
218218

219+
include::static-files.adoc[]
220+
219221
include::execution-model.adoc[]
220222

221223
include::responses.adoc[]

docs/asciidoc/static-files.adoc

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
== Static Files
2+
3+
Static files are available via javadoc:Router[assets, java.lang.String] route. The `assets` route
4+
supports classpath and file-system resources.
5+
6+
.Classpath resources:
7+
[source, java, role="primary"]
8+
----
9+
{
10+
assets("/static/*"); <1>
11+
}
12+
----
13+
14+
.Kotlin
15+
[source, kotlin, role="secondary"]
16+
----
17+
{
18+
assets("/static/*") <1>
19+
}
20+
----
21+
22+
<1> Map all the incoming request starting with `/static/` to the root of classpath
23+
24+
- GET `/static/index.html` => `/index.html`
25+
- GET `/static/js/file.js` => `/js/file.js`
26+
- GET `/static/css/styles.css` => `/css/styles.css`
27+
28+
.File system resources:
29+
[source, java, role="primary"]
30+
----
31+
{
32+
assets("/static/*", Paths.get("static")); <1>
33+
}
34+
----
35+
36+
.Kotlin
37+
[source, kotlin, role="secondary"]
38+
----
39+
{
40+
assets("/static/*", Paths.get("www")) <1>
41+
}
42+
----
43+
44+
<1> Map all the incoming request starting with `/static/` to a file system directory `www`
45+
46+
- GET `/static/index.html` => `www/index.html`
47+
- GET `/static/js/file.js` => `www/js/file.js`
48+
- GET `/static/css/styles.css` => `www/css/styles.css`
49+
50+
Individual file mapping is supported too:
51+
52+
.Classpath:
53+
[source, role="primary"]
54+
----
55+
{
56+
assets("/myfile.js", "/static/myfile.js");
57+
}
58+
----
59+
60+
.File system
61+
[source, role="secondary"]
62+
----
63+
{
64+
Path basedir = ...;
65+
assets("/myfile.js", basedire.resolve("/myfile.js"));
66+
}
67+
----
68+
69+
=== Static Site
70+
71+
The `assets` route works for static sites too. Just need to use a special path mapping:
72+
73+
.Classpath resources:
74+
[source, java, role="primary"]
75+
----
76+
{
77+
Path docs = Paths.get("docs"); <1>
78+
assets("/docs/?*"); <2>
79+
}
80+
----
81+
82+
.Kotlin
83+
[source, kotlin, role="secondary"]
84+
----
85+
{
86+
val docs = Paths.get("docs") <1>
87+
assets("/docs/?*", docs) <2>
88+
}
89+
----
90+
91+
<1> Serve from `docs` directory
92+
<2> Use the `/?*` mapping
93+
94+
The key difference is the `/?*` mapping. This mapping add support for base root mapping:
95+
96+
- GET `/docs` => `/docs/index.html`
97+
- GET `/docs/about.html` => `/docs/about.html`
98+
- GET `/docs/note` => `/docs/index.html`
99+
100+
=== Options
101+
102+
The javadoc:AssetHandler[] automatically handle `E-Tag` and `Las-Modified` headers. You can turn
103+
control these headers programmatically:
104+
105+
.Asset handler options:
106+
[source, java, role="primary"]
107+
----
108+
{
109+
AssetSource www = AssetSource.create(Paths.get("www"));
110+
assets("/static/*", new AssetHandler(www)
111+
.setLastModified(false)
112+
.setEtag(false)
113+
);
114+
}
115+
----
116+
117+
.Kotlin
118+
[source, kotlin, role="secondary"]
119+
----
120+
{
121+
val www = AssetSource.create(Paths.get("www"))
122+
assets("/static/*", AssetHandler(www)
123+
.setLastModified(false)
124+
.setEtag(false)
125+
);
126+
}
127+
----
128+
129+
The `maxAge` option set a `Cache-Control` header:
130+
131+
.Cache control:
132+
[source, java, role="primary"]
133+
----
134+
{
135+
AssetSource www = AssetSource.create(Paths.get("www"));
136+
assets("/static/*", new AssetHandler(www)
137+
.setMaxAge(Duration.ofDays(365))
138+
);
139+
}
140+
----
141+
142+
.Kotlin
143+
[source, kotlin, role="secondary"]
144+
----
145+
{
146+
val www = AssetSource.create(Paths.get("www"))
147+
assets("/static/*", AssetHandler(www)
148+
.setMaxAge(Duration.ofDays(365))
149+
);
150+
}
151+
----

docs/src/main/java/io/jooby/adoc/DocApp.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.slf4j.LoggerFactory;
1313

1414
import java.nio.file.Path;
15+
import java.time.Duration;
1516
import java.util.Arrays;
1617

1718
import static org.slf4j.helpers.NOPLogger.NOP_LOGGER;

examples/src/main/java/examples/AssetsApp.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ public class AssetsApp extends Jooby {
1616
Path www = Paths.get(System.getProperty("user.dir"), "examples", "www");
1717
assets("/*", www);
1818
assets("/static/*", www);
19+
assets("/file.js", "/logback.xml");
20+
21+
assets("/cp/*");
1922
}
2023

2124
public static void main(String[] args) {

examples/www/note/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
note

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* @since 2.0.0
1818
*/
1919
public class AssetHandler implements Route.Handler {
20+
private static final int ONE_SEC = 1000;
21+
2022
private final AssetSource[] sources;
2123

2224
private boolean etag = true;
@@ -27,8 +29,6 @@ public class AssetHandler implements Route.Handler {
2729

2830
private String filekey;
2931

30-
private static final int ONE_SEC = 1000;
31-
3232
/**
3333
* Creates a new asset handler.
3434
*
@@ -40,6 +40,9 @@ public AssetHandler(AssetSource... sources) {
4040

4141
@Nonnull @Override public Object apply(@Nonnull Context ctx) throws Exception {
4242
String filepath = ctx.pathMap().get(filekey);
43+
if (filepath == null) {
44+
filepath = "index.html";
45+
}
4346
Asset asset = resolve(filepath);
4447
if (asset == null) {
4548
ctx.send(StatusCode.NOT_FOUND);
@@ -142,7 +145,6 @@ private Asset resolve(String filepath) {
142145
@Override public Route.Handler setRoute(Route route) {
143146
List<String> keys = route.getPathKeys();
144147
this.filekey = keys.size() == 0 ? "*" : keys.get(0);
145-
146148
// NOTE: It send an inputstream we don't need a renderer
147149
route.setReturnType(Context.class);
148150
return this;

jooby/src/main/java/io/jooby/AssetSource.java

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
*/
66
package io.jooby;
77

8+
import io.jooby.internal.FileDiskAssetSource;
9+
import io.jooby.internal.FolderDiskAssetSource;
10+
811
import javax.annotation.Nonnull;
912
import javax.annotation.Nullable;
1013
import java.io.FileNotFoundException;
@@ -41,14 +44,24 @@ public interface AssetSource {
4144
static @Nonnull AssetSource create(@Nonnull ClassLoader loader, @Nonnull String location) {
4245
String safeloc = Router.normalizePath(location, true, true)
4346
.substring(1);
44-
String sep = safeloc.length() > 0 ? "/" : "";
47+
MediaType type = MediaType.byFile(location);
48+
if (type != MediaType.octetStream) {
49+
URL resource = loader
50+
.getResource(location.startsWith("/") ? location.substring(1) : location);
51+
if (resource != null) {
52+
return path -> Asset.create(location, resource);
53+
}
54+
}
55+
String prefix = safeloc + (safeloc.length() > 0 ? "/" : "");
4556
return path -> {
46-
String absolutePath = safeloc + sep + path;
47-
URL resource = loader.getResource(absolutePath);
48-
if (resource == null) {
49-
return null;
57+
String[] paths = {prefix + path + "/index.html", prefix + path};
58+
for (String it : paths) {
59+
URL resource = loader.getResource(it);
60+
if (resource != null) {
61+
return Asset.create(it, resource);
62+
}
5063
}
51-
return Asset.create(absolutePath, resource);
64+
return null;
5265
};
5366
}
5467

@@ -61,22 +74,9 @@ public interface AssetSource {
6174
static @Nonnull AssetSource create(@Nonnull Path location) {
6275
Path absoluteLocation = location.toAbsolutePath();
6376
if (Files.isDirectory(absoluteLocation)) {
64-
return path -> {
65-
Path resource = absoluteLocation.resolve(path).normalize().toAbsolutePath();
66-
if (resource.toString().length() == 0 || Files.isDirectory(resource)) {
67-
resource = resource.resolve("index.html");
68-
}
69-
if (!Files.exists(resource)
70-
|| Files.isDirectory(resource)
71-
|| !resource.startsWith(absoluteLocation)) {
72-
return null;
73-
}
74-
return Asset.create(resource);
75-
};
76-
}
77-
if (Files.isRegularFile(location)) {
78-
Asset singleFile = Asset.create(absoluteLocation);
79-
return p -> singleFile;
77+
return new FolderDiskAssetSource(absoluteLocation);
78+
} else if (Files.isRegularFile(location)) {
79+
return new FileDiskAssetSource(location);
8080
}
8181
throw Sneaky.propagate(new FileNotFoundException(location.toAbsolutePath().toString()));
8282
}

jooby/src/main/java/io/jooby/Router.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,16 @@ interface Match {
464464
return assets(pattern, AssetSource.create(getClass().getClassLoader(), source));
465465
}
466466

467+
/**
468+
* Add a static resource handler. Static resources are resolved from root classpath.
469+
*
470+
* @param pattern Path pattern.
471+
* @return A route.
472+
*/
473+
default @Nonnull Route assets(@Nonnull String pattern) {
474+
return assets(pattern, "/");
475+
}
476+
467477
/**
468478
* Add a static resource handler.
469479
*

jooby/src/main/java/io/jooby/internal/$Chi.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,9 @@ Route findRoute(RouterMatch rctx, String method, String path) {
396396
default:
397397
// catch-all nodes
398398
// rctx.routeParams.Values = append(rctx.routeParams.Values, search)
399-
rctx.value(search);
399+
if (search.length() > 0) {
400+
rctx.value(search);
401+
}
400402
xn = nds[0];
401403
xsearch = "";
402404
}
@@ -596,6 +598,8 @@ public void destroy() {
596598
}
597599
}
598600

601+
private static String BASE_CATCH_ALL = "/?*";
602+
599603
private Node root = new Node();
600604

601605
private boolean caseSensitive;
@@ -620,12 +624,29 @@ public void setIgnoreTrailingSlash(boolean ignoreTrailingSlash) {
620624
}
621625

622626
public void insert(String method, String pattern, Route route) {
627+
String baseCatchAll = baseCatchAll(pattern);
628+
if (baseCatchAll.length() > 1) {
629+
// Add route pattern: /static/?* => /static
630+
insert(method, baseCatchAll, route);
631+
String tail = pattern.substring(baseCatchAll.length() + 2);
632+
pattern = baseCatchAll + "/" + tail;
633+
}
634+
if (pattern.equals(BASE_CATCH_ALL)) {
635+
pattern = "/*";
636+
}
623637
root.insertRoute(method, pattern, route);
624638
}
625639

640+
private String baseCatchAll(String pattern) {
641+
int i = pattern.indexOf(BASE_CATCH_ALL);
642+
if (i > 0) {
643+
return pattern.substring(0, i);
644+
}
645+
return "";
646+
}
626647

627648
public void insert(Route route) {
628-
root.insertRoute(route.getMethod(), route.getPattern(), route);
649+
insert(route.getMethod(), route.getPattern(), route);
629650
}
630651

631652
@Override public void destroy() {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.jooby.internal;
2+
3+
import io.jooby.Asset;
4+
import io.jooby.AssetSource;
5+
6+
import javax.annotation.Nonnull;
7+
import javax.annotation.Nullable;
8+
import java.nio.file.Path;
9+
10+
public class FileDiskAssetSource implements AssetSource {
11+
private Path filepath;
12+
13+
public FileDiskAssetSource(@Nonnull Path filepath) {
14+
this.filepath = filepath;
15+
}
16+
17+
@Nullable @Override public Asset resolve(@Nonnull String path) {
18+
return Asset.create(filepath);
19+
}
20+
21+
@Override public String toString() {
22+
return filepath.toString();
23+
}
24+
}

0 commit comments

Comments
 (0)