Address @XmlSeeAlso limitation. Provide the ability to inject the JAXB types
[controller.git] / opendaylight / northbound / bundlescanner / implementation / src / main / java / org / opendaylight / controller / northbound / bundlescanner / internal / BundleScanner.java
diff --git a/opendaylight/northbound/bundlescanner/implementation/src/main/java/org/opendaylight/controller/northbound/bundlescanner/internal/BundleScanner.java b/opendaylight/northbound/bundlescanner/implementation/src/main/java/org/opendaylight/controller/northbound/bundlescanner/internal/BundleScanner.java
new file mode 100644 (file)
index 0000000..3e517e9
--- /dev/null
@@ -0,0 +1,277 @@
+/**
+ * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.northbound.bundlescanner.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+import org.opendaylight.controller.northbound.bundlescanner.IBundleScanService;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.SynchronousBundleListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The custom bundle scanner scans annotations on bundles and is used for
+ * constructing JAXBContext instances. It listens for bundle events and updates
+ * the metadata in realtime.
+ */
+/*package*/ class BundleScanner implements SynchronousBundleListener {
+    private static final Logger LOGGER = LoggerFactory.getLogger(BundleScanner.class);
+    private static BundleScanner INSTANCE; // singleton
+
+    private final Pattern annotationPattern;
+    private final Map<Long,BundleInfo> bundleAnnotations =
+            new HashMap<Long, BundleInfo>();
+
+    public static synchronized BundleScanner getInstance() {
+        if (INSTANCE == null) {
+            INSTANCE = new BundleScanner();
+        }
+        return INSTANCE;
+    }
+
+    /*package*/ BundleScanner(Bundle[] bundles) {
+        annotationPattern = mergePatterns(IBundleScanService.ANNOTATIONS_TO_SCAN, true);
+        init(bundles);
+    }
+
+    /*package*/ BundleScanner() {
+        this(FrameworkUtil.getBundle(BundleScanner.class).getBundleContext().getBundles());
+    }
+
+    public List<Class<?>> getAnnotatedClasses(BundleContext context,
+            String[] annotations,
+            boolean includeDependentBundleClasses)
+    {
+        BundleInfo info = bundleAnnotations.get(context.getBundle().getBundleId());
+        if (info == null) return Collections.emptyList();
+        Pattern pattern = mergePatterns(annotations, false);
+        List<Class<?>> result = null;
+        if (includeDependentBundleClasses) {
+            result = info.getAnnotatedClasses(bundleAnnotations.values(), pattern);
+        } else {
+            result = info.getAnnotatedClasses(pattern);
+        }
+        LOGGER.debug("Annotated classes detected: {} matching: {}", result, pattern);
+        return result;
+    }
+
+    ////////////////////////////////////////////////////////////////
+    // SynchronousBundleListener implementation
+    ////////////////////////////////////////////////////////////////
+
+    @Override
+    public void bundleChanged(BundleEvent event) {
+        Bundle bundle = event.getBundle();
+        long id = bundle.getBundleId();
+        switch(event.getType()) {
+            case BundleEvent.RESOLVED :
+                scan(bundle);
+                return;
+            case BundleEvent.UNRESOLVED :
+            case BundleEvent.UNINSTALLED :
+                bundleAnnotations.remove(id);
+                return;
+        }
+    }
+
+
+    ////////////////////////////////////////////////////////////////
+    //  ClassVisitor implementation
+    ////////////////////////////////////////////////////////////////
+
+    private static class AnnotationDetector extends ClassVisitor {
+        private final Map<String, Set<String>> matchedClasses =
+                new HashMap<String, Set<String>>();
+
+        private final Pattern annotationsPattern;
+        private Set<String> annotations;
+        private String className;
+        private boolean accessible;
+        private boolean matchedAnnotation;
+
+        public AnnotationDetector(Pattern pattern) {
+            super(Opcodes.ASM4);
+            this.annotationsPattern = pattern;
+        }
+
+        public Map<String, Set<String>> getMatchedClasses() {
+            return new HashMap<String, Set<String>>(matchedClasses);
+        }
+
+        @Override
+        public void visit(int version, int access, String name, String signature,
+                String superName, String[] interfaces)
+        {
+            //LOGGER.debug("Visiting class:" + name);
+            className = name;
+            accessible = ((access & Opcodes.ACC_PUBLIC) == Opcodes.ACC_PUBLIC);
+            matchedAnnotation = false;
+            annotations = new HashSet<String>();
+        }
+
+        @Override
+        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+            //LOGGER.debug("Visiting annotation:" + desc);
+            annotations.add(signature2class(desc));
+            if (!matchedAnnotation) {
+                matchedAnnotation = (annotationsPattern == null ||
+                        annotationsPattern.matcher(desc).find());
+            }
+            return null;
+        }
+
+        @Override
+        public void visitEnd() {
+            if (matchedAnnotation && accessible) {
+                className = path2class(className);
+                matchedClasses.put(className, new HashSet<String>(annotations));
+            }
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////
+    // Helpers
+    ////////////////////////////////////////////////////////////////
+
+    private synchronized void init(Bundle[] bundles) {
+        for (Bundle bundle : bundles) {
+            int state = bundle.getState();
+            if (state == Bundle.RESOLVED ||
+                state == Bundle.STARTING ||
+                state == Bundle.ACTIVE)
+            {
+                scan(bundle);
+            }
+        }
+    }
+
+    private static String path2class(String path) {
+        return path.replace(".class", "").replaceAll("/", ".");
+    }
+
+    private static String class2path(String clz) {
+        return clz.replaceAll("\\.", "/");
+    }
+
+    @SuppressWarnings("unused")
+    private static String class2signature(String clz) {
+        return "L" + class2path(clz) + ";";
+    }
+
+    private static String signature2class(String sig) {
+        if (sig.startsWith("L") && sig.endsWith(";")) {
+            sig = sig.substring(1, sig.length()-1);
+        }
+        return path2class(sig);
+    }
+
+   private static List<URL> getBundleClasses(Bundle bundle, String[] pkgs) {
+        List<URL> result = new ArrayList<URL>();
+        boolean recurse = false;
+        if (pkgs == null) {
+            recurse = true;
+            pkgs = new String[] { "/" } ;
+        }
+        for (String pkg : pkgs) {
+            pkg = class2path(pkg);
+            final Enumeration<URL> e = bundle.findEntries(pkg, "*.class", recurse);
+            if (e != null) {
+                while (e.hasMoreElements()) {
+                    URL url = e.nextElement();
+                    result.add(url);
+                }
+            }
+        }
+        return result;
+    }
+
+    private synchronized void scan(Bundle bundle) {
+        AnnotationDetector detector = new AnnotationDetector(annotationPattern);
+        try {
+            for (URL u : getBundleClasses(bundle, null)) {
+                InputStream is = u.openStream();
+                new ClassReader(is).accept(detector, 0);
+                is.close();
+            }
+        } catch (IOException ioe) {
+            LOGGER.error("Error scanning classes in bundle: {}", bundle.getSymbolicName(), ioe);
+        }
+        Map<String, Set<String>> classes = detector.getMatchedClasses();
+        if (classes != null && classes.size() > 0) {
+            BundleInfo info = new BundleInfo(bundle, classes);
+            bundleAnnotations.put(bundle.getBundleId(),info);
+            if (LOGGER.isDebugEnabled()) {
+                LOGGER.debug("bindings found in bundle: {}[{}] " +
+                        "dependencies {} classes {}", bundle.getSymbolicName(),
+                        bundle.getBundleId(),
+                        info.getDependencies(bundleAnnotations.values()),
+                        classes);
+            }
+        }
+        // find bundle dependencies
+    }
+
+    public static List<Class<?>> loadClasses(Bundle bundle,
+            Collection<String> annotatedClasses)
+    {
+        List<Class<?>> result = new ArrayList<Class<?>>();
+        for (String name : annotatedClasses) {
+            try {
+                result.add(bundle.loadClass(name));
+            } catch (Exception e) {
+                LOGGER.error("Unable to load class: {}", name, e);
+            }
+        }
+        return result;
+    }
+
+    public static Pattern mergePatterns(String[] patterns, boolean convert2signature) {
+        if (patterns == null || patterns.length == 0) {
+            return null;
+        }
+        StringBuilder regex = new StringBuilder();
+        for (String c : patterns) {
+            if (c.endsWith("*")) {
+                c = c.substring(0, c.length() - 1);
+            }
+            if (regex.length() > 0) regex.append("|");
+            regex.append("^");
+            if (convert2signature) {
+                regex.append("L").append(c.replaceAll("\\.", "/"));
+            } else {
+                regex.append(c);
+            }
+        }
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Merged regex: [{}]", regex.toString());
+        }
+        return Pattern.compile(regex.toString());
+    }
+
+}