All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.rapidoid.http.FastHttp 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;

import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.buffer.Buf;
import org.rapidoid.bytes.BytesUtil;
import org.rapidoid.cache.Cache;
import org.rapidoid.collection.Coll;
import org.rapidoid.config.Config;
import org.rapidoid.config.ConfigImpl;
import org.rapidoid.data.BufRange;
import org.rapidoid.data.KeyValueRanges;
import org.rapidoid.http.customize.Customization;
import org.rapidoid.http.customize.HttpResponseRenderer;
import org.rapidoid.http.handler.HttpHandler;
import org.rapidoid.http.impl.*;
import org.rapidoid.http.impl.lowlevel.HttpIO;
import org.rapidoid.http.processor.AbstractHttpProcessor;
import org.rapidoid.io.Upload;
import org.rapidoid.log.Log;
import org.rapidoid.log.LogLevel;
import org.rapidoid.net.abstracts.Channel;
import org.rapidoid.net.impl.RapidoidHelper;
import org.rapidoid.u.U;
import org.rapidoid.util.Msc;

import java.util.Collections;
import java.util.List;
import java.util.Map;


@Authors("Nikolche Mihajlovski")
@Since("4.3.0")
public class FastHttp extends AbstractHttpProcessor {

	private static final HttpParser HTTP_PARSER = new HttpParser();

	private static final String INTERNAL_SERVER_ERROR = "Internal Server Error!";

	private static final byte[] BUILT_IN_RES_PATH = "/_rapidoid/".getBytes();

	private final HttpRoutesImpl routes;

	private final Map attributes = Coll.synchronizedMap();

	public FastHttp(HttpRoutesImpl routes) {
		this(routes, new ConfigImpl());
	}

	public FastHttp(HttpRoutesImpl routes, @SuppressWarnings("unused") Config serverConfig) {
		super(null);
		this.routes = routes;
		routes.setHttp(this);
	}

	@Override
	public void onRequest(Channel channel, RapidoidHelper data) {

		Buf buf = channel.input();

		boolean isGet = data.isGet.value;
		boolean isKeepAlive = data.isKeepAlive.value;

		BufRange verb = data.verb;
		BufRange uri = data.uri;
		BufRange path = data.path;

		HttpIO.INSTANCE.removeTrailingSlash(buf, path);
		HttpIO.INSTANCE.removeTrailingSlash(buf, uri);

		String err = validateRequest(buf, verb, uri);
		if (err != null) {
			HttpIO.INSTANCE.writeBadRequest(channel);
			return;
		}

		HttpStatus status = HttpStatus.NOT_FOUND;

		Route matchingRoute = null;
		HandlerMatch match;

		if (isGet && shouldServeBuiltInResources(buf, path)) {
			match = routes.builtInResourcesHandler();
			if (match != null) {
				matchingRoute = match.getRoute();
			}

		} else {

			match = routes.findHandler(buf, isGet, verb, path);
			if (match != null) {
				matchingRoute = match.getRoute();
			}

			if (match == null && isGet) {
				match = routes.staticResourcesHandler();
				if (match != null) {
					matchingRoute = match.getRoute();
				}
			}
		}

		HttpHandler handler = match != null ? match.getHandler() : null;
		boolean noReq = (handler != null && !handler.needsParams());

		ReqImpl req = null;

		if (!noReq) {
			req = createReq(channel, isGet, isKeepAlive, data, buf, matchingRoute, match, handler);

			if (serveFromCache(req)) return;
		}

		try {
			if (handler != null) {
				status = handleIfFound(channel, isKeepAlive, handler, req);
			}

			if (status == HttpStatus.NOT_FOUND) {
				status = tryGenericHandlers(channel, isKeepAlive, req);
			}

		} catch (Throwable e) {
			if (handleError(channel, isKeepAlive, req, e)) return;
		}

		if (status == HttpStatus.NOT_FOUND) {
			handleNotFound(channel, isKeepAlive, req);
			return;
		}

		if (status != HttpStatus.ASYNC) {
			channel.closeIf(!isKeepAlive);
		}
	}

