/* 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.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; import de.uhilger.neon.entity.ContextDescriptor; import de.uhilger.neon.entity.NeonDescriptor; import de.uhilger.neon.entity.ServerDescriptor; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; 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; import java.util.Iterator; 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 * * Die Werte aus der Beschreibungsdatei werden in die Attribute der HttpContext-Objekte geschrieben, * die zu jedem Server eroeffnet werden. * * Die Entitaeten stehen wie folgt in Beziehung: HttpServer -1:n-> HttpContext -1:1-> HttpHandler * * Die Factory legt die Kontexte, Handler sowie die Verbindung zu den Actors selbsttaetig an. Alle * Parameter aus 'attributes'-Elementen der Beschreibungsdatei werden als Attribute in den * HttpContext uebertragen. Deshalb ist es wichtig, dass die Attributnamen eindeutig gewaehlt * werden, damit sie sich nicht gegenseitig ueberschreiben. * * @author Ulrich Hilger * @version 1, 6.2.2024 */ public class Factory implements ScannerListener { private Map> actorMap; public Factory() { listeners = new ArrayList<>(); actorMap = new HashMap<>(); } /** * Beschreibungsdatei lesen * * @param file die Datei, die den Server beschreibt * @return ein Objekt, das den Server beschreibt * @throws IOException wenn die Datei nicht gelesen werden konnte */ public NeonDescriptor readDescriptor(File file) throws IOException { //Logger logger = Logger.getLogger(Factory.class.getName()); //logger.log(Level.INFO, "reading NeonDescriptor from {0}", file.getAbsolutePath()); StringBuilder sb = new StringBuilder(); BufferedReader r = new BufferedReader(new FileReader(file)); String line = r.readLine(); while (line != null) { sb.append(line); line = r.readLine(); } r.close(); Gson gson = new Gson(); return gson.fromJson(sb.toString(), NeonDescriptor.class); } 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 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 * @param sdp die DataProvider fuer diese Neon-Instanz * @throws ClassNotFoundException * @throws NoSuchMethodException * @throws InstantiationException * @throws IllegalAccessException * @throws IllegalArgumentException * @throws InvocationTargetException * @throws IOException */ public void runInstance(Class starter, NeonDescriptor d, List packageNames, List sdp) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException { Logger.getLogger(Factory.class.getName()).log(Level.FINER, System.getProperty("java.class.path")); List serverList = d.server; for (ServerDescriptor sd : serverList) { HttpServer server = HttpServer.create(new InetSocketAddress(sd.port), 0); fireServerCreated(server); 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, sdp); server.setExecutor(Executors.newFixedThreadPool(10)); server.start(); } fireInstanceStarted(); } private Authenticator createAuthenticator(NeonDescriptor d) { Authenticator auth = null; if (d.authenticator != null) { try { Object authObj = Class.forName(d.authenticator.className) .getDeclaredConstructor().newInstance(); if (authObj instanceof Authenticator) { auth = (Authenticator) authObj; return auth; } } 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 sdp) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException { Map sharedHandlers = new HashMap<>(); Iterator 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); Map ctxAttrs = ctx.getAttributes(); /* Achtung: Wenn verschiedene Elemente dasselbe Attribut deklarieren, ueberschreiben sich die Attribute gegenseitig. */ 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); ctx.getAttributes().putAll(d.authenticator.attributes); fireAuthenticatorCreated(ctx, auth); // event umbenennen in etwas wie authAdded oder so } } 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 { // Handler konnte nicht erstellt werden } } } private HttpHandler buildHandler(ContextDescriptor cd, Map sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException { HttpHandler h; if (!cd.sharedHandler) { h = getHandlerInstance(cd); } else { HttpHandler sharedHandler = sharedHandlers.get(cd.attributes.get("contextName")); if (sharedHandler instanceof HttpHandler) { h = sharedHandler; } else { h = getHandlerInstance(cd); sharedHandlers.put(cd.attributes.get("contextName"), h); } } return h; } private HttpHandler getHandlerInstance(ContextDescriptor cd) { try { Object handlerObj = Class.forName(cd.className) .getDeclaredConstructor().newInstance(); if (handlerObj instanceof HttpHandler) { return (HttpHandler) handlerObj; } else { // kein HttpHandler aus newInstance return null; } } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden? return null; } } /** * 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"); } } else { 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 Ausfuehrung der Action verwendet wird. 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. */ /** * 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 actorList = actorMap.get(contextName); Iterator i = actorList.iterator(); while(i.hasNext()) { TempActor actor = i.next(); h.setActor(actor.getHttpMethod(), actor.getRoute(), actor.getActorClassName()); } } /* -------------- FactoryListener Implementierung --------------- */ private List listeners; public void addListener(FactoryListener l) { this.listeners.add(l); } public void removeListener(FactoryListener l) { this.listeners.remove(l); } public void destroy() { this.listeners.clear(); this.listeners = null; this.actorMap.clear(); this.actorMap = null; } private void fireServerCreated(HttpServer server) { for (FactoryListener l : listeners) { l.serverCreated(server); } } private void fireHandlerCreated(HttpContext ctx, HttpHandler h) { for (FactoryListener l : listeners) { l.handlerCreated(ctx, h); } } private void fireContextCreated(HttpContext context) { for (FactoryListener l : listeners) { l.contextCreated(context); } } private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) { for (FactoryListener l : listeners) { l.authenticatorCreated(context, auth); } } private void fireInstanceStarted() { for (FactoryListener l : listeners) { 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 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 actorList = actorMap.get(contextName); if(actorList == null) { actorList = new ArrayList<>(); } actorList.add(tempActor); actorMap.put(contextName, actorList); } } } } }