Ultrakompakter HTTP Server
ulrich
2024-12-01 1f6776f237cb98c1222ee9dd2f75220de0d02c34
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.sun.net.httpserver.HttpExchange;
21 import com.sun.net.httpserver.HttpHandler;
22 import de.uhilger.neon.Action.Type;
5ebab9 23 import de.uhilger.neon.entity.ActionDescriptor;
e58690 24 import java.io.IOException;
f4025a 25 import java.lang.reflect.Constructor;
e58690 26 import java.lang.reflect.InvocationTargetException;
U 27 import java.lang.reflect.Method;
28 import java.lang.reflect.Parameter;
29 import java.util.EnumMap;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Map;
f4025a 34 import java.util.logging.Level;
U 35 import java.util.logging.Logger;
e58690 36
U 37 /**
5ebab9 38  * Objekte der Klasse Handler nehmen Objekte entgegen die die Annotationen NeonActor enthalten.
U 39  * Deren mit NeonMethod annotierten Methoden stellt der Handler via HTTP bereit.
e58690 40  *
5ebab9 41  * Wird ein Neon-Server mit der Klasse NeonFactory erzeugt, kann mit der Verwendung dieses Handlers
U 42  * die NeonFactory den Server selbsttaetig erstellen, ohne zusaetzlichen Boilerplate Code, den eine
43  * eigene Anwendung mitbringen muesste.
e58690 44  *
U 45  * @author Ulrich Hilger
46  * @version 1, 6.2.2024
47  */
48 public class Handler implements HttpHandler {
49
50   private final Map<Type, Map> dispatcher;
51
52   /**
53    * Ein Objekt der Klasse Handler erzeugen
54    */
55   public Handler() {
56     dispatcher = new EnumMap<>(Type.class);
5ebab9 57     //dispatcher.put(Type.GET, new HashMap<String, String>());
U 58     //dispatcher.put(Type.PUT, new HashMap<String, String>());
59     //dispatcher.put(Type.POST, new HashMap<String, String>());
60     //dispatcher.put(Type.DELETE, new HashMap<String, String>());
61     dispatcher.put(Type.GET, new HashMap<String, ActionDescriptor>());
62     dispatcher.put(Type.PUT, new HashMap<String, ActionDescriptor>());
63     dispatcher.put(Type.POST, new HashMap<String, ActionDescriptor>());
64     dispatcher.put(Type.DELETE, new HashMap<String, ActionDescriptor>());
e58690 65   }
U 66
67   /**
68    * Diesem Handler einen Actor hinzufuegen
69    *
70    * @param methodType HTTP Methode
71    * @param route die Route, ueber die der Actor aufgerufen werden soll
72    * @param className die Klasse, die die Methode enthaelt, die zur Verarbeitung der Route
73    * ausgefuehrt werden soll
74    */
75   public void setActor(Type methodType, String route, String className) {
5ebab9 76     ActionDescriptor ad = new ActionDescriptor();
U 77     ad.className = className;
78     ad.routeParams = new HashMap<>();
79     int pos = route.indexOf("{");
80     if (pos > -1) {
81       String paramStr = route.substring(pos);
82       String[] params = paramStr
83               .replaceAll("\\{", "")
84               .replaceAll("\\}", "")
85               .split("/");
86       for (int i = 0; i < params.length; i++) {
87         ad.routeParams.put(params[i], i);
88       }
89       ad.route = route.substring(0, pos - 1);
90     } else {
91       // Map kann leer bleiben
92       ad.route = route;
93     }
94
e58690 95     //Logger.getLogger(Handler.class.getName())
U 96     //        .log(Level.INFO, "{0} {1} {2}", new Object[]{methodType, route, className});
f4025a 97     //dispatcher.get(methodType).put(ad.route, ad);
U 98     Object adMapObj = dispatcher.get(methodType);
99     if(adMapObj instanceof HashMap hashMap) {
100       @SuppressWarnings("unchecked")
101       HashMap<String, ActionDescriptor> map = hashMap;
102       map.put(ad.route, ad);
103       Logger.getLogger(Handler.class.getName()).log(Level.FINER, "ActionDescriptor route {0} className {1}", new Object[]{route, className});              
104     } else {
105       Logger.getLogger(Handler.class.getName()).finer("ActionDescriptorMap nicht gefunden");        
106     }
107     
e58690 108   }
U 109
110   /**
111    * Eine Antwort senden
112    *
113    * Diese Methode kann ueberschrieben werden, falls die Antwort vor dem Senden noch weiter
114    * bearbeiten werden soll.
115    *
116    * @param exchange das Objekt mit Informationen zu HTTP Anfrage und -Antwort
117    * @param response das Ergebnisobjekt, das als Antwort gesendet werden soll
118    * @throws IOException wenn die Antwort nicht gesendet werden kann
119    */
120   public void respond(HttpExchange exchange, Object response) throws IOException {
121     new HttpResponder().antwortSenden(exchange, HttpResponder.SC_OK, response.toString());
122   }
123
124   /**
125    * Das Objekt abrufen, das auf die Routen verweist, die von diesem Handler beantwortet werden.
126    *
127    * eine Map<Type, Map<String, String>> GET - route1 - Klassenname GET - route2 - Klassenname PUT -
128    * route1 - Klassenname PUT - route2 - Klassenname usw.
129    *
130    * @return das Dispatcher-Objekt dieses Handlers
131    */
132   public Map<Type, Map> getDispatcher() {
133     return dispatcher;
134   }
135
136   /**
137    * Eine HTTP-Anfrage ausfuehren
138    *
139    * @param exchange das Objekt mit Informationen zu HTTP Anfrage und -Antwort
140    * @throws IOException
141    */
142   @Override
143   public void handle(HttpExchange exchange) throws IOException {
880582 144     String route = exchange
U 145             .getRequestURI()
146             .getPath()
147             .substring(exchange
148                     .getHttpContext()
149                     .getPath()
150                     .length());            
f4025a 151     String requestMethodStr = exchange.getRequestMethod();        
U 152     Logger.getLogger(Handler.class.getName()).log(Level.FINER, "method {0} route {1}", new Object[]{requestMethodStr, route});   
153     Type requestMethod = Type.valueOf(requestMethodStr);
e58690 154     /*
U 155       Es wird erst geprueft, ob zu einer bestimmten Route 
156       ein Actor registriert wurde. Wenn kein Actor mit dieser 
157       Route existiert, wird geprueft, ob ein Actor 
158       mit der Route '/' vorhanden ist.
159      */
5ebab9 160     boolean found = false;
63bcde 161     Object md = dispatcher.get(requestMethod);
5ebab9 162     if (md instanceof Map) {
U 163       int pos = route.lastIndexOf("/");
f4025a 164       Logger.getLogger(Handler.class.getName()).log(Level.FINER, "pos {0}", pos);   
5ebab9 165       Object o = ((Map) md).get(route);
U 166       if (!(o instanceof ActionDescriptor)) {
167         while (!found && (pos > -1)) {
168           String routeRest = route.substring(0, pos);
f4025a 169           Logger.getLogger(Handler.class.getName()).log(Level.FINER, "pos {0} routeRest {1}", new Object[]{pos, routeRest});
5ebab9 170           o = ((Map) md).get(routeRest);
U 171           if (o instanceof ActionDescriptor) {
172             found = true;
982611 173             handleRequest(exchange, o, routeRest, route.substring(routeRest.length()), requestMethod);
e58690 174           }
5ebab9 175           pos = routeRest.lastIndexOf("/");
e58690 176         }
5ebab9 177       } else {
U 178         found = true;
982611 179         handleRequest(exchange, o, route, route, requestMethod);
e58690 180       }
5ebab9 181       if (!found) {
f4025a 182         Logger.getLogger(Handler.class.getName()).log(Level.FINER, "{0} not found ", route);
5ebab9 183         o = dispatcher.get(requestMethod).get("/");
U 184         if (o instanceof ActionDescriptor) {
982611 185           handleRequest(exchange, o, route, route, requestMethod);
f4025a 186         } else {
U 187           // kein ActionDescriptor für '/'
188           Logger.getLogger(Handler.class.getName()).log(Level.FINER, "Kein Actiondescriptor fuer '/'");   
5ebab9 189         }
U 190       }
f4025a 191     } else {
U 192       // keine Actions fuer HTTP Methode
193       Logger.getLogger(Handler.class.getName()).log(Level.FINER, "Kein Actions fuer HTTP-Methode {0}", requestMethodStr);   
63bcde 194     }
e58690 195
U 196   }
197
982611 198   private void handleRequest(HttpExchange exchange, Object o, String route, String subroute, Type requestMethod) throws IOException {
f4025a 199     Logger.getLogger(Handler.class.getName()).log(Level.FINER, "Handle Request route {0} subroute {1}", new Object[]{route, subroute});  
5ebab9 200     ActionDescriptor ad = (ActionDescriptor) o;
U 201     String actorClassName = ad.className;
202     try {
203       Class actorClass = Class.forName(actorClassName);
204       Method[] methods = actorClass.getMethods();
205       for (Method method : methods) {
206         Action action = method.getAnnotation(Action.class);
207         if (action != null) {
982611 208           if ((action.route().equals("/") || action.route().startsWith(route)) && action.type().equals(requestMethod)) {
5ebab9 209             Object[] actionArgs = getActionArgs(exchange, method, ad, subroute);
f4025a 210             @SuppressWarnings("unchecked")
U 211             Object conObj = actorClass.getDeclaredConstructor();
212             if(conObj instanceof Constructor) {
213               Constructor con = (Constructor) conObj;
214               Object actorObj;
215               actorObj = con.newInstance();
216               addDataProvider(exchange, actorObj);
217               Object antwort = method.invoke(actorObj, actionArgs);
218               if (!action.handlesResponse()) {
219                 respond(exchange, antwort);
220               }
221             } else {
222               // kein Konstruktor
223               Logger.getLogger(Handler.class.getName()).info("Kein Konstruktor gefunden");        
5ebab9 224             }
U 225           }
226         }
227       }
228     } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | 
229             InstantiationException | IllegalAccessException | IllegalArgumentException | 
230             InvocationTargetException ex) {
231       // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
f4025a 232       Logger.getLogger(Handler.class.getName()).finer("Kein passende Actor-Klasse gefunden");        
5ebab9 233     }
U 234     //}
235   }
236
237   private Object[] getActionArgs(HttpExchange exchange, Method method, ActionDescriptor ad, String subroute) {
63bcde 238     int count = method.getParameterCount();
e58690 239     Parameter[] methodParams = method.getParameters();
63bcde 240     Object[] actionArgs = new Object[count];
5ebab9 241     String[] routeParams = subroute.split("/");
880582 242     
U 243     
244     /*
245       Fall 1: Es sind mehr als ein Parameter zu uebergeben und die Route enthaelt 
246       weniger Parameter als die Methode erfordert.
247       Fall 2: Die Methode erwartet Parameter und der erste Parameter ist nicht 
248       vom Typ HttpExchange.
249       
250       Wenn einer dieser beiden Faelle eintritt, wird alles als Parameter an die Methode 
251       uebergeben, was eventuell als Teil einer Query im URL oder im Body enthalten ist.
8bab5f 252       Fuer Mthoden, die nicht vom Typ HTTP GET sind, kann ein Actor kann dann den Body 
U 253       nicht mehr lesen, weil das bereits an dieser Stelle gemacht wurde.
880582 254     */
63bcde 255     Map queryParams = new HashMap();
5ebab9 256     if ((count > 1 && count > routeParams.length)
U 257             || (methodParams.length > 0 && !methodParams[0].getType().equals(HttpExchange.class))) {
63bcde 258       queryParams = new HttpHelper().getQueryMap(exchange);
U 259     }
880582 260     
63bcde 261     int k = 0;
e58690 262     for (Parameter methodParam : methodParams) {
U 263       if (methodParam.getType().equals(HttpExchange.class)) {
264         actionArgs[k] = exchange;
265       } else {
5ebab9 266         Integer i = ad.routeParams.getOrDefault(methodParam.getName(), -1);
U 267         if (i < 0) {
268           actionArgs[k] = queryParams.get(methodParam.getName());
269         } else {
270           actionArgs[k] = routeParams[i + 1];
271         }
e58690 272       }
5ebab9 273       ++k;
e58690 274     }
U 275     return actionArgs;
276   }
277
278   private void addDataProvider(HttpExchange exchange, Object actorObj) {
279     if (actorObj instanceof DataConsumer) {
280       DataConsumer consumer = (DataConsumer) actorObj;
281       Object sdpListObj = exchange.getHttpContext().getAttributes().get("serverDataProviderList");
282       if (sdpListObj instanceof List) {
283         List sdpList = (List) sdpListObj;
284         Iterator i = sdpList.iterator();
285         while (i.hasNext()) {
286           Object value = i.next();
287           if (value instanceof DataProvider) {
288             consumer.addDataProvider((DataProvider) value);
289           }
290         }
291       }
292     }
293   }
294 }