com.spotify.missinglink.ClassLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.missinglink.ClassLoader.java

Source

/*
 * Copyright (c) 2015 Spotify AB
 *
 * 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.spotify.missinglink;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;

import com.spotify.missinglink.datamodel.AccessedField;
import com.spotify.missinglink.datamodel.AccessedFieldBuilder;
import com.spotify.missinglink.datamodel.CalledMethod;
import com.spotify.missinglink.datamodel.CalledMethodBuilder;
import com.spotify.missinglink.datamodel.ClassTypeDescriptor;
import com.spotify.missinglink.datamodel.DeclaredClass;
import com.spotify.missinglink.datamodel.DeclaredClassBuilder;
import com.spotify.missinglink.datamodel.DeclaredField;
import com.spotify.missinglink.datamodel.DeclaredFieldBuilder;
import com.spotify.missinglink.datamodel.DeclaredMethod;
import com.spotify.missinglink.datamodel.DeclaredMethodBuilder;
import com.spotify.missinglink.datamodel.MethodDescriptor;
import com.spotify.missinglink.datamodel.MethodDescriptors;
import com.spotify.missinglink.datamodel.TypeDescriptors;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.util.stream.Collectors.toList;

/**
 * Loads a single class from an input stream.
 */
public final class ClassLoader {

    private ClassLoader() {
        // prevent instantiation
    }

    public static DeclaredClass load(InputStream in) throws IOException {
        ClassNode classNode = readClassNode(in);

        Set<ClassTypeDescriptor> parents = readParents(classNode);
        ImmutableSet<DeclaredField> declaredFields = readDeclaredFields(classNode);

        Map<MethodDescriptor, DeclaredMethod> declaredMethods = Maps.newHashMap();
        Set<ClassTypeDescriptor> loadedClasses = new HashSet<>();

        for (MethodNode method : ClassLoader.<MethodNode>uncheckedCast(classNode.methods)) {
            analyseMethod(classNode.name, method, declaredMethods, loadedClasses);
        }

        return new DeclaredClassBuilder().className(TypeDescriptors.fromClassName(classNode.name))
                .methods(ImmutableMap.copyOf(declaredMethods)).parents(ImmutableSet.copyOf(parents))
                .loadedClasses(ImmutableSet.copyOf(loadedClasses)).fields(declaredFields).build();
    }

    private static ClassNode readClassNode(InputStream in) throws IOException {
        final ClassNode classNode = new ClassNode();
        ClassReader reader = new ClassReader(in);
        reader.accept(classNode, 0);
        return classNode;
    }

    private static Set<ClassTypeDescriptor> readParents(ClassNode classNode) {
        final Set<ClassTypeDescriptor> parents = new HashSet<>();
        parents.addAll(ClassLoader.<String>uncheckedCast(classNode.interfaces).stream()
                .map(TypeDescriptors::fromClassName).collect(toList()));
        // java/lang/Object has no superclass
        if (classNode.superName != null) {
            parents.add(TypeDescriptors.fromClassName(classNode.superName));
        }
        return parents;
    }

    private static ImmutableSet<DeclaredField> readDeclaredFields(ClassNode classNode) {
        ImmutableSet.Builder<DeclaredField> fields = new ImmutableSet.Builder<>();

        @SuppressWarnings("unchecked")
        final Iterable<FieldNode> classFields = (Iterable<FieldNode>) classNode.fields;
        for (FieldNode field : classFields) {
            fields.add(new DeclaredFieldBuilder().name(field.name).descriptor(TypeDescriptors.fromRaw(field.desc))
                    .build());
        }
        return fields.build();
    }

