Ultrakompakter HTTP Server
ulrich
2024-12-03 cc007e5339f7ffc35cdd9b94ce3b712596a1494e
src/de/uhilger/neon/Factory.java
@@ -19,6 +19,7 @@
import com.google.gson.Gson;
import com.sun.net.httpserver.Authenticator;
import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
@@ -34,6 +35,9 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -41,6 +45,9 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.uhilger.neon.Scanner.ScannerListener;
/**
 * Einen Neon-Server aus einer Beschreibungsdatei herstellen
@@ -58,10 +65,13 @@
 * @author Ulrich Hilger
 * @version 1, 6.2.2024
 */
public class Factory {
public class Factory implements ScannerListener {
  private Map<String, List<TempActor>> actorMap;
  public Factory() {
    listeners = new ArrayList<>();
    actorMap = new HashMap<>();
  }
  /**
@@ -87,21 +97,23 @@
    Gson gson = new Gson();
    return gson.fromJson(sb.toString(), NeonDescriptor.class);
  }
  public void runInstance(NeonDescriptor d)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  public void runInstance(Class c, NeonDescriptor d)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
          IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
    this.runInstance(d, null, new ArrayList<>());
    this.runInstance(c, d, null, new ArrayList<>());
  }
  public void runInstance(NeonDescriptor d, List<String> packageNames)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  public void runInstance(Class c, NeonDescriptor d, List<String> packageNames)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
          IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
    this.runInstance(d, packageNames, new ArrayList<>());
    this.runInstance(c, d, packageNames, new ArrayList<>());
  }
  /**
   * Einen Neon-Server gemaess einem Serverbeschreibungsobjekt herstellen und starten
   *
   * @param starter die Klasse, mit der Neon durch Aufruf dieser Methode gestartet wird
   * @param d das Object mit der Serverbeschreibung
   * @param packageNames Namen der Packages, aus der rekursiv vorgefundene Actors eingebaut werden
   * sollen
@@ -114,91 +126,97 @@
   * @throws InvocationTargetException
   * @throws IOException
   */
  public void runInstance(NeonDescriptor d, List<String> packageNames, List<DataProvider> sdp)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  public void runInstance(Class starter, NeonDescriptor d, List<String> packageNames, List<DataProvider> sdp)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
          IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
    List serverList = d.server;
    Iterator<ServerDescriptor> serverIterator = serverList.iterator();
    while (serverIterator.hasNext()) {
      ServerDescriptor sd = serverIterator.next();
    Logger.getLogger(Factory.class.getName()).log(Level.FINER, System.getProperty("java.class.path"));
    List<ServerDescriptor> serverList = d.server;
    for (ServerDescriptor sd : serverList) {
      HttpServer server = HttpServer.create(new InetSocketAddress(sd.port), 0);
      fireServerCreated(server);
      if(packageNames == null) {
      if (packageNames == null) {
        packageNames = d.actorPackages;
      }
      Scanner scn = new Scanner(starter, Actor.class);
      for (String packageName : packageNames) {
        scn.process(this, packageName, new Object[]{});
        // ctx.getAttributes().put("serverDataProviderList", sdp);
      }
      addContexts(d, server, sd.contexts, packageNames, sdp);
      addContexts(d, server, sd.contexts, sdp);
      server.setExecutor(Executors.newFixedThreadPool(10));
      server.start();
    }
    fireInstanceStarted();
  }
  private Authenticator createAuthenticator(NeonDescriptor d) {
    Authenticator auth = null;
    if(d.authenticator != null) {
    if (d.authenticator != null) {
      try {
        Object authObj = Class.forName(d.authenticator.className)
                .getDeclaredConstructor().newInstance();
        if(authObj instanceof Authenticator) {
        if (authObj instanceof Authenticator) {
          auth = (Authenticator) authObj;
          return auth;
        }
      } catch (ClassNotFoundException | NoSuchMethodException | SecurityException |
              InstantiationException | IllegalAccessException | IllegalArgumentException |
              InvocationTargetException ex) {
      } catch (ClassNotFoundException | NoSuchMethodException | SecurityException
              | InstantiationException | IllegalAccessException | IllegalArgumentException
              | InvocationTargetException ex) {
        // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
        return null;
      }
    }
      }
    }
    return auth;
  }
  private void addContexts(NeonDescriptor d, HttpServer server, List contextList, List<String> packageNames,
          List<DataProvider> sdp)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  private void addContexts(NeonDescriptor d, HttpServer server, List<ContextDescriptor> contextList,
          List<DataProvider> sdp)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
          IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
    Map<String, HttpHandler> sharedHandlers = new HashMap();
    Map<String, HttpHandler> sharedHandlers = new HashMap<>();
    Iterator<ContextDescriptor> contextIterator = contextList.iterator();
    Authenticator auth = null;
    while (contextIterator.hasNext()) {
      ContextDescriptor cd = contextIterator.next();
      HttpHandler h = buildHandler(cd, sharedHandlers);
      if (h != null) {
        HttpContext ctx = server.createContext(cd.contextPath, h);
        HttpContext ctx = server.createContext(cd.contextPath, h);
        Map<String, Object> ctxAttrs = ctx.getAttributes();
        /*
          Achtung: Wenn verschiedene Elemente dasselbe Attribut 
          deklarieren, ueberschreiben sie sich die Attribute gegenseitig.
          deklarieren, ueberschreiben sich die Attribute gegenseitig.
         */
        ctxAttrs.putAll(cd.attributes);
        if (h instanceof Handler) {
          for (String packageName : packageNames) {
            wireActors(
                    packageName, Actor.class, (Handler) h,
                    cd.attributes.get("contextName"));
              ctx.getAttributes().put("serverDataProviderList", sdp);
          }
        }
        if(cd.authenticator instanceof String) {
          if(!(auth instanceof Authenticator)) {
        ctxAttrs.putAll(cd.attributes);
        ctxAttrs.put("serverDataProviderList", sdp);
        if (h instanceof Handler handler) {
          wire(handler, cd.attributes.get("contextName"));
        }
        if (cd.authenticator instanceof String) {
          if (!(auth instanceof Authenticator)) {
            auth = createAuthenticator(d);
          }
          if(auth instanceof Authenticator) {
            ctx.setAuthenticator(auth);
          if (auth instanceof Authenticator) {
            ctx.setAuthenticator(auth);
            ctx.getAttributes().putAll(d.authenticator.attributes);
            fireAuthenticatorCreated(ctx, auth); // event umbenennen in etwas wie authAdded oder so
          }
        }
        //Authenticator auth = createAuthenticator(d);
        //if (auth instanceof Authenticator && cd.authenticator instanceof String) {
        //    ctx.setAuthenticator(auth);
        //    ctx.getAttributes().putAll(d.authenticator.attributes);
        //    fireAuthenticatorCreated(ctx, auth);
        //}
        if (cd.filter != null) {
          for (String filterClassName : cd.filter) {
            //
            Object filterObj = Class.forName(filterClassName)
                    .getDeclaredConstructor().newInstance();
            if (filterObj instanceof Filter filter) {
              ctx.getFilters().add(filter);
            }
          }
        }
        fireHandlerCreated(ctx, h);
        fireContextCreated(ctx);
      } else {
@@ -206,10 +224,10 @@
      }
    }
  }
  private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
          IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
    HttpHandler h;
    HttpHandler h;
    if (!cd.sharedHandler) {
      h = getHandlerInstance(cd);
    } else {
@@ -223,7 +241,7 @@
    }
    return h;
  }
  private HttpHandler getHandlerInstance(ContextDescriptor cd) {
    try {
      Object handlerObj = Class.forName(cd.className)
@@ -234,38 +252,72 @@
        // kein HttpHandler aus newInstance
        return null;
      }
    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException |
            InstantiationException | IllegalAccessException | IllegalArgumentException |
            InvocationTargetException ex) {
    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException
            | InstantiationException | IllegalAccessException | IllegalArgumentException
            | InvocationTargetException ex) {
      // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
      return null;
    }
  }
  private void wireActors(String packageName, Class annotation, Handler h, String contextName) {
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    InputStream stream = cl
            .getResourceAsStream(packageName.replaceAll("[.]", "/"));
    BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
    Iterator i = reader.lines().iterator();
    while (i.hasNext()) {
      String line = i.next().toString();
      if (line.endsWith(".class")) {
        try {
          Class actorClass = Class.forName(packageName + "."
                  + line.substring(0, line.lastIndexOf('.')));
          if (actorClass != null && actorClass.isAnnotationPresent(annotation)) {
            wire(h, actorClass, contextName);
  /**
   * Diese Testmethode zeigt, dass die Methode getResourceAsStream nicht funktioniert wie
   * dokumentiert.
   *
   * 1. Sie liefert den Inhalt einer gegebenen Package mitsamt Unterpackages als Stream, wenn sie
   * auf eine Packagestruktur angewendet wird, die unverpackt in einem Verzeichnis des Dateisystems
   * liegt.
   *
   * 2. Sie liefert - faelschlicherweise - null bzw. einen leeren Stream, wenn die Packagestruktur
   * in einem Jar verpackt ist.
   *
   * Saemtliche Versuche, ueber den ClassPath oder die Pfadangabe der Package das Verhalten zu
   * aendern, gehen bislang fehl (z.B. / oder . als Separator, / oder . zu Beginn enthalten oder
   * nicht, realative oder absolute packagepfadangabe). Es ist auch unerheblich, ob
   * Class.getResourceAsStream oder Class.getClassLoader().getResourceAsStream verwendet wird.
   *
   * Unabhaengig davon, ob und wie letztlich im Fall 2. oben die Methode getResourceAsStream dazu zu
   * bringen waere, eine Inhaltsliste fuer eine Package zu liefern ist allein die Tatsache, dass
   * sich die Methode unterschiedlich verhaelt bereits ein schwerer Bug.
   *
   * @param c
   * @param packageName
   */
  private void listClasses(Class c, String packageName) {
    Logger.getLogger(Factory.class.getName()).log(Level.FINER, "packageName: " + packageName);
    //ClassLoader cl = c.getClassLoader();
    String newPackageName = packageName.replaceAll("[.]", "/");
    Logger.getLogger(Factory.class.getName()).log(Level.FINER, "newPackageName: " + newPackageName);
    InputStream stream = c
            .getResourceAsStream(newPackageName);
    if (stream != null) {
      BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
      Iterator i = reader.lines().iterator();
      Logger.getLogger(Factory.class.getName()).log(Level.FINER, Long.toString(reader.lines().count()));
      while (i.hasNext()) {
        String line = i.next().toString();
        Logger.getLogger(Factory.class.getName()).log(Level.FINER, "class to inspect: " + line);
        if (line.endsWith(".class")) {
          try {
            Class actorClass = c.getClassLoader().loadClass(packageName + "."
                    + line.substring(0, line.lastIndexOf('.')));
            if (actorClass != null && actorClass.isAnnotationPresent(Actor.class)) {
              Logger.getLogger(Factory.class.getName()).log(Level.FINER, "ACTOR");
            } else {
              Logger.getLogger(Factory.class.getName()).log(Level.FINER, "no actor");
            }
          } catch (ClassNotFoundException ex) {
            Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Klasse nicht gefunden");
          }
        } catch (ClassNotFoundException ex) {
          // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
        } else {
          listClasses(c, packageName + "." + line);
        }
      } else {
        wireActors(packageName + "." + line, annotation, h, contextName);
      }
    } else {
      Logger.getLogger(Factory.class.getName()).log(Level.FINER, "stream ist null");
    }
  }
  /*
    Eine Action-Annotation enthaelt gewoehnlich die Route, 
    die 'unterhalb' des Kontextpfades als 'Ausloeser' zur 
@@ -274,22 +326,37 @@
    Wenn die Action fuer alle Routen 'unterhalb' des 
    Kontextpfades ausgefuehrt werden soll, muss die Action 
    als Route '/' angeben.
   */
  /*
   Tradeoff:
      Es muss bei Initialisierung die Actor-Klasse ganz durchlaufen werden, um alle Methoden
      zu finden, die eine Action-Annotation haben. Der Handler 'merkt' sich lediglich den Namen der
      Actor-Klassen. Daher muessen bei jedem Aufruf eines Actors ueber den Handler abermals
      alle Methoden dieses Actors durchsucht werden, um diejenige Methode zu finden, die mit
      der zur Route des Requests passenden Action annotiert ist.
      Dieser Tradeoff bewirkt, dass nicht grosse Geflechte aus Klassen- und Methodenobjekten
      fuer die gesamten Actors einer Anwendung im Speicher gehalten werden muessen
      sondern dynamisch zur Laufzeit instanziiert werden.
  */
  private void wire(Handler h, Class c, String contextName) {
    Method[] methods = c.getMethods();
    for (Method method : methods) {
      Action action = method.getAnnotation(Action.class);
      if (action != null) {
        List actionHandlers = Arrays.asList(action.handler());
        if (actionHandlers.contains(contextName)) {
          h.setActor(action.type(), action.route(), c.getName());
        }
      }
  /**
   * Actor-Klassen dem Handler hinzufuegen
   *
   * @param h Handler, dem der Actor hinzugefuegt wird, falls der Kontext uebereinstimmt
   * @param contextName Name des Kontext, dem der Handler zugeordnet ist
   */
  private void wire(Handler h, String contextName) {
    List<TempActor> actorList = actorMap.get(contextName);
    Iterator<TempActor> i = actorList.iterator();
    while(i.hasNext()) {
      TempActor actor = i.next();
      h.setActor(actor.getHttpMethod(), actor.getRoute(), actor.getActorClassName());
    }
  }
  /* -------------- FactoryListener Implementierung --------------- */
  private List<FactoryListener> listeners;
  public void addListener(FactoryListener l) {
@@ -303,6 +370,8 @@
  public void destroy() {
    this.listeners.clear();
    this.listeners = null;
    this.actorMap.clear();
    this.actorMap = null;
  }
  private void fireServerCreated(HttpServer server) {
@@ -310,7 +379,7 @@
      l.serverCreated(server);
    }
  }
  private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
    for (FactoryListener l : listeners) {
      l.handlerCreated(ctx, h);
@@ -322,7 +391,7 @@
      l.contextCreated(context);
    }
  }
  private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
    for (FactoryListener l : listeners) {
      l.authenticatorCreated(context, auth);
@@ -334,4 +403,30 @@
      l.instanceStarted();
    }
  }
}
  /* -------------- ScannerListener Implementierung --------------- */
  @Override
  public void annotationFound(Class foundClass, Object[] params) {
    Method[] methods = foundClass.getMethods();
    for (Method method : methods) {
      Action action = method.getAnnotation(Action.class);
      if (action != null) {
        List<String> actionHandlers = Arrays.asList(action.handler());
        for (String contextName : actionHandlers) {
           TempActor tempActor = new TempActor();
           tempActor.setContextName(contextName);
           tempActor.setHttpMethod(action.type());
           tempActor.setRoute(action.route());
           tempActor.setActorClassName(foundClass.getName());
           List<TempActor> actorList = actorMap.get(contextName);
           if(actorList == null) {
             actorList = new ArrayList<>();
           }
           actorList.add(tempActor);
           actorMap.put(contextName, actorList);
        }
      }
    }
  }
}