/*
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 .
*/
package de.uhilger.neon;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import de.uhilger.neon.Action.Type;
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 dispatcher;
/**
* Ein Objekt der Klasse Handler erzeugen
*/
public Handler() {
dispatcher = new EnumMap<>(Type.class);
dispatcher.put(Type.GET, new HashMap());
dispatcher.put(Type.PUT, new HashMap());
dispatcher.put(Type.POST, new HashMap());
dispatcher.put(Type.DELETE, new HashMap());
}
/**
* 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) {
//Logger.getLogger(Handler.class.getName())
// .log(Level.INFO, "{0} {1} {2}", new Object[]{methodType, route, className});
dispatcher.get(methodType).put(route, className);
}
/**
* 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> GET - route1 - Klassenname GET - route2 - Klassenname PUT -
* route1 - Klassenname PUT - route2 - Klassenname usw.
*
* @return das Dispatcher-Objekt dieses Handlers
*/
public 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 {
HttpHelper hh = new HttpHelper();
String route = hh.getRouteString(exchange);
Type requestMethod = Type.valueOf(exchange.getRequestMethod());
Map queryParams = hh.getQueryMap(exchange);
Object o;
/*
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.
*/
o = dispatcher.get(requestMethod).get(route);
if (!(o instanceof String)) {
o = dispatcher.get(requestMethod).get("/");
}
if (o instanceof String) {
String actorClassName = (String) o;
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().equals(route)) {
Object[] actionArgs = getActionArgs(exchange, method, queryParams);
Object actorObj = actorClass.getDeclaredConstructor().newInstance();
addDataProvider(exchange, actorObj);
Object antwort = method.invoke(actorObj, actionArgs);
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, Map queryParams) {
Object[] actionArgs = new Object[method.getParameterCount()];
int k = 0;
Parameter[] methodParams = method.getParameters();
for (Parameter methodParam : methodParams) {
if (methodParam.getType().equals(HttpExchange.class)) {
actionArgs[k] = exchange;
} else {
/*
Konvention: Aktor-Parameter sind immer vom Typ String
und Parametername der Methode ist gleich dem Namen in der Query
*/
actionArgs[k++] = queryParams.get(methodParam.getName());
}
}
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);
}
}
}
}
}
}