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