// Copyright (C) 2014 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.gerrit.server.plugins; import static com.google.common.base.Objects.firstNonNull; import static com.google.common.collect.Iterables.transform; import com.google.common.base.Function; import com.google.common.base.Optional; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.eclipse.jgit.util.IO; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Attribute; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.Map; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; public class JarScanner implements PluginContentScanner { private static final int SKIP_ALL = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; private static final Function<ClassData, ExtensionMetaData> CLASS_DATA_TO_EXTENSION_META_DATA = new Function<ClassData, ExtensionMetaData>() { @Override public ExtensionMetaData apply(ClassData classData) { return new ExtensionMetaData(classData.className, classData.annotationValue); } }; private final JarFile jarFile; public JarScanner(File srcFile) throws InvalidPluginException { try { this.jarFile = new JarFile(srcFile); } catch (IOException e) { throw new InvalidPluginException("Cannot scan plugin file " + srcFile, e); } } @Override public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan( String pluginName, Iterable<Class<? extends Annotation>> annotations) throws InvalidPluginException { Set<String> descriptors = Sets.newHashSet(); Multimap<String, JarScanner.ClassData> rawMap = ArrayListMultimap.create(); Map<Class<? extends Annotation>, String> classObjToClassDescr = Maps.newHashMap(); for (Class<? extends Annotation> annotation : annotations) { String descriptor = Type.getType(annotation).getDescriptor(); descriptors.add(descriptor); classObjToClassDescr.put(annotation, descriptor); } Enumeration<JarEntry> e = jarFile.entries(); while (e.hasMoreElements()) { JarEntry entry = e.nextElement(); if (skip(entry)) { continue; } ClassData def = new ClassData(descriptors); try { new ClassReader(read(jarFile, entry)).accept(def, SKIP_ALL); } catch (IOException err) { throw new InvalidPluginException("Cannot auto-register", err); } catch (RuntimeException err) { PluginLoader.log.warn(String.format( "Plugin %s has invaild class file %s inside of %s", pluginName, entry.getName(), jarFile.getName()), err); continue; } if (def.isConcrete()) { if (!Strings.isNullOrEmpty(def.annotationName)) { rawMap.put(def.annotationName, def); } } else { PluginLoader.log.warn(String.format( "Plugin %s tries to @%s(\"%s\") abstract class %s", pluginName, def.annotationName, def.annotationValue, def.className)); } } ImmutableMap.Builder<Class<? extends Annotation>, Iterable<ExtensionMetaData>> result = ImmutableMap.builder(); for (Class<? extends Annotation> annotoation : annotations) { String descr = classObjToClassDescr.get(annotoation); Collection<ClassData> discoverdData = rawMap.get(descr); Collection<ClassData> values = firstNonNull(discoverdData, Collections.<ClassData> emptySet()); result.put(annotoation, transform(values, CLASS_DATA_TO_EXTENSION_META_DATA)); } return result.build(); } private static boolean skip(JarEntry entry) { if (!entry.getName().endsWith(".class")) { return true; // Avoid non-class resources. } if (entry.getSize() <= 0) { return true; // Directories have 0 size. } if (entry.getSize() >= 1024 * 1024) { return true; // Do not scan huge class files. } return false; } private static byte[] read(JarFile jarFile, JarEntry entry) throws IOException { byte[] data = new byte[(int) entry.getSize()]; InputStream in = jarFile.getInputStream(entry); try { IO.readFully(in, data, 0, data.length); } finally { in.close(); } return data; } public static class ClassData extends ClassVisitor { int access; String className; String annotationName; String annotationValue; Iterable<String> exports; private ClassData(Iterable<String> exports) { super(Opcodes.ASM4); this.exports = exports; } boolean isConcrete() { return (access & Opcodes.ACC_ABSTRACT) == 0 && (access & Opcodes.ACC_INTERFACE) == 0; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.className = Type.getObjectType(name).getClassName(); this.access = access; } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { Optional<String> found = Iterables.tryFind(exports, Predicates.equalTo(desc)); if (visible && found.isPresent()) { annotationName = desc; return new AbstractAnnotationVisitor() { @Override public void visit(String name, Object value) { annotationValue = (String) value; } }; } return null; } @Override public void visitSource(String arg0, String arg1) { } @Override public void visitOuterClass(String arg0, String arg1, String arg2) { } @Override public MethodVisitor visitMethod(int arg0, String arg1, String arg2, String arg3, String[] arg4) { return null; } @Override public void visitInnerClass(String arg0, String arg1, String arg2, int arg3) { } @Override public FieldVisitor visitField(int arg0, String arg1, String arg2, String arg3, Object arg4) { return null; } @Override public void visitEnd() { } @Override public void visitAttribute(Attribute arg0) { } } private static abstract class AbstractAnnotationVisitor extends AnnotationVisitor { AbstractAnnotationVisitor() { super(Opcodes.ASM4); } @Override public AnnotationVisitor visitAnnotation(String arg0, String arg1) { return null; } @Override public AnnotationVisitor visitArray(String arg0) { return null; } @Override public void visitEnum(String arg0, String arg1, String arg2) { } @Override public void visitEnd() { } } @Override public Optional<PluginEntry> getEntry(String resourcePath) throws IOException { JarEntry jarEntry = jarFile.getJarEntry(resourcePath); if (jarEntry == null || jarEntry.getSize() == 0) { return Optional.absent(); } return Optional.of(resourceOf(jarEntry)); } @Override public Enumeration<PluginEntry> entries() { return Collections.enumeration(Lists.transform( Collections.list(jarFile.entries()), new Function<JarEntry, PluginEntry>() { public PluginEntry apply(JarEntry jarEntry) { try { return resourceOf(jarEntry); } catch (IOException e) { throw new IllegalArgumentException("Cannot convert jar entry " + jarEntry + " to a resource", e); } } })); } @Override public InputStream getInputStream(PluginEntry entry) throws IOException { return jarFile.getInputStream(jarFile .getEntry(entry.getName())); } @Override public Manifest getManifest() throws IOException { return jarFile.getManifest(); } private PluginEntry resourceOf(JarEntry jarEntry) throws IOException { return new PluginEntry(jarEntry.getName(), jarEntry.getTime(), Optional.of(jarEntry.getSize()), attributesOf(jarEntry)); } private Map<Object, String> attributesOf(JarEntry jarEntry) throws IOException { Attributes attributes = jarEntry.getAttributes(); if (attributes == null) { return Collections.emptyMap(); } return Maps.transformEntries(attributes, new Maps.EntryTransformer<Object, Object, String>() { @Override public String transformEntry(Object key, Object value) { return (String) value; } }); } }