org.rapidoid.http.impl.ReqImpl Maven / Gradle / Ivy
The newest version!
/*-
* #%L
* rapidoid-http-fast
* %%
* Copyright (C) 2014 - 2018 Nikolche Mihajlovski and contributors
* %%
* Licensed 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.
* #L%
*/
package org.rapidoid.http.impl;
import org.rapidoid.RapidoidThing;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.buffer.Buf;
import org.rapidoid.cache.Cache;
import org.rapidoid.cls.Cls;
import org.rapidoid.collection.ChangeTrackingMap;
import org.rapidoid.collection.Coll;
import org.rapidoid.commons.Str;
import org.rapidoid.http.*;
import org.rapidoid.http.customize.BeanParameterFactory;
import org.rapidoid.http.customize.Customization;
import org.rapidoid.http.customize.HttpRequestBodyParser;
import org.rapidoid.http.customize.SessionManager;
import org.rapidoid.http.impl.lowlevel.HttpIO;
import org.rapidoid.io.Upload;
import org.rapidoid.log.Log;
import org.rapidoid.net.abstracts.Channel;
import org.rapidoid.net.abstracts.IRequest;
import org.rapidoid.u.U;
import org.rapidoid.util.Constants;
import org.rapidoid.util.Msc;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
@Authors("Nikolche Mihajlovski")
@Since("5.0.2")
public class ReqImpl extends RapidoidThing implements Req, Constants, HttpMetadata, IRequest, MaybeReq {
public static final long UNDEFINED = Long.MAX_VALUE;
private final FastHttp http;
private final Channel channel;
private volatile boolean stopped = false;
private volatile boolean isKeepAlive;
private volatile String verb;
private volatile String uri;
private volatile String path;
private volatile String query;
private volatile String zone;
private volatile String contextPath;
private volatile byte[] body;
private final Map params;
private final Map headers;
private final Map cookies;
private final Map posted;
private final Map> files;
private final Map attrs = Collections.synchronizedMap(new HashMap());
private volatile Map data;
private volatile ChangeTrackingMap token;
final AtomicBoolean tokenChanged = new AtomicBoolean();
private volatile TokenStatus tokenStatus = TokenStatus.PENDING;
private volatile ChangeTrackingMap session;
private final AtomicBoolean sessionChanged = new AtomicBoolean();
private volatile RespImpl response;
private volatile boolean rendering;
private volatile long posContentLengthValue;
private volatile long posBeforeBody = UNDEFINED;
private volatile boolean async;
private volatile boolean done;
private volatile boolean completed;
private volatile boolean pendingBodyParsing;
private final MediaType defaultContentType;
private final HttpRoutesImpl routes;
private final Route route;
private final Customization custom;
private final HTTPCacheKey cacheKey;
private volatile boolean cached;
private final long connId;
private final long handle;
private final long requestId;
public ReqImpl(FastHttp http, Channel channel, boolean isKeepAlive, String verb, String uri, String path,
String query, byte[] body, Map params, Map headers,
Map cookies, Map posted, Map> files,
boolean pendingBodyParsing, MediaType defaultContentType, String zone, Route route) {
this.http = http;
this.channel = channel;
this.isKeepAlive = isKeepAlive;
this.verb = verb;
this.uri = uri;
this.path = path;
this.query = query;
this.body = body;
this.params = params;
this.headers = headers;
this.cookies = cookies;
this.posted = posted;
this.files = files;
this.pendingBodyParsing = pendingBodyParsing;
this.defaultContentType = defaultContentType;
this.zone = zone;
this.routes = http.routes();
this.route = route;
this.connId = channel.connId();
this.handle = channel.handle();
this.requestId = channel.requestId();
this.custom = http.custom();
this.cacheKey = createCacheKey();
}
private HTTPCacheKey createCacheKey() {
return isCacheable() ? new HTTPCacheKey(host(), uri()) : null;
}
@Override
public String verb() {
return verb;
}
public Req verb(String verb) {
this.verb = verb;
return this;
}
@Override
public String uri() {
return uri;
}
public Req uri(String uri) {
this.uri = uri;
return this;
}
@Override
public String path() {
return path;
}
public Req path(String path) {
this.path = path;
return this;
}
@Override
public String query() {
return query;
}
public Req query(String query) {
this.query = query;
return this;
}
@Override
public byte[] body() {
return body;
}
public Req body(byte[] body) {
this.body = body;
return this;
}
@Override
public Map params() {
return params;
}
@Override
public Map headers() {
return headers;
}
@Override
public Map cookies() {
return cookies;
}
@Override
public Map posted() {
if (pendingBodyParsing) {
synchronized (this) {
if (pendingBodyParsing) {
pendingBodyParsing = false;
parseRequestBody();
}
}
}
return posted;
}
@Override
public Map> files() {
return files;
}
@Override
public String clientIpAddress() {
return channel.address();
}
@Override
public String realIpAddress() {
return HttpUtils.inferRealIpAddress(this);
}
@Override
public String host() {
return header(HttpHeaders.HOST.name(), null);
}
public Req host(String host) {
headers().put(HttpHeaders.HOST.name(), host);
return this;
}
@Override
public long connectionId() {
return connId;
}
@Override
public long requestId() {
return requestId;
}
@Override
public String param(String name) {
return U.notNull(params().get(name), "PARAMS[%s]", name);
}
@Override
public String param(String name, String defaultValue) {
return withDefault(params().get(name), defaultValue);
}
@Override
public T param(Class beanType) {
return beanFrom(beanType, params());
}
@Override
public String header(String name) {
return U.notNull(headers().get(name.toLowerCase()), "HEADERS[%s]", name);
}
@Override
public String header(String name, String defaultValue) {
return U.or(headers().get(name.toLowerCase()), defaultValue);
}
@Override
public String cookie(String name) {
return U.notNull(cookies().get(name), "COOKIES[%s]", name);
}
@Override
public String cookie(String name, String defaultValue) {
return U.or(cookies().get(name), defaultValue);
}
@SuppressWarnings("unchecked")
@Override
public T posted(String name) {
return (T) U.notNull(posted().get(name), "POSTED[%s]", name);
}
@Override
public T posted(String name, T defaultValue) {
return withDefault(posted().get(name), defaultValue);
}
@Override
public T posted(Class beanType) {
return beanFrom(beanType, posted());
}
@Override
public List files(String name) {
return U.notNull(files().get(name), "FILES[%s]", name);
}
@Override
public Upload file(String name) {
List uploads = files(name);
U.must(uploads.size() == 1, "Expected exactly 1 uploaded file for parameter '%s', but found %s!", name, uploads.size());
return uploads.get(0);
}
@Override
public Map data() {
if (data == null) {
synchronized (this) {
if (data == null) {
Map allData = U.map();
allData.putAll(params);
allData.putAll(files);
allData.putAll(posted());
data = Collections.unmodifiableMap(allData);
}
}
}
return data;
}
@SuppressWarnings("unchecked")
@Override
public T data(String name) {
return (T) U.notNull(data(name, null), "DATA[%s]", name);
}
@Override
public T data(String name, T defaultValue) {
Object value = posted(name, null);
if (value == null) {
value = files().get(name);
if (value == null) {
value = param(name, null);
}
}
return withDefault(value, defaultValue);
}
@Override
public T data(Class beanType) {
return beanFrom(beanType, data());
}
@SuppressWarnings("unchecked")
private T beanFrom(Class beanType, Map properties) {
String paramName = Str.uncapitalized(beanType.getSimpleName());
BeanParameterFactory beanParameterFactory = custom().beanParameterFactory();
try {
return (T) beanParameterFactory.getParamValue(this, beanType, paramName, (Map) properties);
} catch (Exception e) {
throw new RuntimeException("Couldn't instantiate a bean of type: " + beanType.getName());
}
}
@Override
public Map attrs() {
return attrs;
}
@SuppressWarnings("unchecked")
@Override
public T attr(String name) {
return (T) U.notNull(attrs().get(name), "ATTRS[%s]", name);
}
@Override
public T attr(String name, T defaultValue) {
return withDefault(attrs().get(name), defaultValue);
}
@SuppressWarnings("unchecked")
private T withDefault(Object value, T defaultValue) {
if (value != null) {
return (T) (defaultValue != null ? Cls.convert(value, Cls.of(defaultValue)) : value);
} else {
return defaultValue;
}
}
/* RESPONSE */
@Override
public synchronized Resp response() {
if (response == null) {
response = new RespImpl(this);
if (defaultContentType != null) {
response.contentType(defaultContentType);
}
}
return response;
}
void doRendering(int code, RespBody body) {
if (!isRendering()) {
synchronized (this) {
if (!isRendering()) {
respond(code, body);
}
}
}
}
private void respond(int code, RespBody body) {
MediaType contentType = HttpUtils.getDefaultContentType();
if (tokenChanged.get()) {
HttpUtils.saveTokenBeforeRenderingHeaders(this, token);
}
if (sessionChanged.get()) {
saveSession(session.decorated());
}
if (response != null) {
contentType = U.or(response.contentType(), contentType);
}
renderResponse(code, contentType, body);
}
private void renderResponse(int code, MediaType contentType, RespBody body) {
rendering = true;
completed = body != null;
HttpIO.INSTANCE.respond(
HttpUtils.maybe(this), channel, connId, handle,
code, isKeepAlive, contentType, body,
response != null ? response.headers() : null,
response != null ? response.cookies() : null
);
}
public void responded(long posContentLengthValue, long posBeforeBody, boolean completed) {
this.posContentLengthValue = posContentLengthValue;
this.posBeforeBody = posBeforeBody;
this.completed = completed;
}
public void onHeadersCompleted() {
posBeforeBody = channel.output().size();
}
public boolean isRendering() {
return rendering;
}
public ReqImpl completed(boolean completed) {
this.completed = completed;
return this;
}
@Override
public synchronized Req done() {
if (!done) {
onDone();
done = true;
}
return this;
}
private void onDone() {
if (stopped) {
return;
}
boolean willBeDone = true;
if (!rendering) {
renderResponseOrError();
willBeDone = false;
}
if (!completed) {
// FIXME is this still required?
completed = true;
}
if (response != null) {
response.finish();
}
if (willBeDone) {
HttpIO.INSTANCE.done(ReqImpl.this);
}
}
private void renderResponseOrError() {
String err = validateResponse();
if (err != null) {
doRendering(500, new RespBodyBytes(err.getBytes()));
} else {
renderResponse();
}
}
private void renderResponse() {
HttpUtils.postProcessResponse(response);
if (response.raw() != null) {
int posBeforeResponse = channel.output().size();
byte[] bytes = Msc.toBytes(response.raw());
channel.write(bytes);
if (willSaveToCache()) posBeforeBody = posBeforeResponse + HttpUtils.findBodyStart(bytes);
completed = true;
HttpIO.INSTANCE.done(this);
} else {
// render the response body
RespBody body = BodyRenderer.toRespBody(this, response);
// render the response
doRendering(response.code(), body);
}
}
private String validateResponse() {
if (response == null) {
return "Response wasn't provided!";
}
if (response.result() == null && response.body() == null && response.redirect() == null
&& response.file() == null && response.raw() == null && !response().mvc()) {
return "Response content wasn't provided!";
}
if (response.contentType() == null && response.raw() == null) {
return "Response content type wasn't provided!";
}
return null;
}
@Override
public boolean isDone() {
return done;
}
@Override
public HttpRoutes routes() {
return routes;
}
@Override
public Route route() {
return route;
}
@Override
public Customization custom() {
return custom;
}
@Override
public Req async() {
this.async = true;
if (channel.onSameThread()) channel.async();
return this;
}
@Override
public boolean isAsync() {
return async;
}
/* SESSION: */
@Override
public String sessionId() {
String sessionId = cookie(SESSION_COOKIE, null);
if (U.isEmpty(sessionId)) {
sessionId = UUID.randomUUID().toString();
synchronized (cookies) {
if (cookie(SESSION_COOKIE, null) == null) {
cookies.put(SESSION_COOKIE, sessionId);
response().cookie(SESSION_COOKIE, sessionId, "HttpOnly");
}
}
}
return sessionId;
}
@Override
public boolean hasSession() {
return cookie(SESSION_COOKIE, null) != null;
}
@Override
public Map session() {
if (session == null) {
synchronized (this) {
if (session == null) {
session = Coll.trackChanges(loadSession(), sessionChanged);
}
}
}
return session;
}
@SuppressWarnings("unchecked")
@Override
public T session(String name) {
Serializable value = hasSession() ? session().get(name) : null;
return (T) U.notNull(value, "SESSION[%s]", name);
}
@Override
public T session(String name, T defaultValue) {
Serializable value = hasSession() ? session().get(name) : null;
return withDefault(value, defaultValue);
}
/* TOKEN: */
@Override
public boolean hasToken() {
// FIXME don't deserialize token, just check if it exists
return U.notEmpty(token()); // try to find and deserialize the token
}
@Override
public Map token() {
if (tokenStatus == TokenStatus.PENDING) {
synchronized (this) {
if (tokenStatus == TokenStatus.PENDING) {
Map tokenData = null;
try {
tokenData = HttpUtils.initAndDeserializeToken(this);
tokenStatus(tokenData != null ? TokenStatus.LOADED : TokenStatus.NONE);
} catch (Exception e) {
Log.error("Token deserialization error!", e);
tokenStatus(TokenStatus.INVALID);
}
token = Coll.trackChanges(Collections.synchronizedMap(U.safe(tokenData)), tokenChanged);
}
}
}
return token;
}
@SuppressWarnings("unchecked")
@Override
public T token(String name) {
Serializable value = hasToken() ? token().get(name) : null;
return (T) U.notNull(value, "TOKEN[%s]", name);
}
@Override
public T token(String name, T defaultValue) {
Serializable value = hasToken() ? token().get(name) : null;
return withDefault(value, defaultValue);
}
public TokenStatus tokenStatus() {
return tokenStatus;
}
public Req tokenStatus(TokenStatus tokenStatus) {
this.tokenStatus = tokenStatus;
return this;
}
@Override
public String zone() {
return zone;
}
public Req zone(String zone) {
this.zone = zone;
return this;
}
@Override
public String contextPath() {
if (contextPath == null) {
synchronized (this) {
if (contextPath == null) {
contextPath = HttpUtils.getContextPath(this);
}
}
}
return contextPath;
}
public Req contextPath(String contextPath) {
this.contextPath = contextPath;
return this;
}
@Override
public String toString() {
String info = verb() + " " + path();
if (U.notEmpty(params)) {
info += "?" + U.join("&", Msc.protectSensitiveInfo(params, "<...>").entrySet());
}
return info;
}
public Channel channel() {
return channel;
}
public FastHttp http() {
return http;
}
@Override
public void stop() {
this.stopped = true;
}
@Override
public boolean isStopped() {
return stopped;
}
@Override
public void revert() {
rendering = false;
posContentLengthValue = 0;
posBeforeBody = 0;
async = false;
done = false;
completed = false;
response = null;
}
private void parseRequestBody() {
if (U.isEmpty(body())) return;
String contentTypeHeader = header("Content-Type", "application/json");
if (contentTypeHeader.startsWith("application/json")) {
parseRequestBodyUsing(custom().jsonRequestBodyParser());
} else if (contentTypeHeader.startsWith("application/xml")) {
parseRequestBodyUsing(custom().xmlRequestBodyParser());
} else {
throw U.rte("Couldn't parse the request body - unsupported content type: " + contentTypeHeader);
}
}
private void parseRequestBodyUsing(HttpRequestBodyParser parser) {
if (U.notEmpty(body())) {
Map bodyData = null;
try {
bodyData = parser.parseRequestBody(this, body);
} catch (Exception e) {
Log.error("Couldn't parse the request body! Please make sure the correct content type is specified in the request header!", e);
}
if (bodyData != null) {
posted.putAll(bodyData);
}
}
}
private Map loadSession() {
SessionManager sessionManager = U.notNull(custom().sessionManager(), "session manager");
try {
return sessionManager.loadSession(this, sessionId());
} catch (Exception e) {
throw U.rte("Error occurred while loading the session!", e);
}
}
private void saveSession(Map session) {
SessionManager sessionManager = U.notNull(custom().sessionManager(), "session manager");
try {
sessionManager.saveSession(this, sessionId(), session);
} catch (Exception e) {
throw U.rte("Error occurred while saving the session!", e);
}
}
@Override
public OutputStream out() {
return response().out();
}
@Override
public MediaType contentType() {
MediaType contentType = response != null ? response.contentType() : null;
return U.or(contentType, defaultContentType);
}
@Override
public long handle() {
return handle;
}
public boolean isKeepAlive() {
return isKeepAlive;
}
public void doneProcessing() {
done = true;
if (willSaveToCache()) saveToCache();
}
private void saveToCache() {
U.must(posBeforeBody != UNDEFINED);
Buf out = channel.output();
int posAfterBody = out.size();
int bodyLength = (int) (posAfterBody - posBeforeBody);
// FIXME validate '\r\n\r\n' before the start position of the response body
Cache cache = route.cache();
U.notNull(cache, "route.cache");
SimpleHttpResp proxyResp = new SimpleHttpResp();
proxyResp.cookies = U.map(U.safe(response != null ? response.cookies() : null));
Map headers = response != null ? response.headers() : Collections.emptyMap();
HttpUtils.proxyResponseHeaders(headers, proxyResp);
proxyResp.code = response != null ? response.code() : 200;
if (proxyResp.contentType == null) {
proxyResp.contentType = response != null ? response.contentType() : defaultContentType;
}
// don't cache the response if it contains cookies or token data
if (U.notEmpty(proxyResp.cookies) || hasToken()) return;
ByteBuffer body = writeBodyToBuf(out, bodyLength);
CachedResp cached = new CachedResp(proxyResp.code, proxyResp.contentType, proxyResp.headers, body);
cache.set(cacheKey, cached);
}
private ByteBuffer writeBodyToBuf(Buf out, int bodyLength) {
ByteBuffer body = ByteBuffer.allocateDirect(bodyLength);
out.writeTo(body, (int) posBeforeBody, bodyLength);
body.flip();
return body;
}
@Override
public Req getReqOrNull() {
return this;
}
private boolean willSaveToCache() {
return cacheKey != null && !cached;
}
public HTTPCacheKey cacheKey() {
return cacheKey;
}
public boolean cached() {
return cached;
}
public ReqImpl cached(boolean cached) {
this.cached = cached;
return this;
}
private boolean isCacheable() {
return route != null
&& HttpUtils.isGetReq(this)
&& route.cache() != null
&& cookies.isEmpty()
&& U.notEmpty(host())
&& !hasToken();
}
public boolean hasResponseAttached() {
return response != null;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy