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