/*
|
neon - Embeddable HTTP Server based on jdk.httpserver
|
Copyright (C) 2024 Ulrich Hilger
|
|
This program is free software: you can redistribute it and/or modify
|
it under the terms of the GNU Affero General Public License as
|
published by the Free Software Foundation, either version 3 of the
|
License, or (at your option) any later version.
|
|
This program is distributed in the hope that it will be useful,
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
GNU Affero General Public License for more details.
|
|
You should have received a copy of the GNU Affero General Public License
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*/
|
package de.uhilger.neon;
|
|
import com.google.gson.Gson;
|
import com.sun.net.httpserver.Authenticator;
|
import com.sun.net.httpserver.Filter;
|
import com.sun.net.httpserver.HttpContext;
|
import com.sun.net.httpserver.HttpHandler;
|
import com.sun.net.httpserver.HttpServer;
|
import de.uhilger.neon.entity.ContextDescriptor;
|
import de.uhilger.neon.entity.NeonDescriptor;
|
import de.uhilger.neon.entity.ServerDescriptor;
|
import java.io.BufferedReader;
|
import java.io.File;
|
import java.io.FileReader;
|
import java.io.IOException;
|
import java.io.InputStream;
|
import java.io.InputStreamReader;
|
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.Method;
|
import java.net.InetSocketAddress;
|
import java.net.MalformedURLException;
|
import java.net.URI;
|
import java.net.URISyntaxException;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.HashMap;
|
import java.util.Iterator;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.concurrent.Executors;
|
import java.util.logging.Level;
|
import java.util.logging.Logger;
|
import de.uhilger.neon.Scanner.ScannerListener;
|
|
/**
|
* Einen Neon-Server aus einer Beschreibungsdatei herstellen
|
*
|
* Die Werte aus der Beschreibungsdatei werden in die Attribute der HttpContext-Objekte geschrieben,
|
* die zu jedem Server eroeffnet werden.
|
*
|
* Die Entitaeten stehen wie folgt in Beziehung: HttpServer -1:n-> HttpContext -1:1-> HttpHandler
|
*
|
* Die Factory legt die Kontexte, Handler sowie die Verbindung zu den Actors selbsttaetig an. Alle
|
* Parameter aus 'attributes'-Elementen der Beschreibungsdatei werden als Attribute in den
|
* HttpContext uebertragen. Deshalb ist es wichtig, dass die Attributnamen eindeutig gewaehlt
|
* werden, damit sie sich nicht gegenseitig ueberschreiben.
|
*
|
* @author Ulrich Hilger
|
* @version 1, 6.2.2024
|
*/
|
public class Factory implements ScannerListener {
|
|
public Factory() {
|
listeners = new ArrayList<>();
|
}
|
|
/**
|
* Beschreibungsdatei lesen
|
*
|
* @param file die Datei, die den Server beschreibt
|
* @return ein Objekt, das den Server beschreibt
|
* @throws IOException wenn die Datei nicht gelesen werden konnte
|
*/
|
public NeonDescriptor readDescriptor(File file) throws IOException {
|
//Logger logger = Logger.getLogger(Factory.class.getName());
|
//logger.log(Level.INFO, "reading NeonDescriptor from {0}", file.getAbsolutePath());
|
|
StringBuilder sb = new StringBuilder();
|
BufferedReader r = new BufferedReader(new FileReader(file));
|
String line = r.readLine();
|
while (line != null) {
|
sb.append(line);
|
line = r.readLine();
|
}
|
r.close();
|
|
Gson gson = new Gson();
|
return gson.fromJson(sb.toString(), NeonDescriptor.class);
|
}
|
|
public void runInstance(Class c, NeonDescriptor d)
|
throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
|
IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
|
this.runInstance(c, d, null, new ArrayList<>());
|
}
|
|
public void runInstance(Class c, NeonDescriptor d, List<String> packageNames)
|
throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
|
IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
|
this.runInstance(c, d, packageNames, new ArrayList<>());
|
}
|
|
/**
|
* Einen Neon-Server gemaess einem Serverbeschreibungsobjekt herstellen und starten
|
*
|
* @param starter die Klasse, mit der Neon durch Aufruf dieser Methode gestartet wird
|
* @param d das Object mit der Serverbeschreibung
|
* @param packageNames Namen der Packages, aus der rekursiv vorgefundene Actors eingebaut werden
|
* sollen
|
* @param sdp die DataProvider fuer diese Neon-Instanz
|
* @throws ClassNotFoundException
|
* @throws NoSuchMethodException
|
* @throws InstantiationException
|
* @throws IllegalAccessException
|
* @throws IllegalArgumentException
|
* @throws InvocationTargetException
|
* @throws IOException
|
*/
|
public void runInstance(Class starter, NeonDescriptor d, List<String> packageNames, List<DataProvider> sdp)
|
throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
|
IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
|
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, System.getProperty("java.class.path"));
|
|
List serverList = d.server;
|
Iterator<ServerDescriptor> serverIterator = serverList.iterator();
|
while (serverIterator.hasNext()) {
|
ServerDescriptor sd = serverIterator.next();
|
HttpServer server = HttpServer.create(new InetSocketAddress(sd.port), 0);
|
fireServerCreated(server);
|
|
if (packageNames == null) {
|
packageNames = d.actorPackages;
|
}
|
addContexts(new Scanner(starter, Actor.class), d, server, sd.contexts, packageNames, sdp);
|
|
server.setExecutor(Executors.newFixedThreadPool(10));
|
server.start();
|
}
|
fireInstanceStarted();
|
}
|
|
private Authenticator createAuthenticator(NeonDescriptor d) {
|
Authenticator auth = null;
|
if (d.authenticator != null) {
|
try {
|
Object authObj = Class.forName(d.authenticator.className)
|
.getDeclaredConstructor().newInstance();
|
if (authObj instanceof Authenticator) {
|
auth = (Authenticator) authObj;
|
return auth;
|
}
|
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException
|
| InstantiationException | IllegalAccessException | IllegalArgumentException
|
| InvocationTargetException ex) {
|
// Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
|
return null;
|
}
|
}
|
return auth;
|
}
|
|
private void addContexts(Scanner scn, NeonDescriptor d, HttpServer server, List contextList, List<String> packageNames,
|
List<DataProvider> sdp)
|
throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
|
IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
|
Map<String, HttpHandler> sharedHandlers = new HashMap();
|
Iterator<ContextDescriptor> contextIterator = contextList.iterator();
|
Authenticator auth = null;
|
while (contextIterator.hasNext()) {
|
ContextDescriptor cd = contextIterator.next();
|
HttpHandler h = buildHandler(cd, sharedHandlers);
|
if (h != null) {
|
HttpContext ctx = server.createContext(cd.contextPath, h);
|
Map<String, Object> ctxAttrs = ctx.getAttributes();
|
/*
|
Achtung: Wenn verschiedene Elemente dasselbe Attribut
|
deklarieren, ueberschreiben sie sich die Attribute gegenseitig.
|
*/
|
ctxAttrs.putAll(cd.attributes);
|
if (h instanceof Handler) {
|
for (String packageName : packageNames) {
|
scn.process(this, packageName, (Handler) h, cd.attributes.get("contextName"));
|
ctx.getAttributes().put("serverDataProviderList", sdp);
|
}
|
}
|
if (cd.authenticator instanceof String) {
|
if (!(auth instanceof Authenticator)) {
|
auth = createAuthenticator(d);
|
}
|
if (auth instanceof Authenticator) {
|
ctx.setAuthenticator(auth);
|
ctx.getAttributes().putAll(d.authenticator.attributes);
|
fireAuthenticatorCreated(ctx, auth); // event umbenennen in etwas wie authAdded oder so
|
}
|
|
}
|
|
//Authenticator auth = createAuthenticator(d);
|
//if (auth instanceof Authenticator && cd.authenticator instanceof String) {
|
// ctx.setAuthenticator(auth);
|
// ctx.getAttributes().putAll(d.authenticator.attributes);
|
// fireAuthenticatorCreated(ctx, auth);
|
//}
|
if (cd.filter != null) {
|
for (String filterClassName : cd.filter) {
|
//
|
Object filterObj = Class.forName(filterClassName)
|
.getDeclaredConstructor().newInstance();
|
if (filterObj instanceof Filter) {
|
Filter filter = (Filter) filterObj;
|
ctx.getFilters().add(filter);
|
}
|
}
|
}
|
fireHandlerCreated(ctx, h);
|
fireContextCreated(ctx);
|
} else {
|
// Handler konnte nicht erstellt werden
|
}
|
}
|
}
|
|
private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
|
IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
|
HttpHandler h;
|
if (!cd.sharedHandler) {
|
h = getHandlerInstance(cd);
|
} else {
|
HttpHandler sharedHandler = sharedHandlers.get(cd.attributes.get("contextName"));
|
if (sharedHandler instanceof HttpHandler) {
|
h = sharedHandler;
|
} else {
|
h = getHandlerInstance(cd);
|
sharedHandlers.put(cd.attributes.get("contextName"), h);
|
}
|
}
|
return h;
|
}
|
|
private HttpHandler getHandlerInstance(ContextDescriptor cd) {
|
try {
|
Object handlerObj = Class.forName(cd.className)
|
.getDeclaredConstructor().newInstance();
|
if (handlerObj instanceof HttpHandler) {
|
return (HttpHandler) handlerObj;
|
} else {
|
// kein HttpHandler aus newInstance
|
return null;
|
}
|
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException
|
| InstantiationException | IllegalAccessException | IllegalArgumentException
|
| InvocationTargetException ex) {
|
// Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
|
return null;
|
}
|
}
|
|
/**
|
* Diese Testmethode zeigt, dass die Methode getResourceAsStream nicht funktioniert wie
|
* dokumentiert.
|
*
|
* 1. Sie liefert den Inhalt einer gegebenen Package mitsamt Unterpackages als Stream, wenn sie
|
* auf eine Packagestruktur angewendet wird, die unverpackt in einem Verzeichnis des Dateisystems
|
* liegt.
|
*
|
* 2. Sie liefert - faelschlicherweise - null bzw. einen leeren Stream, wenn die Packagestruktur
|
* in einem Jar verpackt ist.
|
*
|
* Saemtliche Versuche, ueber den ClassPath oder die Pfadangabe der Package das Verhalten zu
|
* aendern, gehen bislang fehl (z.B. / oder . als Separator, / oder . zu Beginn enthalten oder
|
* nicht, realative oder absolute packagepfadangabe). Es ist auch unerheblich, ob
|
* Class.getResourceAsStream oder Class.getClassLoader().getResourceAsStream verwendet wird.
|
*
|
* Unabhaengig davon, ob und wie letztlich im Fall 2. oben die Methode getResourceAsStream dazu zu
|
* bringen waere, eine Inhaltsliste fuer eine Package zu liefern ist allein die Tatsache, dass
|
* sich die Methode unterschiedlich verhaelt bereits ein schwerer Bug.
|
*
|
* @param c
|
* @param packageName
|
*/
|
private void listClasses(Class c, String packageName) {
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, "packageName: " + packageName);
|
//ClassLoader cl = c.getClassLoader();
|
String newPackageName = packageName.replaceAll("[.]", "/");
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, "newPackageName: " + newPackageName);
|
InputStream stream = c
|
.getResourceAsStream(newPackageName);
|
if (stream != null) {
|
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
|
Iterator i = reader.lines().iterator();
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, Long.toString(reader.lines().count()));
|
while (i.hasNext()) {
|
String line = i.next().toString();
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, "class to inspect: " + line);
|
if (line.endsWith(".class")) {
|
try {
|
Class actorClass = c.getClassLoader().loadClass(packageName + "."
|
+ line.substring(0, line.lastIndexOf('.')));
|
if (actorClass != null && actorClass.isAnnotationPresent(Actor.class)) {
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, "ACTOR");
|
} else {
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, "no actor");
|
}
|
} catch (ClassNotFoundException ex) {
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Klasse nicht gefunden");
|
}
|
} else {
|
listClasses(c, packageName + "." + line);
|
}
|
}
|
} else {
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, "stream ist null");
|
}
|
}
|
|
/*
|
Eine Action-Annotation enthaelt gewoehnlich die Route,
|
die 'unterhalb' des Kontextpfades als 'Ausloeser' zur
|
Ausfuehrung der Action verwendet wird.
|
|
Wenn die Action fuer alle Routen 'unterhalb' des
|
Kontextpfades ausgefuehrt werden soll, muss die Action
|
als Route '/' angeben.
|
*/
|
/**
|
*
|
* @param h Handler, dem der Actor hinzugefuegt wird, falls der Kontext uebereinstimmt
|
* @param c hinzuzufuegende Actor-Klasse
|
* @param contextName Name des Kontext, dem der Actor hinzugefuegt wird
|
*/
|
private void wire(Handler h, Class c, String contextName) {
|
Method[] methods = c.getMethods();
|
for (Method method : methods) {
|
Action action = method.getAnnotation(Action.class);
|
if (action != null) {
|
List actionHandlers = Arrays.asList(action.handler());
|
if (actionHandlers.contains(contextName)) {
|
h.setActor(action.type(), action.route(), c.getName());
|
}
|
}
|
}
|
}
|
|
/* -------------- FactoryListener Implementierung --------------- */
|
private List<FactoryListener> listeners;
|
|
public void addListener(FactoryListener l) {
|
this.listeners.add(l);
|
}
|
|
public void removeListener(FactoryListener l) {
|
this.listeners.remove(l);
|
}
|
|
public void destroy() {
|
this.listeners.clear();
|
this.listeners = null;
|
}
|
|
private void fireServerCreated(HttpServer server) {
|
for (FactoryListener l : listeners) {
|
l.serverCreated(server);
|
}
|
}
|
|
private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
|
for (FactoryListener l : listeners) {
|
l.handlerCreated(ctx, h);
|
}
|
}
|
|
private void fireContextCreated(HttpContext context) {
|
for (FactoryListener l : listeners) {
|
l.contextCreated(context);
|
}
|
}
|
|
private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
|
for (FactoryListener l : listeners) {
|
l.authenticatorCreated(context, auth);
|
}
|
}
|
|
private void fireInstanceStarted() {
|
for (FactoryListener l : listeners) {
|
l.instanceStarted();
|
}
|
}
|
|
/* -------------- ScannerListener Implementierung --------------- */
|
@Override
|
public void annotationFound(Class foundClass, Object[] params) {
|
Handler h = null;
|
String contextName = null;
|
for(Object param : params) {
|
if(param instanceof Handler) {
|
h = (Handler) param;
|
} else if(param instanceof String) {
|
contextName = (String) param;
|
}
|
}
|
if(h == null || contextName == null) {
|
Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Handler oder contextName ist null");
|
} else {
|
wire(h, foundClass, contextName);
|
}
|
}
|
}
|