Ultrakompakter HTTP Server
ulrich
2024-02-17 607a22b68052299cc8dedee4b948198ddce62cef
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();
164     while (contextIterator.hasNext()) {
165       ContextDescriptor cd = contextIterator.next();
166       HttpHandler h = buildHandler(cd, sharedHandlers);
167       if (h != null) {
168         HttpContext ctx = server.createContext(cd.contextPath, h);        
169         Map<String, Object> ctxAttrs = ctx.getAttributes();
170         /*
171           Achtung: Wenn verschiedene Elemente dasselbe Attribut 
172           deklarieren, ueberschreiben sie sich die Attribute gegenseitig.
173          */
174         ctxAttrs.putAll(cd.attributes);        
175         if (h instanceof Handler) {         
176           for (String packageName : packageNames) {
177             wireActors(
178                     packageName, Actor.class, (Handler) h, 
179                     cd.attributes.get("contextName"));
180               ctx.getAttributes().put("serverDataProviderList", sdp);
181           }
182         }        
183         Authenticator auth = createAuthenticator(d);
184         if (auth instanceof Authenticator && cd.authenticator instanceof String) {
185             ctx.setAuthenticator(auth);      
186             ctx.getAttributes().putAll(d.authenticator.attributes);
187             fireAuthenticatorCreated(ctx, auth);
188         }
189         fireHandlerCreated(ctx, h);
190         fireContextCreated(ctx);
191       } else {
192         // Handler konnte nicht erstellt werden
193       }
194     }
195   }
196   
197   private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, 
198           IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
199     HttpHandler h;    
200     if (!cd.sharedHandler) {
201       h = getHandlerInstance(cd);
202     } else {
203       HttpHandler sharedHandler = sharedHandlers.get(cd.attributes.get("contextName"));
204       if (sharedHandler instanceof HttpHandler) {
205         h = sharedHandler;
206       } else {
207         h = getHandlerInstance(cd);
208         sharedHandlers.put(cd.attributes.get("contextName"), h);
209       }
210     }
211     return h;
212   }
213   
214   private HttpHandler getHandlerInstance(ContextDescriptor cd) {
215     try {
216       Object handlerObj = Class.forName(cd.className)
217               .getDeclaredConstructor().newInstance();
218       if (handlerObj instanceof HttpHandler) {
219         return (HttpHandler) handlerObj;
220       } else {
221         // kein HttpHandler aus newInstance
222         return null;
223       }
224     } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | 
225             InstantiationException | IllegalAccessException | IllegalArgumentException | 
226             InvocationTargetException ex) {
227       // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
228       return null;
229     }
230   }
231
232   private void wireActors(String packageName, Class annotation, Handler h, String contextName) {
233     ClassLoader cl = ClassLoader.getSystemClassLoader();
234     InputStream stream = cl
235             .getResourceAsStream(packageName.replaceAll("[.]", "/"));
236     BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
237     Iterator i = reader.lines().iterator();
238     while (i.hasNext()) {
239       String line = i.next().toString();
240       if (line.endsWith(".class")) {
241         try {
242           Class actorClass = Class.forName(packageName + "."
243                   + line.substring(0, line.lastIndexOf('.')));
244           if (actorClass != null && actorClass.isAnnotationPresent(annotation)) {
245             wire(h, actorClass, contextName);
246           }
247         } catch (ClassNotFoundException ex) {
248           // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
249         }
250       } else {
251         wireActors(packageName + "." + line, annotation, h, contextName);
252       }
253     }
254   }
255   
256   /*
257     Eine Action-Annotation enthaelt gewoehnlich die Route, 
258     die 'unterhalb' des Kontextpfades als 'Ausloeser' zur 
259     Ausfuehrung der Action verwendet wird.
260   
261     Wenn die Action fuer alle Routen 'unterhalb' des 
262     Kontextpfades ausgefuehrt werden soll, muss die Action 
263     als Route '/' angeben.
264   */
265   private void wire(Handler h, Class c, String contextName) {
266     Method[] methods = c.getMethods();
267     for (Method method : methods) {
268       Action action = method.getAnnotation(Action.class);
269       if (action != null) {
270         List actionHandlers = Arrays.asList(action.handler());
271         if (actionHandlers.contains(contextName)) {
272           h.setActor(action.type(), action.route(), c.getName());
273         }
274       }
275     }
276   }
277
278   /* -------------- FactoryListener Implementierung --------------- */
279   
280   private List<FactoryListener> listeners;
281
282   public void addListener(FactoryListener l) {
283     this.listeners.add(l);
284   }
285
286   public void removeListener(FactoryListener l) {
287     this.listeners.remove(l);
288   }
289
290   public void destroy() {
291     this.listeners.clear();
292     this.listeners = null;
293   }
294
295   private void fireServerCreated(HttpServer server) {
296     for (FactoryListener l : listeners) {
297       l.serverCreated(server);
298     }
299   }
300   
301   private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
302     for (FactoryListener l : listeners) {
303       l.handlerCreated(ctx, h);
304     }
305   }
306
307   private void fireContextCreated(HttpContext context) {
308     for (FactoryListener l : listeners) {
309       l.contextCreated(context);
310     }
311   }
312   
313   private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
314     for (FactoryListener l : listeners) {
315       l.authenticatorCreated(context, auth);
316     }
317   }
318
319   private void fireInstanceStarted() {
320     for (FactoryListener l : listeners) {
321       l.instanceStarted();
322     }
323   }
324 }