Ultrakompakter HTTP Server
1 files deleted
1 files added
1 files modified
627 ■■■■■ changed files
src/de/uhilger/neon/Factory.java 203 ●●●●● patch | view | raw | blame | history
src/de/uhilger/neon/JarScanner.java 156 ●●●●● patch | view | raw | blame | history
src/de/uhilger/neon/Scanner.java 268 ●●●●● patch | view | raw | blame | history
src/de/uhilger/neon/Factory.java
@@ -23,7 +23,6 @@
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;
@@ -48,6 +47,7 @@
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
@@ -65,7 +65,7 @@
 * @author Ulrich Hilger
 * @version 1, 6.2.2024
 */
public class Factory implements JarScannerListener {
public class Factory implements ScannerListener {
  public Factory() {
    listeners = new ArrayList<>();
@@ -94,21 +94,23 @@
    Gson gson = new Gson();
    return gson.fromJson(sb.toString(), NeonDescriptor.class);
  }
  public void runInstance(Class c, NeonDescriptor d)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  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,
  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
@@ -121,9 +123,12 @@
   * @throws InvocationTargetException
   * @throws IOException
   */
  public void runInstance(Class c, NeonDescriptor d, List<String> packageNames, List<DataProvider> sdp)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  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()) {
@@ -131,40 +136,40 @@
      HttpServer server = HttpServer.create(new InetSocketAddress(sd.port), 0);
      fireServerCreated(server);
      if(packageNames == null) {
      if (packageNames == null) {
        packageNames = d.actorPackages;
      }
      addContexts(c, d, server, sd.contexts, packageNames, sdp);
      }
      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) {
    if (d.authenticator != null) {
      try {
        Object authObj = Class.forName(d.authenticator.className)
                .getDeclaredConstructor().newInstance();
        if(authObj instanceof Authenticator) {
        if (authObj instanceof Authenticator) {
          auth = (Authenticator) authObj;
          return auth;
        }
      } catch (ClassNotFoundException | NoSuchMethodException | SecurityException |
              InstantiationException | IllegalAccessException | IllegalArgumentException |
              InvocationTargetException ex) {
      } 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<String> packageNames,
          List<DataProvider> sdp)
          throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  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();
@@ -173,45 +178,43 @@
      ContextDescriptor cd = contextIterator.next();
      HttpHandler h = buildHandler(cd, sharedHandlers);
      if (h != null) {
        HttpContext ctx = server.createContext(cd.contextPath, h);
        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) {
        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);
            scn.process(this, packageName, (Handler) h, cd.attributes.get("contextName"));
            ctx.getAttributes().put("serverDataProviderList", sdp);
          }
        }
        if(cd.authenticator instanceof String) {
          if(!(auth instanceof Authenticator)) {
        }
        if (cd.authenticator instanceof String) {
          if (!(auth instanceof Authenticator)) {
            auth = createAuthenticator(d);
          }
          if(auth instanceof Authenticator) {
            ctx.setAuthenticator(auth);
          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) {
        if (cd.filter != null) {
          for (String filterClassName : cd.filter) {
            //
            Object filterObj = Class.forName(filterClassName)
                  .getDeclaredConstructor().newInstance();
            if(filterObj instanceof Filter) {
                    .getDeclaredConstructor().newInstance();
            if (filterObj instanceof Filter) {
              Filter filter = (Filter) filterObj;
              ctx.getFilters().add(filter);
            }
@@ -224,10 +227,10 @@
      }
    }
  }
  private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
  private HttpHandler buildHandler(ContextDescriptor cd, Map<String, HttpHandler> sharedHandlers) throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
          IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
    HttpHandler h;
    HttpHandler h;
    if (!cd.sharedHandler) {
      h = getHandlerInstance(cd);
    } else {
@@ -241,7 +244,7 @@
    }
    return h;
  }
  private HttpHandler getHandlerInstance(ContextDescriptor cd) {
    try {
      Object handlerObj = Class.forName(cd.className)
@@ -252,50 +255,72 @@
        // kein HttpHandler aus newInstance
        return null;
      }
    } catch (ClassNotFoundException | NoSuchMethodException | SecurityException |
            InstantiationException | IllegalAccessException | IllegalArgumentException |
            InvocationTargetException ex) {
    } 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?
  /**
   * 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");
            }
          } else {
            wireActors(c, packageName + "." + line, annotation, h, contextName);
          } catch (ClassNotFoundException ex) {
            Logger.getLogger(Factory.class.getName()).log(Level.FINER, "Klasse nicht gefunden");
          }
        } else {
          listClasses(c, packageName + "." + line);
        }
      } 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);
    } 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 
@@ -304,7 +329,13 @@
    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) {
@@ -319,7 +350,6 @@
  }
  /* -------------- FactoryListener Implementierung --------------- */
  private List<FactoryListener> listeners;
  public void addListener(FactoryListener l) {
@@ -340,7 +370,7 @@
      l.serverCreated(server);
    }
  }
  private void fireHandlerCreated(HttpContext ctx, HttpHandler h) {
    for (FactoryListener l : listeners) {
      l.handlerCreated(ctx, h);
@@ -352,7 +382,7 @@
      l.contextCreated(context);
    }
  }
  private void fireAuthenticatorCreated(HttpContext context, Authenticator auth) {
    for (FactoryListener l : listeners) {
      l.authenticatorCreated(context, auth);
@@ -365,10 +395,9 @@
    }
  }
  /* -------------- JarScannerListener Implementierung --------------- */
  /* -------------- ScannerListener Implementierung --------------- */
  @Override
  public void actorFound(Class actorClass, Handler h, String contextName) {
    wire(h, actorClass, contextName);
  public void annotationFound(Class foundClass, Handler h, String contextName) {
    wire(h, foundClass, contextName);
  }
}
}
src/de/uhilger/neon/JarScanner.java
File was deleted
src/de/uhilger/neon/Scanner.java
New file
@@ -0,0 +1,268 @@
/*
  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 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);
  }
}