Ultrakompakter HTTP Server
ulrich
2024-12-01 40f0b0d27ec38c97fadb4c64dba6855c6ee2e904
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) {
1f6776 190             scn.process(this, packageName, (Handler) h, cd.attributes.get("contextName"));
47e67b 191             ctx.getAttributes().put("serverDataProviderList", sdp);
e58690 192           }
47e67b 193         }
U 194         if (cd.authenticator instanceof String) {
195           if (!(auth instanceof Authenticator)) {
6c6a73 196             auth = createAuthenticator(d);
U 197           }
47e67b 198           if (auth instanceof Authenticator) {
U 199             ctx.setAuthenticator(auth);
e58690 200             ctx.getAttributes().putAll(d.authenticator.attributes);
6c6a73 201             fireAuthenticatorCreated(ctx, auth); // event umbenennen in etwas wie authAdded oder so
U 202           }
47e67b 203
e58690 204         }
47e67b 205
6c6a73 206         //Authenticator auth = createAuthenticator(d);
U 207         //if (auth instanceof Authenticator && cd.authenticator instanceof String) {
208         //    ctx.setAuthenticator(auth);      
209         //    ctx.getAttributes().putAll(d.authenticator.attributes);
210         //    fireAuthenticatorCreated(ctx, auth);
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();
U 217             if (filterObj instanceof Filter) {
821908 218               Filter filter = (Filter) filterObj;
U 219               ctx.getFilters().add(filter);
220             }
4d253a 221           }
U 222         }
e58690 223         fireHandlerCreated(ctx, h);
U 224         fireContextCreated(ctx);
225       } else {
226         // Handler konnte nicht erstellt werden
227       }
228     }
229   }
47e67b 230
U 231   private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
e58690 232           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
47e67b 233     HttpHandler h;
e58690 234     if (!cd.sharedHandler) {
U 235       h = getHandlerInstance(cd);
236     } else {
237       HttpHandler sharedHandler = sharedHandlers.get(cd.attributes.get("contextName"));
238       if (sharedHandler instanceof HttpHandler) {
239         h = sharedHandler;
240       } else {
241         h = getHandlerInstance(cd);
242         sharedHandlers.put(cd.attributes.get("contextName"), h);
243       }
244     }
245     return h;
246   }
47e67b 247
e58690 248   private HttpHandler getHandlerInstance(ContextDescriptor cd) {
U 249     try {
250       Object handlerObj = Class.forName(cd.className)
251               .getDeclaredConstructor().newInstance();
252       if (handlerObj instanceof HttpHandler) {
253         return (HttpHandler) handlerObj;
254       } else {
255         // kein HttpHandler aus newInstance
256         return null;
257       }
47e67b 258     } catch (ClassNotFoundException | NoSuchMethodException | SecurityException
U 259             | InstantiationException | IllegalAccessException | IllegalArgumentException
260             | InvocationTargetException ex) {
e58690 261       // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
U 262       return null;
263     }
264   }
47e67b 265
675190 266   /**
47e67b 267    * Diese Testmethode zeigt, dass die Methode getResourceAsStream nicht funktioniert wie
U 268    * dokumentiert.
269    *
270    * 1. Sie liefert den Inhalt einer gegebenen Package mitsamt Unterpackages als Stream, wenn sie
271    * auf eine Packagestruktur angewendet wird, die unverpackt in einem Verzeichnis des Dateisystems
272    * liegt.
273    *
274    * 2. Sie liefert - faelschlicherweise - null bzw. einen leeren Stream, wenn die Packagestruktur
675190 275    * in einem Jar verpackt ist.
47e67b 276    *
U 277    * Saemtliche Versuche, ueber den ClassPath oder die Pfadangabe der Package das Verhalten zu
278    * aendern, gehen bislang fehl (z.B. / oder . als Separator, / oder . zu Beginn enthalten oder
279    * nicht, realative oder absolute packagepfadangabe). Es ist auch unerheblich, ob
675190 280    * Class.getResourceAsStream oder Class.getClassLoader().getResourceAsStream verwendet wird.
47e67b 281    *
U 282    * Unabhaengig davon, ob und wie letztlich im Fall 2. oben die Methode getResourceAsStream dazu zu
283    * bringen waere, eine Inhaltsliste fuer eine Package zu liefern ist allein die Tatsache, dass
284    * sich die Methode unterschiedlich verhaelt bereits ein schwerer Bug.
285    *
675190 286    * @param c
47e67b 287    * @param packageName
675190 288    */
U 289   private void listClasses(Class c, String packageName) {
47e67b 290     Logger.getLogger(Factory.class.getName()).log(Level.FINER, "packageName: " + packageName);
U 291     //ClassLoader cl = c.getClassLoader();
292     String newPackageName = packageName.replaceAll("[.]", "/");
293     Logger.getLogger(Factory.class.getName()).log(Level.FINER, "newPackageName: " + newPackageName);
294     InputStream stream = c
295             .getResourceAsStream(newPackageName);
296     if (stream != null) {
297       BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
298       Iterator i = reader.lines().iterator();
299       Logger.getLogger(Factory.class.getName()).log(Level.FINER, Long.toString(reader.lines().count()));
300       while (i.hasNext()) {
301         String line = i.next().toString();
302         Logger.getLogger(Factory.class.getName()).log(Level.FINER, "class to inspect: " + line);
303         if (line.endsWith(".class")) {
304           try {
305             Class actorClass = c.getClassLoader().loadClass(packageName + "."
306                     + line.substring(0, line.lastIndexOf('.')));
307             if (actorClass != null && actorClass.isAnnotationPresent(Actor.class)) {
308               Logger.getLogger(Factory.class.getName()).log(Level.FINER, "ACTOR");
309             } else {
310               Logger.getLogger(Factory.class.getName()).log(Level.FINER, "no actor");
675190 311             }
47e67b 312           } catch (ClassNotFoundException ex) {
U 313             Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Klasse nicht gefunden");
675190 314           }
U 315         } else {
47e67b 316           listClasses(c, packageName + "." + line);
675190 317         }
47e67b 318       }
U 319     } else {
320       Logger.getLogger(Factory.class.getName()).log(Level.FINER, "stream ist null");
321     }
675190 322   }
47e67b 323
e58690 324   /*
U 325     Eine Action-Annotation enthaelt gewoehnlich die Route, 
326     die 'unterhalb' des Kontextpfades als 'Ausloeser' zur 
327     Ausfuehrung der Action verwendet wird.
328   
329     Wenn die Action fuer alle Routen 'unterhalb' des 
330     Kontextpfades ausgefuehrt werden soll, muss die Action 
331     als Route '/' angeben.
47e67b 332    */
U 333   /**
334    *
335    * @param h Handler, dem der Actor hinzugefuegt wird, falls der Kontext uebereinstimmt
336    * @param c hinzuzufuegende Actor-Klasse
337    * @param contextName Name des Kontext, dem der Actor hinzugefuegt wird
338    */
e58690 339   private void wire(Handler h, Class c, String contextName) {
U 340     Method[] methods = c.getMethods();
341     for (Method method : methods) {
342       Action action = method.getAnnotation(Action.class);
343       if (action != null) {
344         List actionHandlers = Arrays.asList(action.handler());
345         if (actionHandlers.contains(contextName)) {
346           h.setActor(action.type(), action.route(), c.getName());
347         }
348       }
349     }
350   }
351
352   /* -------------- FactoryListener Implementierung --------------- */
353   private List<FactoryListener> listeners;
354
355   public void addListener(FactoryListener l) {
356     this.listeners.add(l);
357   }
358
359   public void removeListener(FactoryListener l) {
360     this.listeners.remove(l);
361   }
362
363   public void destroy() {
364     this.listeners.clear();
365     this.listeners = null;
366   }
367
368   private void fireServerCreated(HttpServer server) {
369     for (FactoryListener l : listeners) {
370       l.serverCreated(server);
371     }
372   }
47e67b 373
e58690 374   private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
U 375     for (FactoryListener l : listeners) {
376       l.handlerCreated(ctx, h);
377     }
378   }
379
380   private void fireContextCreated(HttpContext context) {
381     for (FactoryListener l : listeners) {
382       l.contextCreated(context);
383     }
384   }
47e67b 385
e58690 386   private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
U 387     for (FactoryListener l : listeners) {
388       l.authenticatorCreated(context, auth);
389     }
390   }
391
392   private void fireInstanceStarted() {
393     for (FactoryListener l : listeners) {
394       l.instanceStarted();
395     }
396   }
f4025a 397
692dc7 398   /* -------------- ScannerListener Implementierung --------------- */
f4025a 399   @Override
47e67b 400   public void annotationFound(Class foundClass, Handler h, String contextName) {
U 401     wire(h, foundClass, contextName);
f4025a 402   }
47e67b 403 }