/*
|
neon - Embeddable HTTP Server based on jdk.httpserver
|
Copyright (C) 2024 Ulrich Hilger
|
|
This program is free software: you can redistribute it and/or modify
|
it under the terms of the GNU Affero General Public License as
|
published by the Free Software Foundation, either version 3 of the
|
License, or (at your option) any later version.
|
|
This program is distributed in the hope that it will be useful,
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
GNU Affero General Public License for more details.
|
|
You should have received a copy of the GNU Affero General Public License
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*/
|
package de.uhilger.neon;
|
|
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpHandler;
|
import de.uhilger.neon.Action.Type;
|
import de.uhilger.neon.entity.ActionDescriptor;
|
import java.io.IOException;
|
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.Method;
|
import java.lang.reflect.Parameter;
|
import java.util.EnumMap;
|
import java.util.HashMap;
|
import java.util.Iterator;
|
import java.util.List;
|
import java.util.Map;
|
|
/**
|
* Objekte der Klasse Handler nehmen Objekte entgegen die die Annotationen NeonActor enthalten.
|
* Deren mit NeonMethod annotierten Methoden stellt der Handler via HTTP bereit.
|
*
|
* Wird ein Neon-Server mit der Klasse NeonFactory erzeugt, kann mit der Verwendung dieses Handlers
|
* die NeonFactory den Server selbsttaetig erstellen, ohne zusaetzlichen Boilerplate Code, den eine
|
* eigene Anwendung mitbringen muesste.
|
*
|
* @author Ulrich Hilger
|
* @version 1, 6.2.2024
|
*/
|
public class Handler implements HttpHandler {
|
|
private final Map<Type, Map> dispatcher;
|
|
/**
|
* Ein Objekt der Klasse Handler erzeugen
|
*/
|
public Handler() {
|
dispatcher = new EnumMap<>(Type.class);
|
//dispatcher.put(Type.GET, new HashMap<String, String>());
|
//dispatcher.put(Type.PUT, new HashMap<String, String>());
|
//dispatcher.put(Type.POST, new HashMap<String, String>());
|
//dispatcher.put(Type.DELETE, new HashMap<String, String>());
|
dispatcher.put(Type.GET, new HashMap<String, ActionDescriptor>());
|
dispatcher.put(Type.PUT, new HashMap<String, ActionDescriptor>());
|
dispatcher.put(Type.POST, new HashMap<String, ActionDescriptor>());
|
dispatcher.put(Type.DELETE, new HashMap<String, ActionDescriptor>());
|
}
|
|
/**
|
* Diesem Handler einen Actor hinzufuegen
|
*
|
* @param methodType HTTP Methode
|
* @param route die Route, ueber die der Actor aufgerufen werden soll
|
* @param className die Klasse, die die Methode enthaelt, die zur Verarbeitung der Route
|
* ausgefuehrt werden soll
|
*/
|
public void setActor(Type methodType, String route, String className) {
|
ActionDescriptor ad = new ActionDescriptor();
|
ad.className = className;
|
ad.routeParams = new HashMap<>();
|
int pos = route.indexOf("{");
|
if (pos > -1) {
|
String paramStr = route.substring(pos);
|
String[] params = paramStr
|
.replaceAll("\\{", "")
|
.replaceAll("\\}", "")
|
.split("/");
|
for (int i = 0; i < params.length; i++) {
|
ad.routeParams.put(params[i], i);
|
}
|
ad.route = route.substring(0, pos - 1);
|
} else {
|
// Map kann leer bleiben
|
ad.route = route;
|
}
|
|
//Logger.getLogger(Handler.class.getName())
|
// .log(Level.INFO, "{0} {1} {2}", new Object[]{methodType, route, className});
|
dispatcher.get(methodType).put(ad.route, ad);
|
}
|
|
/**
|
* Eine Antwort senden
|
*
|
* Diese Methode kann ueberschrieben werden, falls die Antwort vor dem Senden noch weiter
|
* bearbeiten werden soll.
|
*
|
* @param exchange das Objekt mit Informationen zu HTTP Anfrage und -Antwort
|
* @param response das Ergebnisobjekt, das als Antwort gesendet werden soll
|
* @throws IOException wenn die Antwort nicht gesendet werden kann
|
*/
|
public void respond(HttpExchange exchange, Object response) throws IOException {
|
new HttpResponder().antwortSenden(exchange, HttpResponder.SC_OK, response.toString());
|
}
|
|
/**
|
* Das Objekt abrufen, das auf die Routen verweist, die von diesem Handler beantwortet werden.
|
*
|
* eine Map<Type, Map<String, String>> GET - route1 - Klassenname GET - route2 - Klassenname PUT -
|
* route1 - Klassenname PUT - route2 - Klassenname usw.
|
*
|
* @return das Dispatcher-Objekt dieses Handlers
|
*/
|
public Map<Type, Map> getDispatcher() {
|
return dispatcher;
|
}
|
|
/**
|
* Eine HTTP-Anfrage ausfuehren
|
*
|
* @param exchange das Objekt mit Informationen zu HTTP Anfrage und -Antwort
|
* @throws IOException
|
*/
|
@Override
|
public void handle(HttpExchange exchange) throws IOException {
|
String route = exchange
|
.getRequestURI()
|
.getPath()
|
.substring(exchange
|
.getHttpContext()
|
.getPath()
|
.length());
|
|
Type requestMethod = Type.valueOf(exchange.getRequestMethod());
|
/*
|
Es wird erst geprueft, ob zu einer bestimmten Route
|
ein Actor registriert wurde. Wenn kein Actor mit dieser
|
Route existiert, wird geprueft, ob ein Actor
|
mit der Route '/' vorhanden ist.
|
*/
|
boolean found = false;
|
Object md = dispatcher.get(requestMethod);
|
if (md instanceof Map) {
|
int pos = route.lastIndexOf("/");
|
Object o = ((Map) md).get(route);
|
if (!(o instanceof ActionDescriptor)) {
|
while (!found && (pos > -1)) {
|
String routeRest = route.substring(0, pos);
|
o = ((Map) md).get(routeRest);
|
if (o instanceof ActionDescriptor) {
|
found = true;
|
handleRequest(exchange, o, routeRest, route.substring(routeRest.length()), requestMethod);
|
}
|
pos = routeRest.lastIndexOf("/");
|
}
|
} else {
|
found = true;
|
handleRequest(exchange, o, route, route, requestMethod);
|
}
|
if (!found) {
|
o = dispatcher.get(requestMethod).get("/");
|
if (o instanceof ActionDescriptor) {
|
handleRequest(exchange, o, route, route, requestMethod);
|
}
|
}
|
}
|
|
}
|
|
private void handleRequest(HttpExchange exchange, Object o, String route, String subroute, Type requestMethod) throws IOException {
|
ActionDescriptor ad = (ActionDescriptor) o;
|
String actorClassName = ad.className;
|
try {
|
Class actorClass = Class.forName(actorClassName);
|
Method[] methods = actorClass.getMethods();
|
for (Method method : methods) {
|
Action action = method.getAnnotation(Action.class);
|
if (action != null) {
|
if ((action.route().equals("/") || action.route().startsWith(route)) && action.type().equals(requestMethod)) {
|
Object[] actionArgs = getActionArgs(exchange, method, ad, subroute);
|
Object actorObj = actorClass.getDeclaredConstructor().newInstance();
|
addDataProvider(exchange, actorObj);
|
Object antwort = method.invoke(actorObj, actionArgs);
|
if (!action.handlesResponse()) {
|
respond(exchange, antwort);
|
}
|
}
|
}
|
}
|
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException |
|
InstantiationException | IllegalAccessException | IllegalArgumentException |
|
InvocationTargetException ex) {
|
// Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
|
}
|
//}
|
}
|
|
private Object[] getActionArgs(HttpExchange exchange, Method method, ActionDescriptor ad, String subroute) {
|
int count = method.getParameterCount();
|
Parameter[] methodParams = method.getParameters();
|
Object[] actionArgs = new Object[count];
|
String[] routeParams = subroute.split("/");
|
|
|
/*
|
Fall 1: Es sind mehr als ein Parameter zu uebergeben und die Route enthaelt
|
weniger Parameter als die Methode erfordert.
|
Fall 2: Die Methode erwartet Parameter und der erste Parameter ist nicht
|
vom Typ HttpExchange.
|
|
Wenn einer dieser beiden Faelle eintritt, wird alles als Parameter an die Methode
|
uebergeben, was eventuell als Teil einer Query im URL oder im Body enthalten ist.
|
Fuer Mthoden, die nicht vom Typ HTTP GET sind, kann ein Actor kann dann den Body
|
nicht mehr lesen, weil das bereits an dieser Stelle gemacht wurde.
|
*/
|
Map queryParams = new HashMap();
|
if ((count > 1 && count > routeParams.length)
|
|| (methodParams.length > 0 && !methodParams[0].getType().equals(HttpExchange.class))) {
|
queryParams = new HttpHelper().getQueryMap(exchange);
|
}
|
|
int k = 0;
|
for (Parameter methodParam : methodParams) {
|
if (methodParam.getType().equals(HttpExchange.class)) {
|
actionArgs[k] = exchange;
|
} else {
|
Integer i = ad.routeParams.getOrDefault(methodParam.getName(), -1);
|
if (i < 0) {
|
actionArgs[k] = queryParams.get(methodParam.getName());
|
} else {
|
actionArgs[k] = routeParams[i + 1];
|
}
|
}
|
++k;
|
}
|
return actionArgs;
|
}
|
|
private void addDataProvider(HttpExchange exchange, Object actorObj) {
|
if (actorObj instanceof DataConsumer) {
|
DataConsumer consumer = (DataConsumer) actorObj;
|
Object sdpListObj = exchange.getHttpContext().getAttributes().get("serverDataProviderList");
|
if (sdpListObj instanceof List) {
|
List sdpList = (List) sdpListObj;
|
Iterator i = sdpList.iterator();
|
while (i.hasNext()) {
|
Object value = i.next();
|
if (value instanceof DataProvider) {
|
consumer.addDataProvider((DataProvider) value);
|
}
|
}
|
}
|
}
|
}
|
}
|