/** * 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(); } }