Skip to content

Commit 920fb41

Browse files
committed
Static files implementation
- support for etag and last-modified headers - support fs and classpath resources
1 parent 673b0ce commit 920fb41

File tree

23 files changed

+658
-29
lines changed

23 files changed

+658
-29
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*
14+
* Copyright 2014 Edgar Espina
15+
*/
16+
package examples;
17+
18+
import io.jooby.Jooby;
19+
20+
import java.nio.file.Path;
21+
import java.nio.file.Paths;
22+
23+
public class AssetsApp extends Jooby {
24+
25+
{
26+
Path www = Paths.get(System.getProperty("user.dir"), "examples", "www");
27+
assets("/*", www);
28+
assets("/static/*", www);
29+
}
30+
31+
public static void main(String[] args) {
32+
run(AssetsApp::new, args);
33+
}
34+
}

examples/www/css/styles.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
body {
2+
background-color: lightblue;
3+
}
4+
5+
h1 {
6+
color: white;
7+
text-align: center;
8+
}
9+
10+
p {
11+
font-family: verdana;
12+
font-size: 20px;
13+
}

examples/www/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Title of the document</title>
6+
<link rel="stylesheet" type="text/css" href="css/styles.css">
7+
</head>
8+
<body>
9+
Content of the document......
10+
<script src="js/index.js"></script>
11+
</body>
12+
</html>

