TinyHttpHandler.java
/*
* Copyright 2023 Morimekta Utils Authors
*
* 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 net.morimekta.tiny.http;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Objects.requireNonNullElseGet;
/**
* Class simplifying the HttpHandler in a similar fashion to the jakarta
* <code>HttpServlet</code> class. Just implement the matching method to
* the HTTP method you need served.
*/
public abstract class TinyHttpHandler implements HttpHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(TinyHttpHandler.class);
@Override
public void handle(HttpExchange exchange) throws IOException {
try {
exchange.getResponseHeaders().set("Server", "tiny-server");
switch (exchange.getRequestMethod()) {
case "GET":
doGet(exchange);
break;
case "POST":
doPost(exchange);
break;
case "HEAD":
doHead(exchange);
break;
case "OPTIONS":
doOptions(exchange);
break;
case "TRACE":
doTrace(exchange);
break;
case "PUT":
doPut(exchange);
break;
case "DELETE":
doDelete(exchange);
break;
default: {
LOGGER.warn("Unknown request method: {} {} HTTP/1.1",
exchange.getRequestMethod(),
exchange.getRequestURI().getPath());
exchange.sendResponseHeaders(TinyHttpStatus.SC_METHOD_NOT_ALLOWED, 0);
}
}
} catch (Exception e) {
if (exchange.getResponseCode() <= 0) {
LOGGER.warn("Exception handling: {} {} HTTP/1.1",
exchange.getRequestMethod(),
exchange.getRequestURI().getPath(),
e);
exchange.sendResponseHeaders(TinyHttpStatus.SC_INTERNAL, 0);
} else {
LOGGER.error("Exception handling: {} {} HTTP/1.1",
exchange.getRequestMethod(),
exchange.getRequestURI().getPath(),
e);
}
} finally {
exchange.close();
}
}
/**
* Handle a GET request.
*
* @param exchange The HTTP exchange.
* @throws IOException If failed to handle the request.
*/
protected void doGet(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(TinyHttpStatus.SC_METHOD_NOT_ALLOWED, 0);
}
/**
* Handle a POST request.
*
* @param exchange The HTTP exchange.
* @throws IOException If failed to handle the request.
*/
protected void doPost(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(TinyHttpStatus.SC_METHOD_NOT_ALLOWED, 0);
}
/**
* Handle a HEAD request.
*
* @param exchange The HTTP exchange.
* @throws IOException If failed to handle the request.
*/
protected void doHead(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(TinyHttpStatus.SC_METHOD_NOT_ALLOWED, -1);
}
/**
* Handle an OPTIONS request.
*
* @param exchange The HTTP exchange.
* @throws IOException If failed to handle the request.
*/
protected void doOptions(HttpExchange exchange) throws IOException {
var options = this.options.updateAndGet(old -> requireNonNullElseGet(old, this::buildOptions));
if (!options.methods.isEmpty()) {
exchange.getResponseHeaders().set(
"Allow",
String.join(", ", options.methods));
}
if (!options.corsMethods.isEmpty()) {
exchange.getResponseHeaders().set(
"Access-Control-Allow-Methods",
String.join(", ", options.corsMethods));
}
if (options.methods.isEmpty() && options.corsMethods.isEmpty()) {
// ... nothing implemented?
exchange.sendResponseHeaders(TinyHttpStatus.SC_INTERNAL, 0);
} else {
exchange.sendResponseHeaders(TinyHttpStatus.SC_NO_CONTENT, -1);
}
exchange.getResponseBody().close();
}
/**
* Handle a TRACE request.
*
* @param exchange The HTTP exchange.
* @throws IOException If failed to handle the request.
*/
protected void doTrace(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(TinyHttpStatus.SC_METHOD_NOT_ALLOWED, 0);
}
/**
* Handle a PUT request.
*
* @param exchange The HTTP exchange.
* @throws IOException If failed to handle the request.
*/
protected void doPut(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(TinyHttpStatus.SC_METHOD_NOT_ALLOWED, 0);
}
/**
* Handle a DELETE request.
*
* @param exchange The HTTP exchange.
* @throws IOException If failed to handle the request.
*/
protected void doDelete(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(TinyHttpStatus.SC_METHOD_NOT_ALLOWED, 0);
}
private static class Options {
private final Set<String> methods;
private final Set<String> corsMethods;
private Options(Set<String> methods, Set<String> corsMethods) {
this.methods = Collections.unmodifiableSet(methods);
this.corsMethods = Collections.unmodifiableSet(corsMethods);
}
}
private final AtomicReference<Options> options = new AtomicReference<>();
private Options buildOptions() {
Set<String> methods = new TreeSet<>();
Set<String> corsMethods = new TreeSet<>();
for (Method method : getClass().getDeclaredMethods()) {
if (method.getParameterCount() != 1 &&
method.getParameters()[0].getType().equals(HttpExchange.class)) {
continue;
}
switch (method.getName()) {
case "doGet":
methods.add("GET");
if (!method.isAnnotationPresent(TinyCorsMethod.class) ||
method.getAnnotation(TinyCorsMethod.class).cors()) {
corsMethods.add("GET");
}
break;
case "doPost":
methods.add("POST");
if (method.isAnnotationPresent(TinyCorsMethod.class) &&
method.getAnnotation(TinyCorsMethod.class).cors()) {
corsMethods.add("POST");
}
break;
case "doHead":
methods.add("HEAD");
if (method.isAnnotationPresent(TinyCorsMethod.class) &&
method.getAnnotation(TinyCorsMethod.class).cors()) {
corsMethods.add("HEAD");
}
break;
case "doTrace":
methods.add("TRACE");
if (method.isAnnotationPresent(TinyCorsMethod.class) &&
method.getAnnotation(TinyCorsMethod.class).cors()) {
corsMethods.add("TRACE");
}
break;
case "doPut":
methods.add("PUT");
if (method.isAnnotationPresent(TinyCorsMethod.class) &&
method.getAnnotation(TinyCorsMethod.class).cors()) {
corsMethods.add("PUT");
}
break;
case "doDelete":
methods.add("DELETE");
if (method.isAnnotationPresent(TinyCorsMethod.class) &&
method.getAnnotation(TinyCorsMethod.class).cors()) {
corsMethods.add("DELETE");
}
break;
default:
break;
}
}
return new Options(methods, corsMethods);
}
}