/* 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.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.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; /** * 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 { 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(NeonDescriptor d) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException { this.runInstance(d, null, new ArrayList<>()); } public void runInstance(NeonDescriptor d, List packageNames) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException { this.runInstance(d, packageNames, new ArrayList<>()); } /** * Einen Neon-Server gemaess einem Serverbeschreibungsobjekt herstellen und starten * * @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(NeonDescriptor d, List packageNames, List sdp) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException { 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(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(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(); 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) { wireActors( packageName, Actor.class, (Handler) h, cd.attributes.get("contextName")); ctx.getAttributes().put("serverDataProviderList", sdp); } } Authenticator auth = createAuthenticator(d); if (auth instanceof Authenticator && cd.authenticator instanceof String) { ctx.setAuthenticator(auth); ctx.getAttributes().putAll(d.authenticator.attributes); fireAuthenticatorCreated(ctx, auth); } 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; } } 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); } } catch (ClassNotFoundException ex) { // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden? } } else { wireActors(packageName + "." + line, annotation, h, contextName); } } } /* 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. */ 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(); } } }