/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jooby.internal;
import static java.util.Objects.requireNonNull;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.jooby.Asset;
import org.jooby.Cookie;
import org.jooby.Cookie.Definition;
import org.jooby.Deferred;
import org.jooby.MediaType;
import org.jooby.Mutant;
import org.jooby.Renderer;
import org.jooby.Response;
import org.jooby.Result;
import org.jooby.Results;
import org.jooby.Route;
import org.jooby.Route.After;
import org.jooby.Route.Complete;
import org.jooby.Status;
import org.jooby.internal.parser.ParserExecutor;
import org.jooby.spi.NativeResponse;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableList;
import javaslang.control.Try;
public class ResponseImpl implements Response {
private static final String LOCATION = "Location";
/** Char encoded content disposition. */
private static final String CONTENT_DISPOSITION = "attachment; filename=\"%s\"; filename*=%s''%s";
private final NativeResponse rsp;
private final Map<String, Object> locals;
private Route route;
private Charset charset;
private final Optional<String> referer;
private Status status;
private MediaType type;
private long len = -1;
private Map<String, Cookie> cookies = new HashMap<>();
private List<Renderer> renderers;
private ParserExecutor parserExecutor;
private Map<String, Renderer> rendererMap;
private List<Route.After> after = new ArrayList<>();
private List<Route.Complete> complete = new ArrayList<>();
private RequestImpl req;
private boolean failure;
private Optional<String> byteRange;
public ResponseImpl(final RequestImpl req, final ParserExecutor parserExecutor,
final NativeResponse rsp, final Route route, final List<Renderer> renderers,
final Map<String, Renderer> rendererMap, final Map<String, Object> locals,
final Charset charset, final Optional<String> referer, final Optional<String> byteRange) {
this.req = req;
this.parserExecutor = parserExecutor;
this.rsp = rsp;
this.route = route;
this.locals = locals;
this.renderers = renderers;
this.rendererMap = rendererMap;
this.charset = charset;
this.referer = referer;
this.byteRange = byteRange;
}
@Override
public void download(final String filename, final InputStream stream) throws Throwable {
requireNonNull(filename, "A file's name is required.");
requireNonNull(stream, "A stream is required.");
// handle type
type(type().orElseGet(() -> MediaType.byPath(filename).orElse(MediaType.octetstream)));
Asset asset = new InputStreamAsset(stream, filename, type().get());
contentDisposition(filename);
send(Results.with(asset.stream()));
}
@Override
public void download(final String filename, final String location) throws Throwable {
URL url = getClass().getResource(location.startsWith("/") ? location : "/" + location);
if (url == null) {
throw new FileNotFoundException(location);
}
// handle type
type(type().orElseGet(() -> MediaType.byPath(filename).orElse(MediaType.byPath(location)
.orElse(MediaType.octetstream))));
URLAsset asset = new URLAsset(url, location, type().get());
length(asset.length());
contentDisposition(filename);
send(Results.with(asset));
}
@Override
public Response clearCookie(final String name) {
requireNonNull(name, "Cookie's name required.");
return cookie(new Cookie.Definition(name).maxAge(0));
}
@Override
public Response cookie(final Definition cookie) {
requireNonNull(cookie, "Cookie required.");
// use default path if none-set
cookie.path(cookie.path().orElse(Route.normalize(req.contextPath() + "/")));
return cookie(cookie.toCookie());
}
@Override
public Response cookie(final Cookie cookie) {
requireNonNull(cookie, "Cookie required.");
String name = cookie.name();
// clear cookie?
if (cookie.maxAge() == 0) {
// clear previously set cookie, otherwise ignore them
if (cookies.remove(name) == null) {
// cookie was set in a previous req, we must send a expire header.
cookies.put(name, cookie);
}
} else {
cookies.put(name, cookie);
}
return this;
}
@Override
public Mutant header(final String name) {
requireNonNull(name, "A header's name is required.");
return new MutantImpl(parserExecutor,
new StrParamReferenceImpl("header", name, rsp.headers(name)));
}
@Override
public Response header(final String name, final Object value) {
requireNonNull(name, "Header's name is required.");
requireNonNull(value, "Header's value is required.");
return setHeader(name, value);
}
@Override
public Response header(final String name, final Iterable<Object> values) {
requireNonNull(name, "Header's name is required.");
requireNonNull(values, "Header's values are required.");
return setHeader(name, values);
}
@Override
public Charset charset() {
return charset;
}
@Override
public Response charset(final Charset charset) {
this.charset = requireNonNull(charset, "A charset is required.");
type(type().orElse(MediaType.html));
return this;
}
@Override
public Response length(final long length) {
len = length;
rsp.header("Content-Length", Long.toString(length));
return this;
}
@Override
public Optional<MediaType> type() {
return Optional.ofNullable(type);
}
@Override
public Response type(final MediaType type) {
this.type = type;
if (type.isText()) {
header("Content-Type", type.name() + ";charset=" + charset.name());
} else {
header("Content-Type", type.name());
}
return this;
}
@Override
public void redirect(final Status status, final String location) throws Throwable {
requireNonNull(status, "Status required.");
requireNonNull(location, "Location required.");
send(Results.with(status).header(LOCATION, location));
}
@Override
public Optional<Status> status() {
return Optional.ofNullable(status);
}
@Override
public Response status(final Status status) {
this.status = requireNonNull(status, "Status required.");
rsp.statusCode(status.value());
failure = status.isError();
return this;
}
@Override
public boolean committed() {
return rsp.committed();
}
public void done(final Optional<Throwable> cause) {
if (complete.size() > 0) {
for (Route.Complete h : complete) {
Try.run(() -> h.handle(req, this, cause))
.onFailure(x -> LoggerFactory.getLogger(Response.class)
.error("complete listener resulted in error", x));
}
complete.clear();
}
end();
}
@Override
public void end() {
if (!rsp.committed()) {
if (status == null) {
status(rsp.statusCode());
}
writeCookies();
/**
* Do we need to figure it out Content-Length?
*/
boolean lenSet = rsp.header("Content-Length").isPresent()
|| rsp.header("Transfer-Encoding").isPresent();
if (!lenSet) {
int statusCode = status.value();
boolean hasBody = true;
if (statusCode >= 100 && statusCode < 200) {
hasBody = false;
} else if (statusCode == 204 || statusCode == 304) {
hasBody = false;
}
if (hasBody) {
rsp.header("Content-Length", "0");
}
}
}
rsp.end();
}
@Override
public void send(final Result result) throws Throwable {
if (result instanceof Deferred) {
throw new DeferredExecution((Deferred) result);
}
Result finalResult = result;
if (!failure) {
// after filter
for (int i = after.size() - 1; i >= 0; i--) {
finalResult = after.get(i).handle(req, this, finalResult);
}
}
Optional<MediaType> rtype = finalResult.type();
if (rtype.isPresent()) {
type(rtype.get());
}
status(finalResult.status().orElseGet(() -> status().orElseGet(() -> Status.OK)));
Map<String, Object> headers = finalResult.headers();
if (headers.size() > 0) {
headers.forEach(this::setHeader);
}
writeCookies();
if (Route.HEAD.equals(route.method())) {
end();
return;
}
/**
* Do we need to figure it out Content-Length?
*/
List<MediaType> produces = this.type == null ? route.produces() : ImmutableList.of(type);
Object value = finalResult.get(produces);
if (value != null) {
if (value instanceof Status) {
// override status when message is a status
status((Status) value);
}
Consumer<Long> setLen = len -> {
if (this.len == -1 && len >= 0) {
length(len);
}
};
Consumer<MediaType> setType = type -> {
if (this.type == null) {
type(type);
}
};
HttpRendererContext ctx = new HttpRendererContext(
renderers,
rsp,
setLen,
setType,
locals,
produces,
charset,
req.locale(),
byteRange);
// explicit renderer?
Renderer renderer = rendererMap.get(route.attr("renderer"));
if (renderer != null) {
renderer.render(value, ctx);
} else {
ctx.render(value);
}
}
// end response
end();
}
@Override
public void after(final After handler) {
after.add(handler);
}
@Override
public void complete(final Complete handler) {
complete.add(handler);
}
private void writeCookies() {
if (cookies.size() > 0) {
List<String> setCookie = cookies.values().stream()
.map(Cookie::encode)
.collect(Collectors.toList());
rsp.header("Set-Cookie", setCookie);
cookies.clear();
}
}
public void reset() {
status = null;
this.cookies.clear();
rsp.reset();
}
void route(final Route route) {
this.route = route;
}
private void contentDisposition(final String filename) throws IOException {
List<String> headers = rsp.headers("Content-Disposition");
if (headers.isEmpty()) {
String basename = filename;
int last = filename.lastIndexOf('/');
if (last >= 0) {
basename = basename.substring(last + 1);
}
String cs = charset.name();
String ebasename = URLEncoder.encode(basename, cs).replaceAll("\\+", "%20");
header("Content-Disposition", String.format(CONTENT_DISPOSITION, basename, cs, ebasename));
}
}
@SuppressWarnings("unchecked")
private Response setHeader(final String name, final Object value) {
if (!committed()) {
if (value instanceof Iterable) {
List<String> values = StreamSupport.stream(((Iterable<Object>) value).spliterator(), false)
.map(Headers::encode)
.collect(Collectors.toList());
rsp.header(name, values);
} else {
if (LOCATION.equalsIgnoreCase(name)) {
String location = value.toString();
String cpath = req.contextPath();
if ("back".equalsIgnoreCase(location)) {
location = referer.orElse(cpath + "/");
} else if (location.startsWith("/") && !location.startsWith(cpath)) {
location = cpath + location;
}
rsp.header(LOCATION, location);
} else {
if ("Content-Type".equalsIgnoreCase(name)) {
// keep type reference
this.type = MediaType.valueOf(value.toString());
}
rsp.header(name, Headers.encode(value));
}
}
}
return this;
}
}