Ultrakompakter HTTP Server
ulrich
2024-12-03 cc007e5339f7ffc35cdd9b94ce3b712596a1494e
commit | author | age
e58690 1 /*
U 2   neon - Embeddable HTTP Server based on jdk.httpserver
3   Copyright (C) 2024  Ulrich Hilger
4
5   This program is free software: you can redistribute it and/or modify
6   it under the terms of the GNU Affero General Public License as
7   published by the Free Software Foundation, either version 3 of the
8   License, or (at your option) any later version.
9
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU Affero General Public License for more details.
14
15   You should have received a copy of the GNU Affero General Public License
16   along with this program.  If not, see <https://www.gnu.org/licenses/>.
17  */
18 package de.uhilger.neon;
19
20 import com.google.gson.Gson;
21 import com.sun.net.httpserver.Authenticator;
4d253a 22 import com.sun.net.httpserver.Filter;
e58690 23 import com.sun.net.httpserver.HttpContext;
U 24 import com.sun.net.httpserver.HttpHandler;
25 import com.sun.net.httpserver.HttpServer;
26 import de.uhilger.neon.entity.ContextDescriptor;
27 import de.uhilger.neon.entity.NeonDescriptor;
28 import de.uhilger.neon.entity.ServerDescriptor;
29 import java.io.BufferedReader;
30 import java.io.File;
31 import java.io.FileReader;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.InputStreamReader;
35 import java.lang.reflect.InvocationTargetException;
36 import java.lang.reflect.Method;
37 import java.net.InetSocketAddress;
f4025a 38 import java.net.MalformedURLException;
U 39 import java.net.URI;
40 import java.net.URISyntaxException;
e58690 41 import java.util.ArrayList;
U 42 import java.util.Arrays;
43 import java.util.HashMap;
44 import java.util.Iterator;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.concurrent.Executors;
f4025a 48 import java.util.logging.Level;
U 49 import java.util.logging.Logger;
692dc7 50 import de.uhilger.neon.Scanner.ScannerListener;
e58690 51
U 52 /**
53  * Einen Neon-Server aus einer Beschreibungsdatei herstellen
54  *
55  * Die Werte aus der Beschreibungsdatei werden in die Attribute der HttpContext-Objekte geschrieben,
56  * die zu jedem Server eroeffnet werden.
57  *
58  * Die Entitaeten stehen wie folgt in Beziehung: HttpServer -1:n-> HttpContext -1:1-> HttpHandler
59  *
60  * Die Factory legt die Kontexte, Handler sowie die Verbindung zu den Actors selbsttaetig an. Alle
61  * Parameter aus 'attributes'-Elementen der Beschreibungsdatei werden als Attribute in den
62  * HttpContext uebertragen. Deshalb ist es wichtig, dass die Attributnamen eindeutig gewaehlt
63  * werden, damit sie sich nicht gegenseitig ueberschreiben.
64  *
65  * @author Ulrich Hilger
66  * @version 1, 6.2.2024
67  */
692dc7 68 public class Factory implements ScannerListener {
cc007e 69   
U 70   private Map<String, List<TempActor>> actorMap;
e58690 71
U 72   public Factory() {
73     listeners = new ArrayList<>();
cc007e 74     actorMap = new HashMap<>();
e58690 75   }
U 76
77   /**
78    * Beschreibungsdatei lesen
79    *
80    * @param file die Datei, die den Server beschreibt
81    * @return ein Objekt, das den Server beschreibt
82    * @throws IOException wenn die Datei nicht gelesen werden konnte
83    */
84   public NeonDescriptor readDescriptor(File file) throws IOException {
85     //Logger logger = Logger.getLogger(Factory.class.getName());
86     //logger.log(Level.INFO, "reading NeonDescriptor from {0}", file.getAbsolutePath());
87
88     StringBuilder sb = new StringBuilder();
89     BufferedReader r = new BufferedReader(new FileReader(file));
90     String line = r.readLine();
91     while (line != null) {
92       sb.append(line);
93       line = r.readLine();
94     }
95     r.close();
96
97     Gson gson = new Gson();
98     return gson.fromJson(sb.toString(), NeonDescriptor.class);
99   }
47e67b 100
U 101   public void runInstance(Class c, NeonDescriptor d)
102           throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
e58690 103           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
f4025a 104     this.runInstance(c, d, null, new ArrayList<>());
e58690 105   }
U 106
47e67b 107   public void runInstance(Class c, NeonDescriptor d, List<String> packageNames)
U 108           throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
e58690 109           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
f4025a 110     this.runInstance(c, d, packageNames, new ArrayList<>());
e58690 111   }
47e67b 112
e58690 113   /**
U 114    * Einen Neon-Server gemaess einem Serverbeschreibungsobjekt herstellen und starten
115    *
47e67b 116    * @param starter die Klasse, mit der Neon durch Aufruf dieser Methode gestartet wird
e58690 117    * @param d das Object mit der Serverbeschreibung
U 118    * @param packageNames Namen der Packages, aus der rekursiv vorgefundene Actors eingebaut werden
119    * sollen
120    * @param sdp die DataProvider fuer diese Neon-Instanz
121    * @throws ClassNotFoundException
122    * @throws NoSuchMethodException
123    * @throws InstantiationException
124    * @throws IllegalAccessException
125    * @throws IllegalArgumentException
126    * @throws InvocationTargetException
127    * @throws IOException
128    */
47e67b 129   public void runInstance(Class starter, NeonDescriptor d, List<String> packageNames, List<DataProvider> sdp)
U 130           throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
e58690 131           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
675190 132
U 133     Logger.getLogger(Factory.class.getName()).log(Level.FINER, System.getProperty("java.class.path"));
47e67b 134
cc007e 135     List<ServerDescriptor> serverList = d.server;
U 136     for (ServerDescriptor sd : serverList) {
e58690 137       HttpServer server = HttpServer.create(new InetSocketAddress(sd.port), 0);
U 138       fireServerCreated(server);
139
47e67b 140       if (packageNames == null) {
e58690 141         packageNames = d.actorPackages;
692dc7 142       }      
cc007e 143       
U 144       Scanner scn = new Scanner(starter, Actor.class);
145       for (String packageName : packageNames) {
146         scn.process(this, packageName, new Object[]{});
147         // ctx.getAttributes().put("serverDataProviderList", sdp);
148       }
149       
150       addContexts(d, server, sd.contexts, sdp);
e58690 151
U 152       server.setExecutor(Executors.newFixedThreadPool(10));
153       server.start();
154     }
155     fireInstanceStarted();
156   }
47e67b 157
e58690 158   private Authenticator createAuthenticator(NeonDescriptor d) {
U 159     Authenticator auth = null;
47e67b 160     if (d.authenticator != null) {
e58690 161       try {
U 162         Object authObj = Class.forName(d.authenticator.className)
163                 .getDeclaredConstructor().newInstance();
47e67b 164         if (authObj instanceof Authenticator) {
e58690 165           auth = (Authenticator) authObj;
U 166           return auth;
167         }
47e67b 168       } catch (ClassNotFoundException | NoSuchMethodException | SecurityException
U 169               | InstantiationException | IllegalAccessException | IllegalArgumentException
170               | InvocationTargetException ex) {
e58690 171         // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
U 172         return null;
47e67b 173       }
U 174     }
e58690 175     return auth;
U 176   }
177
cc007e 178   private void addContexts(NeonDescriptor d, HttpServer server, List<ContextDescriptor> contextList,
47e67b 179           List<DataProvider> sdp)
U 180           throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
e58690 181           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
cc007e 182     Map<String, HttpHandler> sharedHandlers = new HashMap<>();
e58690 183     Iterator<ContextDescriptor> contextIterator = contextList.iterator();
6c6a73 184     Authenticator auth = null;
e58690 185     while (contextIterator.hasNext()) {
U 186       ContextDescriptor cd = contextIterator.next();
187       HttpHandler h = buildHandler(cd, sharedHandlers);
188       if (h != null) {
47e67b 189         HttpContext ctx = server.createContext(cd.contextPath, h);
e58690 190         Map<String, Object> ctxAttrs = ctx.getAttributes();
U 191         /*
192           Achtung: Wenn verschiedene Elemente dasselbe Attribut 
cc007e 193           deklarieren, ueberschreiben sich die Attribute gegenseitig.
e58690 194          */
47e67b 195         ctxAttrs.putAll(cd.attributes);
cc007e 196         ctxAttrs.put("serverDataProviderList", sdp);
U 197         if (h instanceof Handler handler) {
198           wire(handler, cd.attributes.get("contextName"));
47e67b 199         }
U 200         if (cd.authenticator instanceof String) {
201           if (!(auth instanceof Authenticator)) {
6c6a73 202             auth = createAuthenticator(d);
U 203           }
47e67b 204           if (auth instanceof Authenticator) {
U 205             ctx.setAuthenticator(auth);
e58690 206             ctx.getAttributes().putAll(d.authenticator.attributes);
6c6a73 207             fireAuthenticatorCreated(ctx, auth); // event umbenennen in etwas wie authAdded oder so
U 208           }
e58690 209         }
47e67b 210         if (cd.filter != null) {
U 211           for (String filterClassName : cd.filter) {
821908 212             //
U 213             Object filterObj = Class.forName(filterClassName)
47e67b 214                     .getDeclaredConstructor().newInstance();
cc007e 215             if (filterObj instanceof Filter filter) {
821908 216               ctx.getFilters().add(filter);
U 217             }
4d253a 218           }
U 219         }
e58690 220         fireHandlerCreated(ctx, h);
U 221         fireContextCreated(ctx);
222       } else {
223         // Handler konnte nicht erstellt werden
224       }
225     }
226   }
47e67b 227
U 228   private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
e58690 229           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
47e67b 230     HttpHandler h;
e58690 231     if (!cd.sharedHandler) {
U 232       h = getHandlerInstance(cd);
233     } else {
234       HttpHandler sharedHandler = sharedHandlers.get(cd.attributes.get("contextName"));
235       if (sharedHandler instanceof HttpHandler) {
236         h = sharedHandler;
237       } else {
238         h = getHandlerInstance(cd);
239         sharedHandlers.put(cd.attributes.get("contextName"), h);
240       }
241     }
242     return h;
243   }
47e67b 244
e58690 245   private HttpHandler getHandlerInstance(ContextDescriptor cd) {
U 246     try {
247       Object handlerObj = Class.forName(cd.className)
248               .getDeclaredConstructor().newInstance();
249       if (handlerObj instanceof HttpHandler) {
250         return (HttpHandler) handlerObj;
251       } else {
252         // kein HttpHandler aus newInstance
253         return null;
254       }
47e67b 255     } catch (ClassNotFoundException | NoSuchMethodException | SecurityException
U 256             | InstantiationException | IllegalAccessException | IllegalArgumentException
257             | InvocationTargetException ex) {
e58690 258       // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
U 259       return null;
260     }
261   }
47e67b 262
675190 263   /**
47e67b 264    * Diese Testmethode zeigt, dass die Methode getResourceAsStream nicht funktioniert wie
U 265    * dokumentiert.
266    *
267    * 1. Sie liefert den Inhalt einer gegebenen Package mitsamt Unterpackages als Stream, wenn sie
268    * auf eine Packagestruktur angewendet wird, die unverpackt in einem Verzeichnis des Dateisystems
269    * liegt.
270    *
271    * 2. Sie liefert - faelschlicherweise - null bzw. einen leeren Stream, wenn die Packagestruktur
675190 272    * in einem Jar verpackt ist.
47e67b 273    *
U 274    * Saemtliche Versuche, ueber den ClassPath oder die Pfadangabe der Package das Verhalten zu
275    * aendern, gehen bislang fehl (z.B. / oder . als Separator, / oder . zu Beginn enthalten oder
276    * nicht, realative oder absolute packagepfadangabe). Es ist auch unerheblich, ob
675190 277    * Class.getResourceAsStream oder Class.getClassLoader().getResourceAsStream verwendet wird.
47e67b 278    *
U 279    * Unabhaengig davon, ob und wie letztlich im Fall 2. oben die Methode getResourceAsStream dazu zu
280    * bringen waere, eine Inhaltsliste fuer eine Package zu liefern ist allein die Tatsache, dass
281    * sich die Methode unterschiedlich verhaelt bereits ein schwerer Bug.
282    *
675190 283    * @param c
47e67b 284    * @param packageName
675190 285    */
U 286   private void listClasses(Class c, String packageName) {
47e67b 287     Logger.getLogger(Factory.class.getName()).log(Level.FINER, "packageName: " + packageName);
U 288     //ClassLoader cl = c.getClassLoader();
289     String newPackageName = packageName.replaceAll("[.]", "/");
290     Logger.getLogger(Factory.class.getName()).log(Level.FINER, "newPackageName: " + newPackageName);
291     InputStream stream = c
292             .getResourceAsStream(newPackageName);
293     if (stream != null) {
294       BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
295       Iterator i = reader.lines().iterator();
296       Logger.getLogger(Factory.class.getName()).log(Level.FINER, Long.toString(reader.lines().count()));
297       while (i.hasNext()) {
298         String line = i.next().toString();
299         Logger.getLogger(Factory.class.getName()).log(Level.FINER, "class to inspect: " + line);
300         if (line.endsWith(".class")) {
301           try {
302             Class actorClass = c.getClassLoader().loadClass(packageName + "."
303                     + line.substring(0, line.lastIndexOf('.')));
304             if (actorClass != null && actorClass.isAnnotationPresent(Actor.class)) {
305               Logger.getLogger(Factory.class.getName()).log(Level.FINER, "ACTOR");
306             } else {
307               Logger.getLogger(Factory.class.getName()).log(Level.FINER, "no actor");
675190 308             }
47e67b 309           } catch (ClassNotFoundException ex) {
U 310             Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Klasse nicht gefunden");
675190 311           }
U 312         } else {
47e67b 313           listClasses(c, packageName + "." + line);
675190 314         }
47e67b 315       }
U 316     } else {
317       Logger.getLogger(Factory.class.getName()).log(Level.FINER, "stream ist null");
318     }
675190 319   }
47e67b 320
e58690 321   /*
U 322     Eine Action-Annotation enthaelt gewoehnlich die Route, 
323     die 'unterhalb' des Kontextpfades als 'Ausloeser' zur 
324     Ausfuehrung der Action verwendet wird.
325   
326     Wenn die Action fuer alle Routen 'unterhalb' des 
327     Kontextpfades ausgefuehrt werden soll, muss die Action 
328     als Route '/' angeben.
47e67b 329    */
cc007e 330   
U 331   /*
332    Tradeoff:
333       Es muss bei Initialisierung die Actor-Klasse ganz durchlaufen werden, um alle Methoden 
334       zu finden, die eine Action-Annotation haben. Der Handler 'merkt' sich lediglich den Namen der 
335       Actor-Klassen. Daher muessen bei jedem Aufruf eines Actors ueber den Handler abermals 
336       alle Methoden dieses Actors durchsucht werden, um diejenige Methode zu finden, die mit 
337       der zur Route des Requests passenden Action annotiert ist.
338   
339       Dieser Tradeoff bewirkt, dass nicht grosse Geflechte aus Klassen- und Methodenobjekten
340       fuer die gesamten Actors einer Anwendung im Speicher gehalten werden muessen 
341       sondern dynamisch zur Laufzeit instanziiert werden.
342   */
343  
47e67b 344   /**
cc007e 345    * Actor-Klassen dem Handler hinzufuegen
U 346    * 
47e67b 347    * @param h Handler, dem der Actor hinzugefuegt wird, falls der Kontext uebereinstimmt
cc007e 348    * @param contextName Name des Kontext, dem der Handler zugeordnet ist
47e67b 349    */
cc007e 350   private void wire(Handler h, String contextName) {
U 351     List<TempActor> actorList = actorMap.get(contextName);
352     Iterator<TempActor> i = actorList.iterator();
353     while(i.hasNext()) {
354       TempActor actor = i.next();
355       h.setActor(actor.getHttpMethod(), actor.getRoute(), actor.getActorClassName());
e58690 356     }
U 357   }
358
359   /* -------------- FactoryListener Implementierung --------------- */
360   private List<FactoryListener> listeners;
361
362   public void addListener(FactoryListener l) {
363     this.listeners.add(l);
364   }
365
366   public void removeListener(FactoryListener l) {
367     this.listeners.remove(l);
368   }
369
370   public void destroy() {
371     this.listeners.clear();
372     this.listeners = null;
cc007e 373     this.actorMap.clear();
U 374     this.actorMap = null;
e58690 375   }
U 376
377   private void fireServerCreated(HttpServer server) {
378     for (FactoryListener l : listeners) {
379       l.serverCreated(server);
380     }
381   }
47e67b 382
e58690 383   private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
U 384     for (FactoryListener l : listeners) {
385       l.handlerCreated(ctx, h);
386     }
387   }
388
389   private void fireContextCreated(HttpContext context) {
390     for (FactoryListener l : listeners) {
391       l.contextCreated(context);
392     }
393   }
47e67b 394
e58690 395   private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
U 396     for (FactoryListener l : listeners) {
397       l.authenticatorCreated(context, auth);
398     }
399   }
400
401   private void fireInstanceStarted() {
402     for (FactoryListener l : listeners) {
403       l.instanceStarted();
404     }
405   }
f4025a 406
692dc7 407   /* -------------- ScannerListener Implementierung --------------- */
f4025a 408   @Override
24ad39 409   public void annotationFound(Class foundClass, Object[] params) {
cc007e 410     Method[] methods = foundClass.getMethods();
U 411     for (Method method : methods) {
412       Action action = method.getAnnotation(Action.class);
413       if (action != null) {
414         List<String> actionHandlers = Arrays.asList(action.handler());
415         for (String contextName : actionHandlers) {
416            TempActor tempActor = new TempActor();
417            tempActor.setContextName(contextName);
418            tempActor.setHttpMethod(action.type());
419            tempActor.setRoute(action.route());
420            tempActor.setActorClassName(foundClass.getName());
421            
422            List<TempActor> actorList = actorMap.get(contextName);
423            if(actorList == null) {
424              actorList = new ArrayList<>();
425            }
426            actorList.add(tempActor);
427            actorMap.put(contextName, actorList);
428         }
24ad39 429       }
U 430     }
f4025a 431   }
47e67b 432 }