Ultrakompakter HTTP Server
ulrich
2024-12-03 cc007e5339f7ffc35cdd9b94ce3b712596a1494e
src/de/uhilger/neon/Factory.java
@@ -23,7 +23,6 @@
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import de.uhilger.neon.JarScanner.JarScannerListener;
import de.uhilger.neon.entity.ContextDescriptor;
import de.uhilger.neon.entity.NeonDescriptor;
import de.uhilger.neon.entity.ServerDescriptor;
@@ -48,6 +47,7 @@
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
@@ -65,10 +65,13 @@
 * @author Ulrich Hilger
 * @version 1, 6.2.2024
 */
public class Factory implements JarScannerListener {
public class Factory implements ScannerListener {
  private Map<String, List<TempActor>> actorMap;
  public Factory() {
    listeners = new ArrayList<>();
    actorMap = new HashMap<>();
  }
  /**
@@ -94,21 +97,23 @@
    Gson gson = new Gson();
    return gson.fromJson(sb.toString(), NeonDescriptor.class);
  }
  public void runInstance(Class c, NeonDescriptor d)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  public void runInstance(Class c, NeonDescriptor d)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
          IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
    this.runInstance(c, d, null, new ArrayList<>());
  }
  public void runInstance(Class c, 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(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
@@ -121,109 +126,93 @@
   * @throws InvocationTargetException
   * @throws IOException
   */
  public void runInstance(Class c, 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 {
    Logger.getLogger(Factory.class.getName()).log(Level.FINER, System.getProperty("java.class.path"));
    Class pingClass = c.getClassLoader().loadClass("de.uhilger.neonbaselokal.actor.Ping");
    if(pingClass != null) {
      Logger.getLogger(Factory.class.getName()).log(Level.FINER,
              pingClass.getName() + " " + pingClass.getPackageName() +
                      " " + pingClass.getSimpleName());
    } else {
      Logger.getLogger(Factory.class.getName()).log(Level.FINER, "pingClass not found");
    }
    List serverList = d.server;
    Iterator<ServerDescriptor> serverIterator = serverList.iterator();
    while (serverIterator.hasNext()) {
      ServerDescriptor sd = serverIterator.next();
    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(c, 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(Class c, 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(c,
                    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) {
        if (cd.filter != null) {
          for (String filterClassName : cd.filter) {
            //
            Object filterObj = Class.forName(filterClassName)
                  .getDeclaredConstructor().newInstance();
            if(filterObj instanceof Filter) {
              Filter filter = (Filter) filterObj;
                    .getDeclaredConstructor().newInstance();
            if (filterObj instanceof Filter filter) {
              ctx.getFilters().add(filter);
            }
          }
@@ -235,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 {
@@ -252,7 +241,7 @@
    }
    return h;
  }
  private HttpHandler getHandlerInstance(ContextDescriptor cd) {
    try {
      Object handlerObj = Class.forName(cd.className)
@@ -263,112 +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(Class c, String packageName, Class annotation, Handler h, String contextName) {
    JarScanner js = new JarScanner();
    URI path;
    try {
      path = js.getPath(c);
      if(path.toString().endsWith(".class")) {
        ClassLoader cl = c.getClassLoader();
        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 = cl.loadClass(packageName + "."
                      + line.substring(0, line.lastIndexOf('.')));
              if (actorClass != null && actorClass.isAnnotationPresent(annotation)) {
                wire(h, actorClass, contextName);
              }
            } catch (ClassNotFoundException ex) {
              // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
            }
          } else {
            wireActors(c, packageName + "." + line, annotation, h, contextName);
          }
        }
      } else {
        ClassLoader cl = js.getUrlClassLoader(c);
        js.processZipContent(cl, new File(path), packageName, this, h, contextName);
      }
      //listClasses(c, packageName);
    } catch (URISyntaxException ex) {
      Logger.getLogger(Factory.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
    }
  }
  /**
   * 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
   * 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
   *
   * 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.
   *
   *
   * 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
   * @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");
    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");
            }
          } else {
            listClasses(c, packageName + "." + line);
          } catch (ClassNotFoundException ex) {
            Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Klasse nicht gefunden");
          }
        }
        } else {
         Logger.getLogger(Factory.class.getName()).log(Level.FINER, "stream ist null");
          listClasses(c, packageName + "." + line);
        }
      }
    } 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 
@@ -377,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) {
@@ -406,6 +370,8 @@
  public void destroy() {
    this.listeners.clear();
    this.listeners = null;
    this.actorMap.clear();
    this.actorMap = null;
  }
  private void fireServerCreated(HttpServer server) {
@@ -413,7 +379,7 @@
      l.serverCreated(server);
    }
  }
  private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
    for (FactoryListener l : listeners) {
      l.handlerCreated(ctx, h);
@@ -425,7 +391,7 @@
      l.contextCreated(context);
    }
  }
  private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
    for (FactoryListener l : listeners) {
      l.authenticatorCreated(context, auth);
@@ -438,10 +404,29 @@
    }
  }
  /* -------------- JarScannerListener Implementierung --------------- */
  /* -------------- ScannerListener Implementierung --------------- */
  @Override
  public void actorFound(Class actorClass, Handler h, String contextName) {
    wire(h, actorClass, contextName);
  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);
        }
      }
    }
  }
}
}