/*
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 .
*/
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.JarScanner.JarScannerListener;
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;
/**
* 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 JarScannerListener {
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 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 packageNames, List 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 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(starter, 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(Class c, NeonDescriptor d, HttpServer server, List contextList, List packageNames,
List sdp)
throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
Map sharedHandlers = new HashMap();
Iterator 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 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) {
wireActors(c,
packageName, Actor.class, (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 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;
}
}
@SuppressWarnings("unchecked")
private void wireActors(Class c, String packageName, Class annotation, Handler h, String contextName) {
JarScanner js = new JarScanner(c, annotation);
if (!js.isJar()) {
ClassLoader cl = c.getClassLoader();
InputStream stream = cl
.getResourceAsStream(packageName.replaceAll("[.]", "/"));
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
Iterator i = reader.lines().iterator();
while (i.hasNext()) {
String line = i.next().toString();
if (line.endsWith(".class")) {
try {
Class actorClass = cl.loadClass(packageName + "."
+ line.substring(0, line.lastIndexOf('.')));
if (actorClass != null && actorClass.isAnnotationPresent(annotation)) {
wire(h, actorClass, contextName);
}
} catch (ClassNotFoundException ex) {
// Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden?
}
} else {
wireActors(c, packageName + "." + line, annotation, h, contextName);
}
}
} else {
js.processZipContent(packageName, this, h, contextName);
}
}
/**
* 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 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();
}
}
/* -------------- JarScannerListener Implementierung --------------- */
@Override
public void annotationFound(Class foundClass, Handler h, String contextName) {
wire(h, foundClass, contextName);
}
}