/* 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 { public Factory() { listeners = new ArrayList<>(); } /** * 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; Iterator serverIterator = serverList.iterator(); while (serverIterator.hasNext()) { ServerDescriptor sd = serverIterator.next(); HttpServer server = HttpServer.create(new InetSocketAddress(sd.port), 0); fireServerCreated(server); if (packageNames == null) { packageNames = d.actorPackages; } addContexts(new Scanner(starter, Actor.class), d, server, sd.contexts, packageNames, 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(Scanner scn, NeonDescriptor d, HttpServer server, List contextList, List packageNames, 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 sie sich die Attribute gegenseitig. */ ctxAttrs.putAll(cd.attributes); if (h instanceof Handler) { for (String packageName : packageNames) { scn.process(this, packageName, (Handler) h, cd.attributes.get("contextName")); ctx.getAttributes().put("serverDataProviderList", sdp); } } 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 } } //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 filter = (Filter) filterObj; 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. */ /** * * @param h Handler, dem der Actor hinzugefuegt wird, falls der Kontext uebereinstimmt * @param c hinzuzufuegende Actor-Klasse * @param contextName Name des Kontext, dem der Actor hinzugefuegt wird */ 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()); } } } } /* -------------- 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; } 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) { Handler h = null; String contextName = null; for(Object param : params) { if(param instanceof Handler) { h = (Handler) param; } else if(param instanceof String) { contextName = (String) param; } } if(h == null || contextName == null) { Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Handler oder contextName ist null"); } else { wire(h, foundClass, contextName); } } }