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.sun.net.httpserver.HttpExchange;
21 import com.sun.net.httpserver.HttpHandler;
22 import de.uhilger.neon.Action.Type;
23 import java.io.IOException;
24 import java.lang.reflect.InvocationTargetException;
25 import java.lang.reflect.Method;
26 import java.lang.reflect.Parameter;
27 import java.util.EnumMap;
28 import java.util.HashMap;
29 import java.util.Iterator;
30 import java.util.List;
31 import java.util.Map;
32
33 /**
34  * Objekte der Klasse Handler nehmen Objekte entgegen die die Annotationen
35  * NeonActor enthalten. Deren mit NeonMethod annotierten Methoden stellt 
36  * der Handler via HTTP bereit.
37  *
38  * Wird ein Neon-Server mit der Klasse NeonFactory erzeugt, kann mit der Verwendung 
39  * dieses Handlers die NeonFactory den Server selbsttaetig erstellen, ohne 
40  * zusaetzlichen Boilerplate Code, den eine eigene Anwendung mitbringen muesste.
41  *
42  * @author Ulrich Hilger
43  * @version 1, 6.2.2024
44  */
45 public class Handler implements HttpHandler {
46
47   private final Map<Type, Map> dispatcher;
48
49   /**
50    * Ein Objekt der Klasse Handler erzeugen
51    */
52   public Handler() {
53     dispatcher = new EnumMap<>(Type.class);
54     dispatcher.put(Type.GET, new HashMap<String, String>());
55     dispatcher.put(Type.PUT, new HashMap<String, String>());
56     dispatcher.put(Type.POST, new HashMap<String, String>());
57     dispatcher.put(Type.DELETE, new HashMap<String, String>());
58   }
59
60   /**
61    * Diesem Handler einen Actor hinzufuegen
62    *
63    * @param methodType HTTP Methode
64    * @param route die Route, ueber die der Actor aufgerufen werden soll
65    * @param className die Klasse, die die Methode enthaelt, die zur Verarbeitung der Route
66    * ausgefuehrt werden soll
67    */
68   public void setActor(Type methodType, String route, String className) {
69     //Logger.getLogger(Handler.class.getName())
70     //        .log(Level.INFO, "{0} {1} {2}", new Object[]{methodType, route, className});
71     dispatcher.get(methodType).put(route, className);
72   }
73
74   /**
75    * Eine Antwort senden
76    *
77    * Diese Methode kann ueberschrieben werden, falls die Antwort vor dem Senden noch weiter
78    * bearbeiten werden soll.
79    *
80    * @param exchange das Objekt mit Informationen zu HTTP Anfrage und -Antwort
81    * @param response das Ergebnisobjekt, das als Antwort gesendet werden soll
82    * @throws IOException wenn die Antwort nicht gesendet werden kann
83    */
84   public void respond(HttpExchange exchange, Object response) throws IOException {
85     new HttpResponder().antwortSenden(exchange, HttpResponder.SC_OK, response.toString());
86   }
87
88   /**
89    * Das Objekt abrufen, das auf die Routen verweist, die von diesem Handler beantwortet werden.
90    *
91    * eine Map<Type, Map<String, String>> GET - route1 - Klassenname GET - route2 - Klassenname PUT -
92    * route1 - Klassenname PUT - route2 - Klassenname usw.
93    *
94    * @return das Dispatcher-Objekt dieses Handlers
95    */
96   public Map<Type, Map> getDispatcher() {
97     return dispatcher;
98   }
99
100   /**
101    * Eine HTTP-Anfrage ausfuehren
102    *
103    * @param exchange das Objekt mit Informationen zu HTTP Anfrage und -Antwort
104    * @throws IOException
105    */
106   @Override
107   public void handle(HttpExchange exchange) throws IOException {
63bcde 108     HttpHelper hh = new HttpHelper();    
e58690 109     String route = hh.getRouteString(exchange);
U 110     Type requestMethod = Type.valueOf(exchange.getRequestMethod());
63bcde 111     //Map queryParams = hh.getQueryMap(exchange);
U 112     //Object o;
e58690 113
U 114     /*
115       Es wird erst geprueft, ob zu einer bestimmten Route 
116       ein Actor registriert wurde. Wenn kein Actor mit dieser 
117       Route existiert, wird geprueft, ob ein Actor 
118       mit der Route '/' vorhanden ist.
119      */
63bcde 120     Object md = dispatcher.get(requestMethod);
U 121     if(md instanceof Map) {
122     Object o = ((Map) md).get(route);
e58690 123     if (!(o instanceof String)) {
U 124       o = dispatcher.get(requestMethod).get("/");
125     }
126
127     if (o instanceof String) {
128       String actorClassName = (String) o;
129       try {
130         Class actorClass = Class.forName(actorClassName);
131         Method[] methods = actorClass.getMethods();
132         for (Method method : methods) {
133           Action action = method.getAnnotation(Action.class);
134           if (action != null) {
135             if (action.route().equals("/") || action.route().equals(route)) {
63bcde 136               Object[] actionArgs = getActionArgs(exchange, method/*, queryParams*/);
e58690 137               Object actorObj = actorClass.getDeclaredConstructor().newInstance();
U 138               addDataProvider(exchange, actorObj);
139               Object antwort = method.invoke(actorObj, actionArgs);
63bcde 140               if(!action.handlesResponse()) {
U 141                 respond(exchange, antwort);
142               }
e58690 143             }
U 144           }
145         }
146       } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
147         // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
148       }
149     }
63bcde 150     }
e58690 151
U 152   }
153
63bcde 154   private Object[] getActionArgs(HttpExchange exchange, Method method/*, Map queryParams*/) {    
U 155     int count = method.getParameterCount();
e58690 156     Parameter[] methodParams = method.getParameters();
63bcde 157     Object[] actionArgs = new Object[count];
U 158     Map queryParams = new HashMap();
159     /*
160       Lesen des Body der Anfrage geht nur einmal. 
161     
162       bodyLesen soll nur in getQueryMap gerufen werden, wenn 
163       die Liste der Parameter mehr als einen Parameter umfasst 
164       oder wenn es nur ein Parameter ist, der nicht 
165       der HttpExchange ist.
166     
167       Anderenfalls sollte erst der Actor den Body aus dem 
168       HttpExchange lesen und nicht hier schon der Handler. 
169     */
170     if(count > 1 || !methodParams[0].getType().equals(HttpExchange.class)) {
171       queryParams = new HttpHelper().getQueryMap(exchange);
172     }
173     int k = 0;
e58690 174     for (Parameter methodParam : methodParams) {
U 175       if (methodParam.getType().equals(HttpExchange.class)) {
176         actionArgs[k] = exchange;
177       } else {
178         /*
179           Konvention: Aktor-Parameter sind immer vom Typ String
180           und Parametername der Methode ist gleich dem Namen in der Query 
181          */
182         actionArgs[k++] = queryParams.get(methodParam.getName());
183       }
184     }
185     return actionArgs;
186   }
187
188   private void addDataProvider(HttpExchange exchange, Object actorObj) {
189     if (actorObj instanceof DataConsumer) {
190       DataConsumer consumer = (DataConsumer) actorObj;
191       Object sdpListObj = exchange.getHttpContext().getAttributes().get("serverDataProviderList");
192       if (sdpListObj instanceof List) {
193         List sdpList = (List) sdpListObj;
194         Iterator i = sdpList.iterator();
195         while (i.hasNext()) {
196           Object value = i.next();
197           if (value instanceof DataProvider) {
198             consumer.addDataProvider((DataProvider) value);
199           }
200         }
201       }
202     }
203   }
204 }