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