Ultrakompakter HTTP Server
ulrich
2024-11-30 f4025a39226331d8c1c4686d2d1c73e080f9f4b3
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 {
127     List serverList = d.server;
128     Iterator<ServerDescriptor> serverIterator = serverList.iterator();
129     while (serverIterator.hasNext()) {
130       ServerDescriptor sd = serverIterator.next();
131       HttpServer server = HttpServer.create(new InetSocketAddress(sd.port), 0);
132       fireServerCreated(server);
133
134       if(packageNames == null) {
135         packageNames = d.actorPackages;
136       }
f4025a 137       addContexts(c, d, server, sd.contexts, packageNames, sdp);
e58690 138
U 139       server.setExecutor(Executors.newFixedThreadPool(10));
140       server.start();
141     }
142     fireInstanceStarted();
143   }
144   
145   private Authenticator createAuthenticator(NeonDescriptor d) {
146     Authenticator auth = null;
147     if(d.authenticator != null) {
148       try {
149         Object authObj = Class.forName(d.authenticator.className)
150                 .getDeclaredConstructor().newInstance();
151         if(authObj instanceof Authenticator) {
152           auth = (Authenticator) authObj;
153           return auth;
154         }
155       } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | 
156               InstantiationException | IllegalAccessException | IllegalArgumentException | 
157               InvocationTargetException ex) {
158         // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
159         return null;
160       }      
161     }    
162     return auth;
163   }
164
f4025a 165   private void addContexts(Class c, NeonDescriptor d, HttpServer server, List contextList, List<String> packageNames, 
e58690 166           List<DataProvider> sdp) 
U 167           throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
168           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
169     Map<String, HttpHandler> sharedHandlers = new HashMap();
170     Iterator<ContextDescriptor> contextIterator = contextList.iterator();
6c6a73 171     Authenticator auth = null;
e58690 172     while (contextIterator.hasNext()) {
U 173       ContextDescriptor cd = contextIterator.next();
174       HttpHandler h = buildHandler(cd, sharedHandlers);
175       if (h != null) {
176         HttpContext ctx = server.createContext(cd.contextPath, h);        
177         Map<String, Object> ctxAttrs = ctx.getAttributes();
178         /*
179           Achtung: Wenn verschiedene Elemente dasselbe Attribut 
180           deklarieren, ueberschreiben sie sich die Attribute gegenseitig.
181          */
182         ctxAttrs.putAll(cd.attributes);        
183         if (h instanceof Handler) {         
184           for (String packageName : packageNames) {
f4025a 185             wireActors(c, 
e58690 186                     packageName, Actor.class, (Handler) h, 
U 187                     cd.attributes.get("contextName"));
188               ctx.getAttributes().put("serverDataProviderList", sdp);
189           }
190         }        
6c6a73 191         if(cd.authenticator instanceof String) {
U 192           if(!(auth instanceof Authenticator)) {
193             auth = createAuthenticator(d);
194           }
195           if(auth instanceof Authenticator) {
e58690 196             ctx.setAuthenticator(auth);      
U 197             ctx.getAttributes().putAll(d.authenticator.attributes);
6c6a73 198             fireAuthenticatorCreated(ctx, auth); // event umbenennen in etwas wie authAdded oder so
U 199           }
200           
e58690 201         }
6c6a73 202         
U 203         //Authenticator auth = createAuthenticator(d);
204         //if (auth instanceof Authenticator && cd.authenticator instanceof String) {
205         //    ctx.setAuthenticator(auth);      
206         //    ctx.getAttributes().putAll(d.authenticator.attributes);
207         //    fireAuthenticatorCreated(ctx, auth);
208         //}
821908 209         if(cd.filter != null) {
U 210           for(String filterClassName : cd.filter) {
211             //
212             Object filterObj = Class.forName(filterClassName)
213                   .getDeclaredConstructor().newInstance();
214             if(filterObj instanceof Filter) {
215               Filter filter = (Filter) filterObj;
216               ctx.getFilters().add(filter);
217             }
4d253a 218           }
U 219         }
e58690 220         fireHandlerCreated(ctx, h);
U 221         fireContextCreated(ctx);
222       } else {
223         // Handler konnte nicht erstellt werden
224       }
225     }
226   }
227   
228   private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
229           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
230     HttpHandler h;    
231     if (!cd.sharedHandler) {
232       h = getHandlerInstance(cd);
233     } else {
234       HttpHandler sharedHandler = sharedHandlers.get(cd.attributes.get("contextName"));
235       if (sharedHandler instanceof HttpHandler) {
236         h = sharedHandler;
237       } else {
238         h = getHandlerInstance(cd);
239         sharedHandlers.put(cd.attributes.get("contextName"), h);
240       }
241     }
242     return h;
243   }
244   
245   private HttpHandler getHandlerInstance(ContextDescriptor cd) {
246     try {
247       Object handlerObj = Class.forName(cd.className)
248               .getDeclaredConstructor().newInstance();
249       if (handlerObj instanceof HttpHandler) {
250         return (HttpHandler) handlerObj;
251       } else {
252         // kein HttpHandler aus newInstance
253         return null;
254       }
255     } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | 
256             InstantiationException | IllegalAccessException | IllegalArgumentException | 
257             InvocationTargetException ex) {
258       // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
259       return null;
260     }
261   }
262
f4025a 263   private void wireActors(Class c, String packageName, Class annotation, Handler h, String contextName) {
U 264     JarScanner js = new JarScanner();
265     URI path;
266     try {
267       path = js.getPath(c);
268       if(path.toString().endsWith(".class")) {
269         ClassLoader cl = c.getClassLoader();
270         InputStream stream = cl
271                 .getResourceAsStream(packageName.replaceAll("[.]", "/"));
272         BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
273         Iterator i = reader.lines().iterator();
274         while (i.hasNext()) {
275           String line = i.next().toString();
276           if (line.endsWith(".class")) {
277             try {
278               Class actorClass = cl.loadClass(packageName + "."
279                       + line.substring(0, line.lastIndexOf('.')));
280               if (actorClass != null && actorClass.isAnnotationPresent(annotation)) {
281                 wire(h, actorClass, contextName);
282               }
283             } catch (ClassNotFoundException ex) {
284               // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
285             }
286           } else {
287             wireActors(c, packageName + "." + line, annotation, h, contextName);
e58690 288           }
U 289         }
290       } else {
f4025a 291         ClassLoader cl = js.getUrlClassLoader(c);
U 292         js.processZipContent(cl, new File(path), packageName, this, h, contextName);
e58690 293       }
f4025a 294     } catch (URISyntaxException ex) {
U 295       Logger.getLogger(Factory.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
e58690 296     }
U 297   }
298   
299   /*
300     Eine Action-Annotation enthaelt gewoehnlich die Route, 
301     die 'unterhalb' des Kontextpfades als 'Ausloeser' zur 
302     Ausfuehrung der Action verwendet wird.
303   
304     Wenn die Action fuer alle Routen 'unterhalb' des 
305     Kontextpfades ausgefuehrt werden soll, muss die Action 
306     als Route '/' angeben.
307   */
308   private void wire(Handler h, Class c, String contextName) {
309     Method[] methods = c.getMethods();
310     for (Method method : methods) {
311       Action action = method.getAnnotation(Action.class);
312       if (action != null) {
313         List actionHandlers = Arrays.asList(action.handler());
314         if (actionHandlers.contains(contextName)) {
315           h.setActor(action.type(), action.route(), c.getName());
316         }
317       }
318     }
319   }
320
321   /* -------------- FactoryListener Implementierung --------------- */
322   
323   private List<FactoryListener> listeners;
324
325   public void addListener(FactoryListener l) {
326     this.listeners.add(l);
327   }
328
329   public void removeListener(FactoryListener l) {
330     this.listeners.remove(l);
331   }
332
333   public void destroy() {
334     this.listeners.clear();
335     this.listeners = null;
336   }
337
338   private void fireServerCreated(HttpServer server) {
339     for (FactoryListener l : listeners) {
340       l.serverCreated(server);
341     }
342   }
343   
344   private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
345     for (FactoryListener l : listeners) {
346       l.handlerCreated(ctx, h);
347     }
348   }
349
350   private void fireContextCreated(HttpContext context) {
351     for (FactoryListener l : listeners) {
352       l.contextCreated(context);
353     }
354   }
355   
356   private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
357     for (FactoryListener l : listeners) {
358       l.authenticatorCreated(context, auth);
359     }
360   }
361
362   private void fireInstanceStarted() {
363     for (FactoryListener l : listeners) {
364       l.instanceStarted();
365     }
366   }
f4025a 367
U 368   /* -------------- JarScannerListener Implementierung --------------- */
369     
370   @Override
371   public void actorFound(Class actorClass, Handler h, String contextName) {
372     wire(h, actorClass, contextName);
373   }
e58690 374 }