Ultrakompakter HTTP Server
ulrich
2024-11-14 ca79593d7659cd9c6dcdd38692888b26dcbef4f5
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;
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.HashMap;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.concurrent.Executors;
45
46 /**
47  * Einen Neon-Server aus einer Beschreibungsdatei herstellen
48  *
49  * Die Werte aus der Beschreibungsdatei werden in die Attribute der HttpContext-Objekte geschrieben,
50  * die zu jedem Server eroeffnet werden.
51  *
52  * Die Entitaeten stehen wie folgt in Beziehung: HttpServer -1:n-> HttpContext -1:1-> HttpHandler
53  *
54  * Die Factory legt die Kontexte, Handler sowie die Verbindung zu den Actors selbsttaetig an. Alle
55  * Parameter aus 'attributes'-Elementen der Beschreibungsdatei werden als Attribute in den
56  * HttpContext uebertragen. Deshalb ist es wichtig, dass die Attributnamen eindeutig gewaehlt
57  * werden, damit sie sich nicht gegenseitig ueberschreiben.
58  *
59  * @author Ulrich Hilger
60  * @version 1, 6.2.2024
61  */
62 public class Factory {
63
64   public Factory() {
65     listeners = new ArrayList<>();
66   }
67
68   /**
69    * Beschreibungsdatei lesen
70    *
71    * @param file die Datei, die den Server beschreibt
72    * @return ein Objekt, das den Server beschreibt
73    * @throws IOException wenn die Datei nicht gelesen werden konnte
74    */
75   public NeonDescriptor readDescriptor(File file) throws IOException {
76     //Logger logger = Logger.getLogger(Factory.class.getName());
77     //logger.log(Level.INFO, "reading NeonDescriptor from {0}", file.getAbsolutePath());
78
79     StringBuilder sb = new StringBuilder();
80     BufferedReader r = new BufferedReader(new FileReader(file));
81     String line = r.readLine();
82     while (line != null) {
83       sb.append(line);
84       line = r.readLine();
85     }
86     r.close();
87
88     Gson gson = new Gson();
89     return gson.fromJson(sb.toString(), NeonDescriptor.class);
90   }
91   
92   public void runInstance(NeonDescriptor d) 
93           throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
94           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
95     this.runInstance(d, null, new ArrayList<>());
96   }
97
98   public void runInstance(NeonDescriptor d, List<String> packageNames) 
99           throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
100           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
101     this.runInstance(d, packageNames, new ArrayList<>());
102   }
103   /**
104    * Einen Neon-Server gemaess einem Serverbeschreibungsobjekt herstellen und starten
105    *
106    * @param d das Object mit der Serverbeschreibung
107    * @param packageNames Namen der Packages, aus der rekursiv vorgefundene Actors eingebaut werden
108    * sollen
109    * @param sdp die DataProvider fuer diese Neon-Instanz
110    * @throws ClassNotFoundException
111    * @throws NoSuchMethodException
112    * @throws InstantiationException
113    * @throws IllegalAccessException
114    * @throws IllegalArgumentException
115    * @throws InvocationTargetException
116    * @throws IOException
117    */
118   public void runInstance(NeonDescriptor d, List<String> packageNames, List<DataProvider> sdp) 
119           throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
120           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
121     List serverList = d.server;
122     Iterator<ServerDescriptor> serverIterator = serverList.iterator();
123     while (serverIterator.hasNext()) {
124       ServerDescriptor sd = serverIterator.next();
125       HttpServer server = HttpServer.create(new InetSocketAddress(sd.port), 0);
126       fireServerCreated(server);
127
128       if(packageNames == null) {
129         packageNames = d.actorPackages;
130       }
131       addContexts(d, server, sd.contexts, packageNames, sdp);
132
133       server.setExecutor(Executors.newFixedThreadPool(10));
134       server.start();
135     }
136     fireInstanceStarted();
137   }
138   
139   private Authenticator createAuthenticator(NeonDescriptor d) {
140     Authenticator auth = null;
141     if(d.authenticator != null) {
142       try {
143         Object authObj = Class.forName(d.authenticator.className)
144                 .getDeclaredConstructor().newInstance();
145         if(authObj instanceof Authenticator) {
146           auth = (Authenticator) authObj;
147           return auth;
148         }
149       } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | 
150               InstantiationException | IllegalAccessException | IllegalArgumentException | 
151               InvocationTargetException ex) {
152         // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
153         return null;
154       }      
155     }    
156     return auth;
157   }
158
159   private void addContexts(NeonDescriptor d, HttpServer server, List contextList, List<String> packageNames, 
160           List<DataProvider> sdp) 
161           throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
162           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
163     Map<String, HttpHandler> sharedHandlers = new HashMap();
164     Iterator<ContextDescriptor> contextIterator = contextList.iterator();
6c6a73 165     Authenticator auth = null;
e58690 166     while (contextIterator.hasNext()) {
U 167       ContextDescriptor cd = contextIterator.next();
168       HttpHandler h = buildHandler(cd, sharedHandlers);
169       if (h != null) {
170         HttpContext ctx = server.createContext(cd.contextPath, h);        
171         Map<String, Object> ctxAttrs = ctx.getAttributes();
172         /*
173           Achtung: Wenn verschiedene Elemente dasselbe Attribut 
174           deklarieren, ueberschreiben sie sich die Attribute gegenseitig.
175          */
176         ctxAttrs.putAll(cd.attributes);        
177         if (h instanceof Handler) {         
178           for (String packageName : packageNames) {
179             wireActors(
180                     packageName, Actor.class, (Handler) h, 
181                     cd.attributes.get("contextName"));
182               ctx.getAttributes().put("serverDataProviderList", sdp);
183           }
184         }        
6c6a73 185         if(cd.authenticator instanceof String) {
U 186           if(!(auth instanceof Authenticator)) {
187             auth = createAuthenticator(d);
188           }
189           if(auth instanceof Authenticator) {
e58690 190             ctx.setAuthenticator(auth);      
U 191             ctx.getAttributes().putAll(d.authenticator.attributes);
6c6a73 192             fireAuthenticatorCreated(ctx, auth); // event umbenennen in etwas wie authAdded oder so
U 193           }
194           
e58690 195         }
6c6a73 196         
U 197         //Authenticator auth = createAuthenticator(d);
198         //if (auth instanceof Authenticator && cd.authenticator instanceof String) {
199         //    ctx.setAuthenticator(auth);      
200         //    ctx.getAttributes().putAll(d.authenticator.attributes);
201         //    fireAuthenticatorCreated(ctx, auth);
202         //}
821908 203         if(cd.filter != null) {
U 204           for(String filterClassName : cd.filter) {
205             //
206             Object filterObj = Class.forName(filterClassName)
207                   .getDeclaredConstructor().newInstance();
208             if(filterObj instanceof Filter) {
209               Filter filter = (Filter) filterObj;
210               ctx.getFilters().add(filter);
211             }
4d253a 212           }
U 213         }
e58690 214         fireHandlerCreated(ctx, h);
U 215         fireContextCreated(ctx);
216       } else {
217         // Handler konnte nicht erstellt werden
218       }
219     }
220   }
221   
222   private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
223           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
224     HttpHandler h;    
225     if (!cd.sharedHandler) {
226       h = getHandlerInstance(cd);
227     } else {
228       HttpHandler sharedHandler = sharedHandlers.get(cd.attributes.get("contextName"));
229       if (sharedHandler instanceof HttpHandler) {
230         h = sharedHandler;
231       } else {
232         h = getHandlerInstance(cd);
233         sharedHandlers.put(cd.attributes.get("contextName"), h);
234       }
235     }
236     return h;
237   }
238   
239   private HttpHandler getHandlerInstance(ContextDescriptor cd) {
240     try {
241       Object handlerObj = Class.forName(cd.className)
242               .getDeclaredConstructor().newInstance();
243       if (handlerObj instanceof HttpHandler) {
244         return (HttpHandler) handlerObj;
245       } else {
246         // kein HttpHandler aus newInstance
247         return null;
248       }
249     } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | 
250             InstantiationException | IllegalAccessException | IllegalArgumentException | 
251             InvocationTargetException ex) {
252       // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
253       return null;
254     }
255   }
256
257   private void wireActors(String packageName, Class annotation, Handler h, String contextName) {
258     ClassLoader cl = ClassLoader.getSystemClassLoader();
259     InputStream stream = cl
260             .getResourceAsStream(packageName.replaceAll("[.]", "/"));
261     BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
262     Iterator i = reader.lines().iterator();
263     while (i.hasNext()) {
264       String line = i.next().toString();
265       if (line.endsWith(".class")) {
266         try {
267           Class actorClass = Class.forName(packageName + "."
268                   + line.substring(0, line.lastIndexOf('.')));
269           if (actorClass != null && actorClass.isAnnotationPresent(annotation)) {
270             wire(h, actorClass, contextName);
271           }
272         } catch (ClassNotFoundException ex) {
273           // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
274         }
275       } else {
276         wireActors(packageName + "." + line, annotation, h, contextName);
277       }
278     }
279   }
280   
281   /*
282     Eine Action-Annotation enthaelt gewoehnlich die Route, 
283     die 'unterhalb' des Kontextpfades als 'Ausloeser' zur 
284     Ausfuehrung der Action verwendet wird.
285   
286     Wenn die Action fuer alle Routen 'unterhalb' des 
287     Kontextpfades ausgefuehrt werden soll, muss die Action 
288     als Route '/' angeben.
289   */
290   private void wire(Handler h, Class c, String contextName) {
291     Method[] methods = c.getMethods();
292     for (Method method : methods) {
293       Action action = method.getAnnotation(Action.class);
294       if (action != null) {
295         List actionHandlers = Arrays.asList(action.handler());
296         if (actionHandlers.contains(contextName)) {
297           h.setActor(action.type(), action.route(), c.getName());
298         }
299       }
300     }
301   }
302
303   /* -------------- FactoryListener Implementierung --------------- */
304   
305   private List<FactoryListener> listeners;
306
307   public void addListener(FactoryListener l) {
308     this.listeners.add(l);
309   }
310
311   public void removeListener(FactoryListener l) {
312     this.listeners.remove(l);
313   }
314
315   public void destroy() {
316     this.listeners.clear();
317     this.listeners = null;
318   }
319
320   private void fireServerCreated(HttpServer server) {
321     for (FactoryListener l : listeners) {
322       l.serverCreated(server);
323     }
324   }
325   
326   private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
327     for (FactoryListener l : listeners) {
328       l.handlerCreated(ctx, h);
329     }
330   }
331
332   private void fireContextCreated(HttpContext context) {
333     for (FactoryListener l : listeners) {
334       l.contextCreated(context);
335     }
336   }
337   
338   private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
339     for (FactoryListener l : listeners) {
340       l.authenticatorCreated(context, auth);
341     }
342   }
343
344   private void fireInstanceStarted() {
345     for (FactoryListener l : listeners) {
346       l.instanceStarted();
347     }
348   }
349 }