	private boolean shouldServeBuiltInResources(Buf buf, BufRange path) {
		return path.length > BUILT_IN_RES_PATH.length
			&& buf.get(path.start + 1) == '_' // quick check
			&& BytesUtil.startsWith(buf.bytes(), path, BUILT_IN_RES_PATH, true);
	}

	@Override
	public void waitToInitialize() {
		routes.waitToStabilize();
	}

	private boolean serveFromCache(ReqImpl req) {

		// if the HTTP request is not cacheable, the cache key will be null
		HTTPCacheKey cacheKey = req.cacheKey();

		Route route = req.route();

		if (route != null) {
			Cache cache = route.cache();

			if (cache != null) {
				if (cacheKey != null) {
					CachedResp resp = cache.getIfExists(cacheKey);

					if (resp != null) {
						serveCached(req, resp);
						return true;
					}

				} else {
					cache.bypass(); // notify it's not cacheable
				}
			}
		}

		return false;
	}

	private void serveCached(ReqImpl req, CachedResp resp) {
		Channel channel = req.channel();

		req.cached(true);

		HttpIO.INSTANCE.respond(HttpUtils.req(req), channel, -1, -1, resp.statusCode,
			req.isKeepAlive(), resp.contentType, new RespBodyBuffer(resp.body.duplicate()), resp.headers, null);

		channel.send().closeIf(!req.isKeepAlive());
	}

	@SuppressWarnings("unchecked")
	public ReqImpl createReq(Channel channel, boolean isGet, boolean isKeepAlive,
	                         RapidoidHelper helper, Buf buf,
	                         Route matchingRoute, HandlerMatch match, HttpHandler handler) {

		ReqImpl req;
		KeyValueRanges paramsKV = helper.params.reset();
		KeyValueRanges headersKV = helper.headersKV.reset();
		KeyValueRanges cookiesKV = helper.cookies.reset();

		HTTP_PARSER.parseParams(buf, paramsKV, helper.query);
		Map params = U.cast(paramsKV.toMap(buf, true, true, false));

		if (match != null && match.getParams() != null) {
			params.putAll(match.getParams());
		}

		HTTP_PARSER.parseHeadersIntoKV(buf, helper.headers, headersKV, cookiesKV, helper);
		Map headers = U.cast(headersKV.toMap(buf, false, false, true));
		Map cookies = U.cast(cookiesKV.toMap(buf, false, false, false));

		byte[] body;
		Map posted;
		Map> files;
		boolean pendingBodyParsing = false;

		if (!isGet && !helper.body.isEmpty()) {
			KeyValueRanges postedKV = helper.pairs3.reset();

			body = helper.body.bytes(buf);

			// parse posted body as data
			posted = U.map();
			files = U.map();

			pendingBodyParsing = !HTTP_PARSER.parsePosted(buf, headersKV, helper.body, postedKV, files, helper, posted);

			posted = Collections.synchronizedMap(posted);
			files = Collections.synchronizedMap(files);

		} else {
			posted = Collections.EMPTY_MAP;
			files = Collections.EMPTY_MAP;
			body = null;
		}

		String verb = helper.verb.str(buf);
		String uri = helper.uri.str(buf);
		String path = Msc.urlDecode(helper.path.str(buf));
		String query = Msc.urlDecodeOrKeepOriginal(helper.query.str(buf));
		String zone = null;

		MediaType contentType = HttpUtils.getDefaultContentType();

		if (handler != null) {
			contentType = handler.contentType();
			zone = handler.options().zone();
		}

		zone = U.or(zone, "main");

		params = Collections.synchronizedMap(params);
		headers = Collections.synchronizedMap(headers);
		cookies = Collections.synchronizedMap(cookies);

		req = new ReqImpl(this, channel, isKeepAlive, verb, uri, path, query, body, params, headers, cookies,
			posted, files, pendingBodyParsing, contentType, zone, matchingRoute);

		if (!attributes.isEmpty()) {
			req.attrs().putAll(attributes);
		}

		channel.setRequest(req);
		return req;
	}

