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