/* 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 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 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()); 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) { 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> 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()); /* 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())); } pos = routeRest.lastIndexOf("/"); } } else { found = true; handleRequest(exchange, o, route, route); } if (!found) { o = dispatcher.get(requestMethod).get("/"); if (o instanceof ActionDescriptor) { handleRequest(exchange, o, route, route); } } } } private void handleRequest(HttpExchange exchange, Object o, String route, String subroute) 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)) { 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("/"); 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); } } } } } }