/* 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 java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.Enumeration; import java.util.Iterator; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * Die Klasse Scanner enthaelt Methoden, um fuer eine Klasse zu bestimmen, an welchem Ablageort sie * sich befindet und diesen Ort nach Klassen zu durchsuchen, die eine gegebene Annotation besitzen. * * Der Ort fur Klassen kann ein Java-Archiv (.jar) oder ein Ordner im Dateisystem sein. * * @author Ulrich Hilger * @version 0.1, 30.11.2024 */ public final class Scanner { private final URI path; private final Class annotation; private final Class cls; private final ClassLoader urlCL; /** * Einen Scanner erzeugen, der den Ort, in dem sich eine gegebene Klasse befindet, nach Klassen * durchsucht, die eine bestimmte Annotation besitzen * * Der Ort fur Klassen kann ein Java-Archiv (.jar) oder ein Ordner im Dateisystem sein. * * @param c eine Klasse die sich im Archiv befindet, das durchsucht werden soll * @param annotation die Annotation, nach der gesucht wird */ public Scanner(Class c, Class annotation) { this.annotation = annotation; this.cls = c; this.urlCL = getUrlClassLoader(cls); this.path = getPath(c); } public Class getAnnotation() { return annotation; } /** * Klassen suchen, die die dem Konstruktor gegebene Annotation besitzen. * * Anhand der im Konstruktor uebergebenen Klasse wird deren Ablageort ermittelt, entweder ein * Ordner im Dateisystem oder ein Java-Archiv (.jar). Dieser Ablageort wird dann nach annotierten * Klassen durchsucht. Gefundene Klassen werden dem Listener gemeldet. * * @param packageName Name der Package, die einschl. Unterpackages durchsucht wird, nur Klassen * dieser Package und ihrer Unterpackages werden geladen und auf die Anotation ueberprueft * @param l ein Objekt, das verstaendigt wird, wenn eine annotierte Klasse gefunden wurde * @param h der Handler, dem die gefundene Klasse hinzugefuegt werden soll * @param contextName Name des Kontext, dem gefundene Klassen hinzugefuegt werden sollen */ public void process(ScannerListener l, String packageName, Handler h, String contextName) { if (isJar()) { processZipContent(packageName, l, h, contextName); } else { processClasses(l, packageName, h, contextName); } } /** * Den Inhalt einer Jar-Datei nach Klassen durchsuchen, die die dem Konstruktor gegebene * Annotation besitzen. * * @param packageName Name der Package, die einschl. Unterpackages durchsucht wird, nur Klassen * dieser Package und ihrer Unterpackages werden geladen und auf die Anotation ueberprueft * @param l ein Objekt, das verstaendigt wird, wenn eine annotierte Klasse gefunden wurde * @param h der Handler, dem die gefundene Klasse hinzugefuegt werden soll * @param contextName Name des Kontext, dem gefundene Klassen hinzugefuegt werden sollen */ private void processZipContent(String packageName, ScannerListener l, Handler h, String contextName) { try { ZipFile zipfile = new ZipFile(new File(path)); Enumeration en = zipfile.entries(); //ClassLoader cl = getUrlClassLoader(cls); while (en.hasMoreElements()) { ZipEntry zipentry = (ZipEntry) en.nextElement(); if (!zipentry.isDirectory()) { processZipEntry(zipentry, packageName, l, h, contextName); } else { // ZIP-Dir muss nicht bearbeitet werden } } } catch (IOException ex) { log(Level.SEVERE, ex.getLocalizedMessage()); } } @SuppressWarnings("unchecked") private void processZipEntry(ZipEntry zipentry, String packageName, ScannerListener l, Handler h, String contextName) { finest(zipentry.getName()); String zName = zipentry.getName(); if (zName.toLowerCase().endsWith(".class")) { int pos = zName.indexOf(".class"); String fullClassName = zName.substring(0, pos); finest("full class name: " + zName); String fullClassNameDots = fullClassName.replace('/', '.'); finest("full class name dots: " + fullClassNameDots); String pkgName = getPackageName(fullClassNameDots); finest(" -- package name: " + pkgName); if (null != urlCL && pkgName.toLowerCase().startsWith(packageName)) { try { Class c = urlCL.loadClass(fullClassNameDots); if (c != null) { if (c.isAnnotationPresent(annotation)) { finest(" ---- ACTOR ---- " + fullClassNameDots); l.annotationFound(c, h, contextName); } else { finest("kein Actor " + fullClassNameDots); } } else { finest("class NOT loaded: " + zName); } } catch (ClassNotFoundException ex) { finest(" +++++ Class not found: " + ex.getMessage()); } } } } /** * Einen Ordner mit Klassen durchsuchen * * @param packageName Name der Package, die einschl. Unterpackages durchsucht wird, nur Klassen * dieser Package und ihrer Unterpackages werden geladen und auf die Anotation ueberprueft * @param l ein Objekt, das verstaendigt wird, wenn eine annotierte Klasse gefunden wurde * @param h der Handler, dem die gefundene Klasse hinzugefuegt werden soll * @param contextName Name des Kontext, dem gefundene Klassen hinzugefuegt werden sollen */ @SuppressWarnings("unchecked") private void processClasses(ScannerListener l, String packageName, Handler h, String contextName) { ClassLoader cl = getCl(); 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(getAnnotation())) { //wire(h, actorClass, contextName); l.annotationFound(actorClass, h, contextName); } } catch (ClassNotFoundException ex) { // Klasse nicht gefunden. Muss das geloggt oder sonstwie behandel werden? } } else { //wireActors(js, packageName + "." + line, h, contextName); processClasses(l, packageName + "." + line, h, contextName); } } } private String getPackageName(String fullClassName) { String packageName; int pos = fullClassName.lastIndexOf("."); if (pos > 0) { packageName = fullClassName.substring(0, pos); } else { packageName = fullClassName; } return packageName; } public ClassLoader getCl() { return cls.getClassLoader(); } public ClassLoader getUrlClassLoader(Class c) { ClassLoader cl = null; try { URL url = getPath(c).toURL(); finer("url: " + url.getPath()); cl = new URLClassLoader(new URL[]{url}); } catch (MalformedURLException ex) { log(Level.SEVERE, ex.getMessage()); } finally { return cl; } } public String getPathStr() { if (path != null) { return path.toString(); } else { return ""; } } public boolean isJar() { return !getPathStr().toLowerCase().endsWith(".class"); } private URI getPath(Class c) { String className = c.getName(); finest("this name: " + className); String classNameWoPkg = c.getSimpleName();//className.substring(className.lastIndexOf(".") + 1); finest("Class name: " + classNameWoPkg); String classPath = c.getResource(classNameWoPkg + ".class").getPath(); int pos = classPath.indexOf("!"); String jarPath; if (pos > -1) { jarPath = /*"jar:" + */ classPath.substring(0, pos); } else { jarPath = classPath; } finest("path: " + jarPath); try { return new URI(jarPath); } catch (URISyntaxException ex) { Logger.getLogger(Scanner.class.getName()).log(Level.SEVERE, ex.getMessage(), ex); return null; } } private void finest(String msg) { log(Level.FINEST, msg); } private void finer(String msg) { log(Level.FINER, msg); } private void log(Level l, String msg) { Logger.getLogger(Scanner.class.getName()).log(l, msg); } public interface ScannerListener { public void annotationFound(Class foundClass, Handler h, String contextName); } }