    private static void analyseMethod(String className, MethodNode method,
            Map<MethodDescriptor, DeclaredMethod> declaredMethods, Set<ClassTypeDescriptor> loadedClasses) {
        final Set<CalledMethod> thisCalls = new HashSet<>();
        final Set<AccessedField> thisFields = new HashSet<>();

        int lineNumber = 0;
        for (Iterator<AbstractInsnNode> instructions = ClassLoader
                .<AbstractInsnNode>uncheckedCast(method.instructions.iterator()); instructions.hasNext();) {
            try {
                final AbstractInsnNode insn = instructions.next();
                if (insn instanceof LineNumberNode) {
                    lineNumber = ((LineNumberNode) insn).line;
                }
                if (insn instanceof MethodInsnNode) {
                    handleMethodCall(thisCalls, lineNumber, (MethodInsnNode) insn);
                }
                if (insn instanceof FieldInsnNode) {
                    handleFieldAccess(thisFields, lineNumber, (FieldInsnNode) insn);
                }
                if (insn instanceof LdcInsnNode) {
                    handleLdc(loadedClasses, (LdcInsnNode) insn);
                }
            } catch (Exception e) {
                throw new MissingLinkException(
                        "Error analysing " + className + "." + method.name + ", line: " + lineNumber, e);
            }
        }

        final DeclaredMethod declaredMethod = new DeclaredMethodBuilder()
                .descriptor(MethodDescriptors.fromDesc(method.desc, method.name)).lineNumber(lineNumber)
                .methodCalls(ImmutableSet.copyOf(thisCalls)).fieldAccesses(ImmutableSet.copyOf(thisFields))
                .isStatic((method.access & Opcodes.ACC_STATIC) != 0).build();

        if (declaredMethods.put(declaredMethod.descriptor(), declaredMethod) != null) {
            throw new RuntimeException(
                    "Multiple definitions of " + declaredMethod.descriptor() + " in class " + className);
        }
    }

    private static void handleMethodCall(Set<CalledMethod> thisCalls, int lineNumber, MethodInsnNode insn) {
        boolean isStatic;
        switch (insn.getOpcode()) {
        case Opcodes.INVOKEVIRTUAL:
        case Opcodes.INVOKEINTERFACE:
            isStatic = false;
            break;
        case Opcodes.INVOKESPECIAL:
            isStatic = false;
            break;
        case Opcodes.INVOKESTATIC:
            isStatic = true;
            break;
        default:
            throw new RuntimeException("Unexpected method call opcode: " + insn.getOpcode());
        }
        if (insn.owner.charAt(0) != '[') {
            thisCalls.add(new CalledMethodBuilder().owner(TypeDescriptors.fromClassName(insn.owner))
                    .descriptor(MethodDescriptors.fromDesc(insn.desc, insn.name)).isStatic(isStatic)
                    .lineNumber(lineNumber).build());
        }
    }

    private static void handleFieldAccess(Set<AccessedField> thisFields, int lineNumber, FieldInsnNode insn) {
        if (insn.owner.charAt(0) != '[') {
            thisFields.add(new AccessedFieldBuilder().name(insn.name).descriptor(TypeDescriptors.fromRaw(insn.desc))
                    .owner(TypeDescriptors.fromClassName(insn.owner)).lineNumber(lineNumber).build());
        }
    }

    private static void handleLdc(Set<ClassTypeDescriptor> loadedClasses, LdcInsnNode insn) {
        // See http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.ldc
        // if an LDC instruction is emitted with a symbolic reference to a class, that class is
        // loaded. This means we need to at least check for presence of that class, and also
        // validate its static initialisation code, if any. It would probably be safe for some
        // future to ignore other methods defined by the class.
        if (insn.cst instanceof Type) {
            Type type = (Type) insn.cst;

            Type loadedType = type;

            if (type.getSort() == Type.ARRAY) {
                loadedType = type.getElementType();
            }

            if (loadedType.getSort() == Type.OBJECT) {
                loadedClasses.add(TypeDescriptors.fromClassName(loadedType.getInternalName()));
            }
        }
    }

    // asm seems to compile it's code with a very low source version, so all collections from it
    // are unchecked types. These helper functions at least suppress the warnings for us:
    //
    @SuppressWarnings("unchecked")
    private static <T> List<T> uncheckedCast(List list) {
        return (List<T>) list;
    }

    @SuppressWarnings("unchecked")
    private static <T> Iterator<T> uncheckedCast(Iterator iterator) {
        return (Iterator<T>) iterator;
    }
}