	private HttpStatus handleIfFound(Channel channel, boolean isKeepAlive, HttpHandler handler, Req req) {
		try {
			return handler.handle(channel, isKeepAlive, req);
		} catch (NotFound nf) {
			return HttpStatus.NOT_FOUND;
		}
	}

	private void internalServerError(final Channel channel, final boolean isKeepAlive, final Req req) {
		MediaType contentType = req != null ? req.contentType() : HttpUtils.getDefaultContentType();

		HttpResponseRenderer jsonRenderer = Customization.of(req).jsonResponseRenderer();
		RespBody body = new RespBodyBytes(HttpUtils.responseToBytes(req, INTERNAL_SERVER_ERROR, contentType, jsonRenderer));

		HttpIO.INSTANCE.respond(HttpUtils.maybe(req), channel, -1, -1, 500, isKeepAlive, contentType, body, null, null);
	}

	private boolean handleError(Channel channel, boolean isKeepAlive, Req req, Throwable e) {
		if (req != null) {
			if (!((ReqImpl) req).isStopped()) {
				try {
					HttpIO.INSTANCE.errorAndDone(req, e, LogLevel.ERROR);
				} catch (Exception e1) {
					Log.error("HTTP error handler error!", e1);
					internalServerError(channel, isKeepAlive, req);
				}
			}
			return true;

		} else {
			Log.error("Low-level HTTP handler error!", e);
			internalServerError(channel, isKeepAlive, null);
		}

		return false;
	}

	private void handleNotFound(Channel channel, boolean isKeepAlive, Req req) {
		handleError(channel, isKeepAlive, req, new NotFound());
	}

	public Customization custom() {
		return routes.custom();
	}

	private String validateRequest(Buf input, BufRange verb, BufRange uri) {
		if (verb.isEmpty()) {
			return "HTTP verb cannot be empty!";
		}

		if (!BytesUtil.isValidURI(input.bytes(), uri)) {
			return "Invalid HTTP URI!";
		}

		return null; // OK, no error
	}

	private HttpStatus tryGenericHandlers(Channel channel, boolean isKeepAlive, ReqImpl req) {

		for (HttpHandler handler : routes.genericHandlers()) {
			HttpStatus status = handleIfFound(channel, isKeepAlive, handler, req);

			if (status != HttpStatus.NOT_FOUND) {
				return status;
			}
		}

		return HttpStatus.NOT_FOUND;
	}

	public synchronized void resetConfig() {
		routes.reset();
	}

	public void notFound(Channel ctx, boolean isKeepAlive, MediaType contentType, HttpHandler fromHandler, Req req) {
		HttpStatus status = HttpStatus.NOT_FOUND;

		List genericHandlers = routes.genericHandlers();
		int count = genericHandlers.size();

		for (int i = 0; i < count; i++) {
			HttpHandler handler = genericHandlers.get(i);
			if (handler == fromHandler) {
				// a generic handler returned "not found" -> go to the next one
				if (i < count - 1) {

					HttpHandler nextHandler = genericHandlers.get(i + 1);
					status = handleIfFound(ctx, isKeepAlive, nextHandler, req);

					break;
				}
			}
		}

		if (status == HttpStatus.NOT_FOUND) {
			handleNotFound(ctx, isKeepAlive, req);
		}
	}

	public Map attributes() {
		return attributes;
	}

	public HttpRoutesImpl routes() {
		return routes;
	}

	public boolean hasRouteOrResource(HttpVerb verb, String uri) {
		return routes.hasRouteOrResource(verb, uri);
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy