Ultrakompakter HTTP Server
ulrich
2024-12-05 7189ff3d683f97870fc11ec4e14de3f23abe3c4b
src/de/uhilger/neon/Handler.java
@@ -20,7 +20,9 @@
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.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
@@ -29,15 +31,16 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
 * Objekte der Klasse Handler nehmen Objekte entgegen die die Annotationen
 * NeonActor enthalten. Deren mit NeonMethod annotierten Methoden stellt
 * der Handler via HTTP bereit.
 * Objekte der Klasse Handler nehmen Objekte entgegen die die Annotationen Actor enthalten.
 * Deren mit Action 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.
 * Wird ein Neon-Server mit der Klasse Factory erzeugt, kann mit der Verwendung dieses Handlers
 * die Factory den Server selbsttaetig erstellen, ohne zusaetzlichen Boilerplate Code, den eine
 * eigene Anwendung mitbringen muesste.
 *
 * @author Ulrich Hilger
 * @version 1, 6.2.2024
@@ -51,10 +54,14 @@
   */
  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, 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>());
  }
  /**
@@ -66,9 +73,38 @@
   * 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(route, className);
    //dispatcher.get(methodType).put(ad.route, ad);
    Object adMapObj = dispatcher.get(methodType);
    if(adMapObj instanceof HashMap hashMap) {
      @SuppressWarnings("unchecked")
      HashMap<String, ActionDescriptor> map = hashMap;
      map.put(ad.route, ad);
      Logger.getLogger(Handler.class.getName()).log(Level.FINER, "ActionDescriptor route {0} className {1}", new Object[]{route, className});
    } else {
      Logger.getLogger(Handler.class.getName()).finer("ActionDescriptorMap nicht gefunden");
    }
  }
  /**
@@ -105,82 +141,136 @@
   */
  @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;
    String route = exchange
            .getRequestURI()
            .getPath()
            .substring(exchange
                    .getHttpContext()
                    .getPath()
                    .length());
    String requestMethodStr = exchange.getRequestMethod();
    Logger.getLogger(Handler.class.getName()).log(Level.FINER, "method {0} route {1}", new Object[]{requestMethodStr, route});
    Type requestMethod = Type.valueOf(requestMethodStr);
    /*
      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) {
    Object o = ((Map) md).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);
              if(!action.handlesResponse()) {
                respond(exchange, antwort);
              }
            }
    if (md instanceof Map) {
      int pos = route.lastIndexOf("/");
      Logger.getLogger(Handler.class.getName()).log(Level.FINER, "pos {0}", pos);
      Object o = ((Map) md).get(route);
      if (!(o instanceof ActionDescriptor)) {
        while (!found && (pos > -1)) {
          String routeRest = route.substring(0, pos);
          Logger.getLogger(Handler.class.getName()).log(Level.FINER, "pos {0} routeRest {1}", new Object[]{pos, routeRest});
          o = ((Map) md).get(routeRest);
          if (o instanceof ActionDescriptor) {
            found = true;
            handleRequest(exchange, o, routeRest, route.substring(routeRest.length()), requestMethod);
          }
          pos = routeRest.lastIndexOf("/");
        }
      } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
        // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
      } else {
        found = true;
        handleRequest(exchange, o, route, route, requestMethod);
      }
    }
      if (!found) {
        Logger.getLogger(Handler.class.getName()).log(Level.FINER, "{0} not found ", route);
        o = dispatcher.get(requestMethod).get("/");
        if (o instanceof ActionDescriptor) {
          handleRequest(exchange, o, route, route, requestMethod);
        } else {
          // kein ActionDescriptor für '/'
          Logger.getLogger(Handler.class.getName()).log(Level.FINER, "Kein Actiondescriptor fuer '/'");
        }
      }
    } else {
      // keine Actions fuer HTTP Methode
      Logger.getLogger(Handler.class.getName()).log(Level.FINER, "Kein Actions fuer HTTP-Methode {0}", requestMethodStr);
    }
  }
  private Object[] getActionArgs(HttpExchange exchange, Method method/*, Map queryParams*/) {
  private void handleRequest(HttpExchange exchange, Object o, String route, String subroute, Type requestMethod) throws IOException {
    Logger.getLogger(Handler.class.getName()).log(Level.FINER, "Handle Request route {0} subroute {1}", new Object[]{route, subroute});
    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);
            @SuppressWarnings("unchecked")
            Object conObj = actorClass.getDeclaredConstructor();
            if(conObj instanceof Constructor) {
              Constructor con = (Constructor) conObj;
              Object actorObj;
              actorObj = con.newInstance();
              addDataProvider(exchange, actorObj);
              Object antwort = method.invoke(actorObj, actionArgs);
              if (!action.handlesResponse()) {
                respond(exchange, antwort);
              }
            } else {
              // kein Konstruktor
              Logger.getLogger(Handler.class.getName()).info("Kein Konstruktor gefunden");
            }
          }
        }
      }
    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException |
            InstantiationException | IllegalAccessException | IllegalArgumentException |
            InvocationTargetException ex) {
      // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
      Logger.getLogger(Handler.class.getName()).finer("Kein passende Actor-Klasse gefunden");
    }
    //}
  }
  private Object[] getActionArgs(HttpExchange exchange, Method method, ActionDescriptor ad, String subroute) {
    int count = method.getParameterCount();
    Parameter[] methodParams = method.getParameters();
    Object[] actionArgs = new Object[count];
    Map queryParams = new HashMap();
    String[] routeParams = subroute.split("/");
    /*
      Lesen des Body der Anfrage geht nur einmal.
      bodyLesen soll nur in getQueryMap gerufen werden, wenn
      die Liste der Parameter mehr als einen Parameter umfasst
      oder wenn es nur ein Parameter ist, der nicht
      der HttpExchange ist.
      Anderenfalls sollte erst der Actor den Body aus dem
      HttpExchange lesen und nicht hier schon der Handler.
      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.
    */
    if(count > 1 || !methodParams[0].getType().equals(HttpExchange.class)) {
    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 {
        /*
          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());
        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;
  }