/**
* 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.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Locale.LanguageRange;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.jooby.Cookie;
import org.jooby.Env;
import org.jooby.Err;
import org.jooby.MediaType;
import org.jooby.Mutant;
import org.jooby.Parser;
import org.jooby.Request;
import org.jooby.Response;
import org.jooby.Route;
import org.jooby.Session;
import org.jooby.Status;
import org.jooby.Upload;
import org.jooby.internal.parser.ParserExecutor;
import org.jooby.spi.NativeRequest;
import org.jooby.spi.NativeUpload;
import com.google.common.collect.ImmutableList;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.typesafe.config.Config;
import javaslang.control.Try;
public class RequestImpl implements Request {
private final Map<String, Mutant> params = new HashMap<>();
private final List<MediaType> accept;
private final MediaType type;
private final Injector injector;
private final NativeRequest req;
private final Map<Object, Object> scope;
private final Map<String, Object> locals;
private Route route;
private Optional<Session> reqSession;
private Charset charset;
private List<File> files;
private int port;
private String contextPath;
private Optional<String> lang;
private List<Locale> locales;
private long timestamp;
public RequestImpl(final Injector injector, final NativeRequest req, final String contextPath,
final int port, final Route route, final Charset charset, final List<Locale> locales,
final Map<Object, Object> scope, final Map<String, Object> locals, final long timestamp) {
this.injector = injector;
this.req = req;
this.route = route;
this.scope = scope;
this.locals = locals;
this.contextPath = contextPath;
Optional<String> accept = req.header("Accept");
this.accept = accept.isPresent() ? MediaType.parse(accept.get()) : MediaType.ALL;
this.lang = req.header("Accept-Language");
this.locales = locales;
this.port = port;
Optional<String> type = req.header("Content-Type");
this.type = type.isPresent() ? MediaType.valueOf(type.get()) : MediaType.all;
String cs = this.type.params().get("charset");
this.charset = cs != null ? Charset.forName(cs) : charset;
this.files = new ArrayList<>();
this.timestamp = timestamp;
}
@Override
public String contextPath() {
return contextPath;
}
@Override
public Optional<String> queryString() {
return req.queryString();
}
@SuppressWarnings("unchecked")
@Override
public <T> Optional<T> ifGet(final String name) {
requireNonNull(name, "A local's name is required.");
return Optional.ofNullable((T) locals.get(name));
}
@Override
public boolean matches(final String pattern) {
RoutePattern p = new RoutePattern("*", pattern);
return p.matcher(route.path()).matches();
}
@Override
public Map<String, Object> attributes() {
return Collections.unmodifiableMap(locals);
}
@SuppressWarnings("unchecked")
@Override
public <T> Optional<T> unset(final String name) {
requireNonNull(name, "A local's name is required.");
return Optional.ofNullable((T) locals.remove(name));
}
@Override
public MediaType type() {
return type;
}
@Override
public List<MediaType> accept() {
return accept;
}
@Override
public Optional<MediaType> accepts(final List<MediaType> types) {
requireNonNull(types, "Media types are required.");
return MediaType.matcher(accept()).first(types);
}
@Override
public Mutant params(final String... xss) {
return _params(xss(xss));
}
@Override
public Mutant params() {
return _params(null);
}
private Mutant _params(final Function<String, String> xss) {
Map<String, Mutant> params = new HashMap<>();
for (Object segment : route.vars().keySet()) {
if (segment instanceof String) {
String name = (String) segment;
params.put(name, _param(name, xss));
}
}
for (String name : paramNames()) {
params.put(name, _param(name, xss));
}
return new MutantImpl(require(ParserExecutor.class), params);
}
@Override
public Mutant param(final String name, final String... xss) {
return _param(name, xss(xss));
}
@Override
public Mutant param(final String name) {
return _param(name, null);
}
private Mutant _param(final String name, final Function<String, String> xss) {
Mutant param = this.params.get(name);
if (param == null) {
List<NativeUpload> files = Try.of(() -> req.files(name)).getOrElseThrow(
ex -> new Err(Status.BAD_REQUEST, "Upload " + name + " resulted in error", ex));
if (files.size() > 0) {
List<Upload> uploads = files.stream()
.map(upload -> new UploadImpl(injector, upload))
.collect(Collectors.toList());
param = new MutantImpl(require(ParserExecutor.class), type(),
new UploadParamReferenceImpl(name, uploads));
this.params.put(name, param);
} else {
StrParamReferenceImpl paramref = new StrParamReferenceImpl("parameter", name,
params(name, xss));
param = new MutantImpl(require(ParserExecutor.class), paramref);
if (paramref.size() > 0) {
this.params.put(name, param);
}
}
}
return param;
}
@Override
public Mutant header(final String name) {
return _header(name, null);
}
@Override
public Mutant header(final String name, final String... xss) {
return _header(name, xss(xss));
}
private Mutant _header(final String name, final Function<String, String> xss) {
requireNonNull(name, "Name required.");
List<String> headers = req.headers(name);
if (xss != null) {
headers = headers.stream()
.map(xss::apply)
.collect(Collectors.toList());
}
return new MutantImpl(require(ParserExecutor.class),
new StrParamReferenceImpl("header", name, headers));
}
@Override
public Map<String, Mutant> headers() {
Map<String, Mutant> headers = new LinkedHashMap<>();
req.headerNames().forEach(name -> headers.put(name, header(name)));
return headers;
}
@Override
public Mutant cookie(final String name) {
List<String> values = req.cookies().stream().filter(c -> c.name().equalsIgnoreCase(name))
.findFirst()
.map(cookie -> ImmutableList.of(cookie.value().get()))
.orElse(ImmutableList.of());
return new MutantImpl(require(ParserExecutor.class),
new StrParamReferenceImpl("cookie", name, values));
}
@Override
public List<Cookie> cookies() {
return req.cookies();
}
@Override
public Mutant body() throws Exception {
long length = length();
if (length > 0) {
MediaType type = type();
Config conf = require(Config.class);
File fbody = new File(conf.getString("application.tmpdir"),
Integer.toHexString(System.identityHashCode(this)));
files.add(fbody);
int bufferSize = conf.getBytes("server.http.RequestBufferSize").intValue();
Parser.BodyReference body = new BodyReferenceImpl(length, charset(), fbody, req.in(),
bufferSize);
return new MutantImpl(require(ParserExecutor.class), type, body);
}
return new MutantImpl(require(ParserExecutor.class), type, new EmptyBodyReference());
}
@Override
public <T> T require(final Key<T> key) {
return injector.getInstance(key);
}
@Override
public Charset charset() {
return charset;
}
@Override
public long length() {
return req.header("Content-Length")
.map(Long::parseLong)
.orElse(-1L);
}
@Override
public List<Locale> locales(
final BiFunction<List<Locale.LanguageRange>, List<Locale>, List<Locale>> filter) {
return lang.map(h -> filter.apply(LocaleUtils.range(h), locales))
.orElseGet(() -> filter.apply(ImmutableList.of(), locales));
}
@Override
public Locale locale(final BiFunction<List<LanguageRange>, List<Locale>, Locale> filter) {
Supplier<Locale> def = () -> filter.apply(ImmutableList.of(), locales);
// don't fail on bad Accept-Language header, just fallback to default locale.
return lang.map(h -> Try.of(() -> filter.apply(LocaleUtils.range(h), locales)).getOrElse(def))
.orElseGet(def);
}
@Override
public String ip() {
return req.ip();
}
@Override
public Route route() {
return route;
}
@Override
public String rawPath() {
return req.rawPath();
}
@Override
public String hostname() {
return req.header("host").map(host -> host.split(":")[0]).orElse(ip());
}
@Override
public int port() {
return req.header("host").map(host -> {
String[] parts = host.split(":");
if (parts.length > 1) {
return Integer.parseInt(parts[1]);
}
// fallback to default ports
return secure() ? 443 : 80;
}).orElse(port);
}
@Override
public Session session() {
return ifSession().orElseGet(() -> {
SessionManager sm = require(SessionManager.class);
Response rsp = require(Response.class);
Session gsession = sm.create(this, rsp);
return setSession(sm, rsp, gsession);
});
}
@Override
public Optional<Session> ifSession() {
if (reqSession == null) {
SessionManager sm = require(SessionManager.class);
Response rsp = require(Response.class);
Session gsession = sm.get(this, rsp);
if (gsession == null) {
reqSession = Optional.empty();
} else {
setSession(sm, rsp, gsession);
}
}
return reqSession;
}
@Override
public String protocol() {
return req.protocol();
}
@Override
public boolean secure() {
return req.secure();
}
@Override
public Request set(final String name, final Object value) {
requireNonNull(name, "A local's name is required.");
requireNonNull(value, "A local's value is required.");
locals.put(name, value);
return this;
}
@Override
public Request set(final Key<?> key, final Object value) {
requireNonNull(key, "A local's jey is required.");
requireNonNull(value, "A local's value is required.");
scope.put(key, value);
return this;
}
@Override
public Request push(final String path, final Map<String, Object> headers) {
if (protocol().equalsIgnoreCase("HTTP/2.0")) {
require(Response.class).after((req, rsp, value) -> {
this.req.push("GET", contextPath + path, headers);
return value;
});
return this;
} else {
throw new UnsupportedOperationException("Push promise not available");
}
}
@Override
public String toString() {
return route().toString();
}
private List<String> paramNames() {
try {
return req.paramNames();
} catch (Exception ex) {
throw new Err(Status.BAD_REQUEST, "Unable to get parameter names", ex);
}
}
private Function<String, String> xss(final String... xss) {
return require(Env.class).xss(xss);
}
private List<String> params(final String name, final Function<String, String> xss) {
try {
List<String> values = new ArrayList<>();
String pathvar = route.vars().get(name);
if (pathvar != null) {
values.add(pathvar);
}
values.addAll(req.params(name));
if (xss == null) {
return values;
}
for (int i = 0; i < values.size(); i++) {
values.set(i, xss.apply(values.get(i)));
}
return values;
} catch (Throwable ex) {
throw new Err(Status.BAD_REQUEST, "Parameter '" + name + "' resulted in error", ex);
}
}
void route(final Route route) {
this.route = route;
}
public void done() {
if (reqSession != null) {
reqSession.ifPresent(session -> require(SessionManager.class).requestDone(session));
}
if (files.size() > 0) {
for (File file : files) {
file.delete();
}
}
}
@Override
public long timestamp() {
return timestamp;
}
private Session setSession(final SessionManager sm, final Response rsp, final Session gsession) {
Session rsession = new RequestScopedSession(sm, rsp, gsession, this::destroySession);
reqSession = Optional.of(rsession);
return rsession;
}
private void destroySession() {
this.reqSession = Optional.empty();
}
}