Ultrakompakter HTTP Server
ulrich
2024-02-23 821908f431da5815b0ca1d0c39f5f30c4c3ce0f3
src/de/uhilger/neon/Handler.java
@@ -20,6 +20,7 @@
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;
@@ -31,13 +32,12 @@
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.
 * 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.
 * 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
@@ -51,10 +51,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 +70,28 @@
   * 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);
  }
  /**
@@ -105,82 +128,115 @@
   */
  @Override
  public void handle(HttpExchange exchange) throws IOException {
    HttpHelper hh = new HttpHelper();
    String route = hh.getRouteString(exchange);
    String route = exchange
            .getRequestURI()
            .getPath()
            .substring(exchange
                    .getHttpContext()
                    .getPath()
                    .length());
    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.
     */
    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("/");
      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("/");
        }
      } 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);
      }
    }
      if (!found) {
        o = dispatcher.get(requestMethod).get("/");
        if (o instanceof ActionDescriptor) {
          handleRequest(exchange, o, route, route);
        }
      }
    }
  }
  private Object[] getActionArgs(HttpExchange exchange, Method method/*, Map queryParams*/) {
  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];
    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;
  }