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.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 {
108     HttpHelper hh = new HttpHelper();
109     String route = hh.getRouteString(exchange);
110     Type requestMethod = Type.valueOf(exchange.getRequestMethod());
111     Map queryParams = hh.getQueryMap(exchange);
112     Object o;
113
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      */
120     o = dispatcher.get(requestMethod).get(route);
121     if (!(o instanceof String)) {
122       o = dispatcher.get(requestMethod).get("/");
123     }
124
125     if (o instanceof String) {
126       String actorClassName = (String) o;
127       try {
128         Class actorClass = Class.forName(actorClassName);
129         Method[] methods = actorClass.getMethods();
130         for (Method method : methods) {
131           Action action = method.getAnnotation(Action.class);
132           if (action != null) {
133             if (action.route().equals("/") || action.route().equals(route)) {
134               Object[] actionArgs = getActionArgs(exchange, method, queryParams);
135               Object actorObj = actorClass.getDeclaredConstructor().newInstance();
136               addDataProvider(exchange, actorObj);
137               Object antwort = method.invoke(actorObj, actionArgs);
138               respond(exchange, antwort);
139             }
140           }
141         }
142       } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
143         // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
144       }
145     }
146
147   }
148
149   private Object[] getActionArgs(HttpExchange exchange, Method method, Map queryParams) {
150     Object[] actionArgs = new Object[method.getParameterCount()];
151     int k = 0;
152     Parameter[] methodParams = method.getParameters();
153     for (Parameter methodParam : methodParams) {
154       if (methodParam.getType().equals(HttpExchange.class)) {
155         actionArgs[k] = exchange;
156       } else {
157         /*
158           Konvention: Aktor-Parameter sind immer vom Typ String
159           und Parametername der Methode ist gleich dem Namen in der Query 
160          */
161         actionArgs[k++] = queryParams.get(methodParam.getName());
162       }
163     }
164     return actionArgs;
165   }
166
167   private void addDataProvider(HttpExchange exchange, Object actorObj) {
168     if (actorObj instanceof DataConsumer) {
169       DataConsumer consumer = (DataConsumer) actorObj;
170       Object sdpListObj = exchange.getHttpContext().getAttributes().get("serverDataProviderList");
171       if (sdpListObj instanceof List) {
172         List sdpList = (List) sdpListObj;
173         Iterator i = sdpList.iterator();
174         while (i.hasNext()) {
175           Object value = i.next();
176           if (value instanceof DataProvider) {
177             consumer.addDataProvider((DataProvider) value);
178           }
179         }
180       }
181     }
182   }
183 }