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