/*
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 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 c, NeonDescriptor d, List packageNames, List sdp)
throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
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(c, 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;
}
}
private void wireActors(Class c, String packageName, Class annotation, Handler h, String contextName) {
JarScanner js = new JarScanner();
URI path;
try {
path = js.getPath(c);
if(path.toString().endsWith(".class")) {
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 {
ClassLoader cl = js.getUrlClassLoader(c);
js.processZipContent(cl, new File(path), packageName, this, h, contextName);
}
} catch (URISyntaxException ex) {
Logger.getLogger(Factory.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);
}
}
/*
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.
*/
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 actorFound(Class actorClass, Handler h, String contextName) {
wire(h, actorClass, contextName);
}
}