examples/www/js/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
document.write(5 + 6 + 7);
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package io.jooby;
2+
3+
import javax.annotation.Nonnull;
4+
import java.io.FileInputStream;
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.net.URL;
8+
import java.net.URLConnection;
9+
import java.nio.ByteBuffer;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.util.Base64;
13+
14+
public interface Asset {
15+
16+
class FileAsset implements Asset {
17+
18+
private Path file;
19+
20+
public FileAsset(Path file) {
21+
this.file = file;
22+
}
23+
24+
@Override public long length() {
25+
try {
26+
return Files.size(file);
27+
} catch (IOException x) {
28+
throw Throwing.sneakyThrow(x);
29+
}
30+
}
31+
32+
@Override public long lastModified() {
33+
try {
34+
return Files.getLastModifiedTime(file).toMillis();
35+
} catch (IOException x) {
36+
throw Throwing.sneakyThrow(x);
37+
}
38+
}
39+
40+
@Nonnull @Override public MediaType type() {
41+
return MediaType.byFile(file);
42+
}
43+
44+
@Override public InputStream content() {
45+
try {
46+
return new FileInputStream(file.toFile());
47+
} catch (IOException x) {
48+
throw Throwing.sneakyThrow(x);
49+
}
50+
}
51+
52+
@Override public void release() {
53+
// NOOP
54+
}
55+
56+
@Override public boolean equals(Object obj) {
57+
if (obj instanceof FileAsset) {
58+
return file.equals(((FileAsset) obj).file);
59+
}
60+
return false;
61+
}
62+
63+
@Override public int hashCode() {
64+
return file.hashCode();
65+
}
66+
67+
@Override public String toString() {
68+
return file.toString();
69+
}
70+
}
71+
72+
class URLAsset implements Asset {
73+
74+
private final URL resource;
75+
76+
private final String path;
77+
78+
private long len;
79+
80+
private long lastModified;
81+
82+
private InputStream content;
83+
84+
private URLAsset(URL resource, String path) {
85+
this.resource = resource;
86+
this.path = path;
87+
}
88+
89+
@Override public long length() {
90+
checkOpen();
91+
return len;
92+
}
93+
94+
@Override public long lastModified() {
95+
checkOpen();
96+
return lastModified;
97+
}
98+
99+
@Nonnull @Override public MediaType type() {
100+
return MediaType.byFile(path);
101+
}
102+
103+
@Override public InputStream content() {
104+
checkOpen();
105+
return content;
106+
}
107+
108+
@Override public void release() {
109+
try {
110+
content.close();
111+
} catch (IOException | NullPointerException x) {
112+
// NPE when content is a directory
113+
}
114+
}
115+
116+
@Override public boolean equals(Object obj) {
117+
if (obj instanceof URLAsset) {
118+
return path.equals(((URLAsset) obj).path);
119+
}
120+
return false;
121+
}
122+
123+
@Override public int hashCode() {
124+
return path.hashCode();
125+
}
126+
127+
@Override public String toString() {
128+
return path;
129+
}
130+
131+
private void checkOpen() {
132+
try {
133+
if (content == null) {
134+
URLConnection connection = resource.openConnection();
135+
connection.setUseCaches(false);
136+
len = connection.getContentLengthLong();
137+
lastModified = connection.getLastModified();
138+
content = connection.getInputStream();
139+
}
140+
} catch (IOException x) {
141+
throw Throwing.sneakyThrow(x);
142+
}
143+
}
144+
}
145+
146+
static Asset create(Path resource) {
147+
return new FileAsset(resource);
148+
}
149+
150+
static Asset create(String path, URL resource) {
151+
return new URLAsset(resource, path);
152+
}
153+
154+
/**
155+
* @return Asset size (in bytes) or <code>-1</code> if undefined.
156+
*/
157+
long length();
158+
159+
/**
160+
* @return The last modified date if possible or -1 when isn't.
161+
*/
162+
long lastModified();
163+
164+
default String etag() {
165+
StringBuilder b = new StringBuilder(32);
166+
b.append("W/\"");
167+
168+
Base64.Encoder encoder = Base64.getEncoder();
169+
int hashCode = hashCode();
170+
long lastModified = lastModified();
171+
long length = length();
172+
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
173+
174+
buffer.putLong(lastModified ^ hashCode);
175+
b.append(Long.toHexString(lastModified ^ hashCode));
176+
177+
buffer.clear();
178+
buffer.putLong(length ^ hashCode);
179+
b.append(encoder.encodeToString(buffer.array()));
180+
181+
b.append('"');
182+
return b.toString();
183+
}
184+
185+
/**
186+
* @return Asset media type.
187+
*/
188+
@Nonnull
189+
MediaType type();
190+
191+
InputStream content();
192+
193+
void release();
194+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package io.jooby;
2+
3+
import javax.annotation.Nonnull;
4+
import java.util.Date;
5+
import java.util.List;
6+
7+
public class AssetHandler implements Route.Handler {
8+
private final AssetSource[] sources;
9+
10+
private boolean etag = true;
11+
12+
private boolean lastModified = true;
13+
14+
private long maxAge = -1;
15+
16+
private String filekey;
17+
18+
public AssetHandler(AssetSource... sources) {
19+
this.sources = sources;
20+
}
21+
22+
@Nonnull @Override public Object apply(@Nonnull Context ctx) throws Exception {
23+
String filepath = ctx.pathMap().get(filekey);
24+
Asset asset = resolve(filepath);
25+
if (asset == null) {
26+
ctx.sendStatusCode(StatusCode.NOT_FOUND);
27+
return ctx;
28+
}
29+
30+
// handle If-None-Match
31+
if (this.etag) {
32+
String ifnm = ctx.header("If-None-Match").value((String) null);
33+
if (ifnm != null && ifnm.equals(asset.etag())) {
34+
ctx.sendStatusCode(StatusCode.NOT_MODIFIED);
35+
asset.release();
36+
return ctx;
37+
} else {
38+
ctx.header("ETag", asset.etag());
39+
}
40+
}
41+
42+
// Handle If-Modified-Since
43+
if (this.lastModified) {
44+
long lastModified = asset.lastModified();
45+
if (lastModified > 0) {
46+
long ifms = ctx.header("If-Modified-Since").longValue(-1);
47+
if (lastModified <= ifms) {
48+
ctx.sendStatusCode(StatusCode.NOT_MODIFIED);
49+
asset.release();
50+
return ctx;
51+
}
52+
ctx.header("Last-Modified", new Date(lastModified));
53+
}
54+
}
55+
56+
// cache max-age
57+
if (maxAge > 0) {
58+
ctx.header("Cache-Control", "max-age=" + maxAge);
59+
}
60+
61+
long length = asset.length();
62+
if (length != -1) {
63+
ctx.length(length);
64+
}
65+
ctx.type(asset.type());
66+
return ctx.sendStream(asset.content());
67+
}
68+
69+
public AssetHandler etag(boolean etag) {
70+
this.etag = etag;
71+
return this;
72+
}
73+
74+
public AssetHandler lastModified(boolean lastModified) {
75+
this.lastModified = lastModified;
76+
return this;
77+
}
78+
79+
public AssetHandler maxAge(long maxAge) {
80+
this.maxAge = maxAge;
81+
return this;
82+
}
83+
84+
private Asset resolve(String filepath) {
85+
for (AssetSource source : sources) {
86+
Asset asset = source.resolve(filepath);
87+
if (asset != null) {
88+
return asset;
89+
}
90+
}
91+
return null;
92+
}
93+
94+
void route(Route route) {
95+
List<String> keys = route.pathKeys();
96+
this.filekey = keys.size() == 0 ? "*" : keys.get(0);
97+
}
98+
}

0 commit comments

Comments
 (0)