package org.ovirt.engine.api.restapi.types; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; /** * Discovers and manages type mappers. */ public class MappingLocator { private String discoverPackageName; private Map<ClassPairKey, Mapper<?, ?>> mappers; /** * Normal constructor used when injected */ public MappingLocator() { mappers = new HashMap<ClassPairKey, Mapper<?, ?>>(); } /** * Constructor intended only for testing. * * @param discoverPackageName * package to look under */ MappingLocator(String discoverPackageName) { this.discoverPackageName = discoverPackageName; mappers = new HashMap<ClassPairKey, Mapper<?, ?>>(); } /** * Discover mappers and populate internal registry. The classloading * environment is scanned for classes contained under the * org.ovirt.engine.api.restapi.types package and exposing methods decorated * with the @Mapping annotation. */ public void populate() { List<Class<?>> classes = discoverClasses(discoverPackageName != null ? discoverPackageName : this.getClass().getPackage().getName()); for (Class<?> clz : classes) { for (Method method : clz.getMethods()) { Mapping mapping = method.getAnnotation(Mapping.class); if (mapping != null) { mappers.put(new ClassPairKey(mapping.from(), mapping.to()), new MethodInvokerMapper(method, mapping.to())); } } } } /** * Get an appropriate mapper mediating between the required types. * * @param <F> * the from type * @param <T> * the to type * @param from * the from class * @param to * the to class * @return a mapped instance of the to type */ @SuppressWarnings("unchecked") public <F, T> Mapper<F, T> getMapper(Class<F> from, Class<T> to) { return (Mapper<F, T>) mappers.get(new ClassPairKey(from, to)); } /** * Discover classes under target package. * * @param packageName * package to look under * @return list of classes found */ private List<Class<?>> discoverClasses(String packageName) { List<Class<?>> ret = new ArrayList<Class<?>>(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try { Enumeration<URL> resources = classLoader.getResources(toPath(packageName)); List<File> dirs = new ArrayList<File>(); List<InputStream> jars = new ArrayList<InputStream>(); while (resources.hasMoreElements()) { URL resource = resources.nextElement(); if (isJar(resource)) { jars.add(new FileInputStream(getJarName(resource))); } else if (containsJar(resource)) { jars.add(getContainingResource(classLoader, resource)); } else { dirs.add(new File(resource.getFile())); } } walkJars(ret, packageName, jars); walkDirs(ret, packageName, dirs); } catch (Exception e) { ret = Collections.emptyList(); } return ret; } private static InputStream getContainingResource(ClassLoader classLoader, URL resource) throws Exception { InputStream ret = null; Enumeration<URL> globals = classLoader.getResources("/"); while (globals.hasMoreElements()) { URL global = globals.nextElement(); if (resource.toString().startsWith(global.toString())) { ret = global.openStream(); break; } } return ret; } private void walkJars(List<Class<?>> classList, String packageName, List<InputStream> jars) throws Exception { for (InputStream jar : jars) { JarInputStream jarFile = null; try { jarFile = new JarInputStream(jar); JarEntry entry = null; while ((entry = jarFile.getNextJarEntry()) != null) { String name = toPackage(entry.getName()); if (name.startsWith(packageName) && isClass(name)) { classList.add(Class.forName(trimClass(name))); } } } finally { if (jarFile != null) { jarFile.close(); } } } } private void walkDirs(List<Class<?>> classList, String packageName, List<File> dirs) throws Exception { for (File directory : dirs) { List<Class<?>> classes = new ArrayList<Class<?>>(); if (directory.exists()) { File[] files = directory.listFiles(); for (File file : files) { if (file.isDirectory()) { classes.addAll(getClassesUnder(file, in(packageName, file.getName()))); } else if (isClass(file.getName())) { classes.add(Class.forName(in(packageName, trimClass(file.getName())))); } } classList.addAll(getClassesUnder(directory, packageName)); } } } private List<Class<?>> getClassesUnder(File directory, String packageName) throws ClassNotFoundException { List<Class<?>> classes = new ArrayList<Class<?>>(); if (!directory.exists()) { return classes; } File[] files = directory.listFiles(); for (File file : files) { if (file.isDirectory()) { classes.addAll(getClassesUnder(file, in(packageName, file.getName()))); } else if (isClass(file.getName())) { classes.add(Class.forName(in(packageName, trimClass(file.getName())))); } } return classes; } private static String toPath(String packageName) { return packageName.replace('.', '/'); } private static String toPackage(String path) { return path.replaceAll("/", "\\."); } private static String in(String packageName, String entry) { return packageName + '.' + entry; } private static String getJarName(URL resource) { return resource.getPath().split("!")[0].substring("file:".length()); } private static boolean isJar(URL resource) { return resource.getProtocol().equals("jar"); } private static boolean containsJar(URL resource) { return resource.getPath().indexOf(".jar") != -1; } private static boolean isClass(String s) { return s.endsWith(".class"); } private static String trimClass(String s) { return s.substring(0, s.length() - 6); } private static class ClassPairKey { private Class<?> from, to; private ClassPairKey(Class<?> from, Class<?> to) { this.from = from; this.to = to; } public int hashCode() { return to.hashCode() + from.hashCode(); } public boolean equals(Object other) { if (other == this) { return true; } else if (other instanceof ClassPairKey) { ClassPairKey key = (ClassPairKey) other; return to == key.to && from == key.from; } return false; } public String toString() { return "map from: " + from + " to: " + to; } } private static class MethodInvokerMapper implements Mapper<Object, Object> { private Method method; private Class<?> to; private MethodInvokerMapper(Method method, Class<?> to) { this.method = method; this.to = to; } @Override public Object map(Object from, Object template) { Object ret = null; try { // REVISIT support non-static mapping methods also ret = method.invoke(null, from, template); } catch (Exception e) { // REVISIT logging, fallback null-mapping e.printStackTrace(); } return to.cast(ret); } public String toString() { return "map to: " + to + " via " + method; } } }