Ultrakompakter HTTP Server
ulrich
2024-12-01 692dc7be9791f131bfb253c13363b75bc41b8467
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 {
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;
692dc7 141       }      
U 142       addContexts(new Scanner(starter, Actor.class), 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
692dc7 170   private void addContexts(Scanner scn, NeonDescriptor d, HttpServer server, List contextList, List<String> packageNames,
47e67b 171           List<DataProvider> sdp)
U 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) {
692dc7 190             wireActors(scn,
U 191                     packageName, (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")
692dc7 269   private void wireActors(Scanner scn, String packageName, /*Class annotation, */Handler h, String contextName) {
U 270     if (!scn.isJar()) {
271       scn.processClasses(this, packageName, h, contextName);
47e67b 272     } else {
692dc7 273       scn.processZipContent(packageName, this, h, contextName);
e58690 274     }
U 275   }
47e67b 276
675190 277   /**
47e67b 278    * Diese Testmethode zeigt, dass die Methode getResourceAsStream nicht funktioniert wie
U 279    * dokumentiert.
280    *
281    * 1. Sie liefert den Inhalt einer gegebenen Package mitsamt Unterpackages als Stream, wenn sie
282    * auf eine Packagestruktur angewendet wird, die unverpackt in einem Verzeichnis des Dateisystems
283    * liegt.
284    *
285    * 2. Sie liefert - faelschlicherweise - null bzw. einen leeren Stream, wenn die Packagestruktur
675190 286    * in einem Jar verpackt ist.
47e67b 287    *
U 288    * Saemtliche Versuche, ueber den ClassPath oder die Pfadangabe der Package das Verhalten zu
289    * aendern, gehen bislang fehl (z.B. / oder . als Separator, / oder . zu Beginn enthalten oder
290    * nicht, realative oder absolute packagepfadangabe). Es ist auch unerheblich, ob
675190 291    * Class.getResourceAsStream oder Class.getClassLoader().getResourceAsStream verwendet wird.
47e67b 292    *
U 293    * Unabhaengig davon, ob und wie letztlich im Fall 2. oben die Methode getResourceAsStream dazu zu
294    * bringen waere, eine Inhaltsliste fuer eine Package zu liefern ist allein die Tatsache, dass
295    * sich die Methode unterschiedlich verhaelt bereits ein schwerer Bug.
296    *
675190 297    * @param c
47e67b 298    * @param packageName
675190 299    */
U 300   private void listClasses(Class c, String packageName) {
47e67b 301     Logger.getLogger(Factory.class.getName()).log(Level.FINER, "packageName: " + packageName);
U 302     //ClassLoader cl = c.getClassLoader();
303     String newPackageName = packageName.replaceAll("[.]", "/");
304     Logger.getLogger(Factory.class.getName()).log(Level.FINER, "newPackageName: " + newPackageName);
305     InputStream stream = c
306             .getResourceAsStream(newPackageName);
307     if (stream != null) {
308       BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
309       Iterator i = reader.lines().iterator();
310       Logger.getLogger(Factory.class.getName()).log(Level.FINER, Long.toString(reader.lines().count()));
311       while (i.hasNext()) {
312         String line = i.next().toString();
313         Logger.getLogger(Factory.class.getName()).log(Level.FINER, "class to inspect: " + line);
314         if (line.endsWith(".class")) {
315           try {
316             Class actorClass = c.getClassLoader().loadClass(packageName + "."
317                     + line.substring(0, line.lastIndexOf('.')));
318             if (actorClass != null && actorClass.isAnnotationPresent(Actor.class)) {
319               Logger.getLogger(Factory.class.getName()).log(Level.FINER, "ACTOR");
320             } else {
321               Logger.getLogger(Factory.class.getName()).log(Level.FINER, "no actor");
675190 322             }
47e67b 323           } catch (ClassNotFoundException ex) {
U 324             Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Klasse nicht gefunden");
675190 325           }
U 326         } else {
47e67b 327           listClasses(c, packageName + "." + line);
675190 328         }
47e67b 329       }
U 330     } else {
331       Logger.getLogger(Factory.class.getName()).log(Level.FINER, "stream ist null");
332     }
675190 333   }
47e67b 334
e58690 335   /*
U 336     Eine Action-Annotation enthaelt gewoehnlich die Route, 
337     die 'unterhalb' des Kontextpfades als 'Ausloeser' zur 
338     Ausfuehrung der Action verwendet wird.
339   
340     Wenn die Action fuer alle Routen 'unterhalb' des 
341     Kontextpfades ausgefuehrt werden soll, muss die Action 
342     als Route '/' angeben.
47e67b 343    */
U 344   /**
345    *
346    * @param h Handler, dem der Actor hinzugefuegt wird, falls der Kontext uebereinstimmt
347    * @param c hinzuzufuegende Actor-Klasse
348    * @param contextName Name des Kontext, dem der Actor hinzugefuegt wird
349    */
e58690 350   private void wire(Handler h, Class c, String contextName) {
U 351     Method[] methods = c.getMethods();
352     for (Method method : methods) {
353       Action action = method.getAnnotation(Action.class);
354       if (action != null) {
355         List actionHandlers = Arrays.asList(action.handler());
356         if (actionHandlers.contains(contextName)) {
357           h.setActor(action.type(), action.route(), c.getName());
358         }
359       }
360     }
361   }
362
363   /* -------------- FactoryListener Implementierung --------------- */
364   private List<FactoryListener> listeners;
365
366   public void addListener(FactoryListener l) {
367     this.listeners.add(l);
368   }
369
370   public void removeListener(FactoryListener l) {
371     this.listeners.remove(l);
372   }
373
374   public void destroy() {
375     this.listeners.clear();
376     this.listeners = null;
377   }
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
47e67b 411   public void annotationFound(Class foundClass, Handler h, String contextName) {
U 412     wire(h, foundClass, contextName);
f4025a 413   }
47e67b 414 }