All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.builder.testing.MockableJarGenerator Maven / Gradle / Ivy

There is a newer version: 2.3.0
Show newest version
/*
 * 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.android.builder.testing;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
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.FieldNode;
import org.objectweb.asm.tree.InnerClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;

/**
 * Given a "standard" android.jar, creates a "mockable" version, where all classes and methods
 * are not final. Optionally makes all methods return "default" values, instead of throwing the
 * infamous "Stub!" exceptions.
 *
 * 

ATTENTION! If you change this class, please update the gradle tasks to reflect that in the * generated file name (e.g. add a "v2"). This will force Gradle to re-generate the jars in * existing projects. */ public class MockableJarGenerator { private static final int EMPTY_FLAGS = 0; private static final String CONSTRUCTOR = ""; private static final String CLASS_CONSTRUCTOR = ""; private static final ImmutableSet ENUM_METHODS = ImmutableSet.of( CLASS_CONSTRUCTOR, "valueOf", "values"); private static final ImmutableSet INTEGER_LIKE_TYPES = ImmutableSet.of( Type.INT_TYPE, Type.BYTE_TYPE, Type.BOOLEAN_TYPE, Type.CHAR_TYPE, Type.SHORT_TYPE); private final boolean returnDefaultValues; private final ImmutableSet prefixesToSkip = ImmutableSet.of( "java.", "javax.", "org.xml.", "org.w3c.", "junit.", "org.apache.commons.logging"); public MockableJarGenerator(boolean returnDefaultValues) { this.returnDefaultValues = returnDefaultValues; } public void createMockableJar(File input, File output) throws IOException { Preconditions.checkState( output.createNewFile(), "Output file [%s] already exists.", output.getAbsolutePath()); JarFile androidJar = null; JarOutputStream outputStream = null; try { androidJar = new JarFile(input); outputStream = new JarOutputStream(new FileOutputStream(output)); for (JarEntry entry : Collections.list(androidJar.entries())) { InputStream inputStream = androidJar.getInputStream(entry); if (entry.getName().endsWith(".class")) { if (!skipClass(entry.getName().replace("/", "."))) { rewriteClass(entry, inputStream, outputStream); } } else { outputStream.putNextEntry(entry); ByteStreams.copy(inputStream, outputStream); } inputStream.close(); } } finally { if (androidJar != null) { androidJar.close(); } if (outputStream != null) { outputStream.close(); } } } private boolean skipClass(String className) { for (String prefix : prefixesToSkip) { if (className.startsWith(prefix)) { return true; } } return false; } /** * Writes a modified *.class file to the output JAR file. */ private void rewriteClass( JarEntry entry, InputStream inputStream, JarOutputStream outputStream) throws IOException { ClassReader classReader = new ClassReader(inputStream); ClassNode classNode = new ClassNode(Opcodes.ASM5); classReader.accept(classNode, EMPTY_FLAGS); modifyClass(classNode); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); classNode.accept(classWriter); outputStream.putNextEntry(new ZipEntry(entry.getName())); outputStream.write(classWriter.toByteArray()); } /** * Modifies a {@link ClassNode} to clear final flags and rewrite byte code. */ @SuppressWarnings("unchecked") private void modifyClass(ClassNode classNode) { // Make the class not final. classNode.access &= ~Opcodes.ACC_FINAL; List methodNodes = classNode.methods; for (MethodNode methodNode : methodNodes) { methodNode.access &= ~Opcodes.ACC_FINAL; fixMethodBody(methodNode, classNode); } List fieldNodes = classNode.fields; for (FieldNode fieldNode : fieldNodes) { // Make public instance fields non-final. This is needed e.g. to "mock" SyncResult.stats. if ((fieldNode.access & Opcodes.ACC_PUBLIC) != 0 && (fieldNode.access & Opcodes.ACC_STATIC) == 0) { fieldNode.access &= ~Opcodes.ACC_FINAL; } } List innerClasses = classNode.innerClasses; for (InnerClassNode innerClassNode : innerClasses) { innerClassNode.access &= ~Opcodes.ACC_FINAL; } } /** * Rewrites the method bytecode to remove the "Stub!" exception. */ private void fixMethodBody(MethodNode methodNode, ClassNode classNode) { if ((methodNode.access & Opcodes.ACC_NATIVE) != 0 || (methodNode.access & Opcodes.ACC_ABSTRACT) != 0) { // Abstract and native method don't have bodies to rewrite. return; } if ((classNode.access & Opcodes.ACC_ENUM) != 0 && ENUM_METHODS.contains(methodNode.name)) { // Don't break enum classes. return; } Type returnType = Type.getReturnType(methodNode.desc); InsnList instructions = methodNode.instructions; if (methodNode.name.equals(CONSTRUCTOR)) { // Keep the call to parent constructor, delete the exception after that. boolean deadCode = false; for (AbstractInsnNode instruction : instructions.toArray()) { if (!deadCode) { if (instruction.getOpcode() == Opcodes.INVOKESPECIAL) { instructions.insert(instruction, new InsnNode(Opcodes.RETURN)); // Start removing all following instructions. deadCode = true; } } else { instructions.remove(instruction); } } } else { instructions.clear(); if (returnDefaultValues || methodNode.name.equals(CLASS_CONSTRUCTOR)) { if (INTEGER_LIKE_TYPES.contains(returnType)) { instructions.add(new InsnNode(Opcodes.ICONST_0)); } else if (returnType.equals(Type.LONG_TYPE)) { instructions.add(new InsnNode(Opcodes.LCONST_0)); } else if (returnType.equals(Type.FLOAT_TYPE)) { instructions.add(new InsnNode(Opcodes.FCONST_0)); } else if (returnType.equals(Type.DOUBLE_TYPE)) { instructions.add(new InsnNode(Opcodes.DCONST_0)); } else { instructions.add(new InsnNode(Opcodes.ACONST_NULL)); } instructions.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); } else { instructions.insert(throwExceptionsList(methodNode, classNode)); } } } private static InsnList throwExceptionsList(MethodNode methodNode, ClassNode classNode) { try { String runtimeException = Type.getInternalName(RuntimeException.class); Constructor constructor = RuntimeException.class.getConstructor(String.class); InsnList instructions = new InsnList(); instructions.add( new TypeInsnNode(Opcodes.NEW, runtimeException)); instructions.add(new InsnNode(Opcodes.DUP)); String className = classNode.name.replace('/', '.'); instructions.add(new LdcInsnNode("Method " + methodNode.name + " in " + className + " not mocked. " + "See http://g.co/androidstudio/not-mocked for details.")); instructions.add(new MethodInsnNode( Opcodes.INVOKESPECIAL, runtimeException, CONSTRUCTOR, Type.getType(constructor).getDescriptor(), false)); instructions.add(new InsnNode(Opcodes.ATHROW)); return instructions; } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy