/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.felix.ipojo.manipulation;
import org.apache.felix.ipojo.metadata.Attribute;
import org.apache.felix.ipojo.metadata.Element;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.util.CheckClassAdapter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
/**
* iPOJO Byte code Manipulator.
* @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
*
*/
public class Manipulator {
/**
* A classloader used to compute frames.
*/
private final ClassLoader m_classLoader;
/**
* Store the visited fields : [name of the field, type of the field].
*/
private Map<String, String> m_fields;
/**
* Store the interface implemented by the class.
*/
private List<String> m_interfaces;
/**
* Store the methods list.
*/
private List<MethodDescriptor> m_methods;
/**
* Pojo super class.
*/
private String m_superClass;
/**
* List of owned inner classes and internal methods.
*/
private Map<String, List<MethodDescriptor>> m_inners;
/**
* Java byte code version.
*/
private int m_version;
/**
* Was the class already manipulated.
*/
private boolean m_alreadyManipulated;
/**
* The manipulated class name.
*/
private String m_className;
public Manipulator(ClassLoader loader) {
// No classloader set, use current one.
m_classLoader = loader;
}
/**
* Checks the given bytecode, determines if the class was already manipulated, and collect the metadata about the
* class.
* @param origin the bytecode
*/
public void prepare(byte[] origin) throws IOException {
InputStream is = new ByteArrayInputStream(origin);
// First check if the class is already manipulated :
ClassReader ckReader = new ClassReader(is);
ClassChecker ck = new ClassChecker();
ckReader.accept(ck, ClassReader.SKIP_FRAMES);
is.close();
m_fields = ck.getFields(); // Get visited fields (contains only POJO fields)
m_className = ck.getClassName();
// Get interfaces and super class.
m_interfaces = ck.getInterfaces();
m_superClass = ck.getSuperClass();
// Get the methods list
m_methods = ck.getMethods();
// Methods are not yet collected, but the structure is ready.
m_inners = ck.getInnerClassesAndMethods();
m_version = ck.getClassVersion();
m_alreadyManipulated = ck.isAlreadyManipulated();
}
/**
* Manipulate the given byte array.
* @param origin : original class.
* @return the manipulated class, if the class is already manipulated, the original class.
* @throws IOException : if an error occurs during the manipulation.
*/
public byte[] manipulate(byte[] origin) throws IOException {
if (!m_alreadyManipulated) {
InputStream is2 = new ByteArrayInputStream(origin);
ClassReader reader = new ClassReader(is2);
ClassWriter writer = new ClassLoaderAwareClassWriter(ClassWriter.COMPUTE_FRAMES, m_className, m_superClass,
m_classLoader);
ClassManipulator process = new ClassManipulator(new CheckClassAdapter(writer, false), this);
if (m_version >= Opcodes.V1_6) {
reader.accept(process, ClassReader.EXPAND_FRAMES);
} else {
reader.accept(process, 0);
}
is2.close();
return writer.toByteArray();
} else {
return origin;
}
}
/**
* Checks whether the class was already manipulated.
* @return {@code true} if the class was already manipulated, {@code false} otherwise
*/
public boolean isAlreadyManipulated() {
return m_alreadyManipulated;
}
public static String toQualifiedName(String clazz) {
return clazz.replace("/", ".");
}
/**
* Compute component type manipulation metadata.
* @return the manipulation metadata of the class.
*/
public Element getManipulationMetadata() {
Element elem = new Element("Manipulation", "");
elem.addAttribute(new Attribute("className", toQualifiedName(m_className)));
if (m_superClass != null) {
elem.addAttribute(new Attribute("super", m_superClass));
}
for (String m_interface : m_interfaces) {
Element itf = new Element("Interface", "");
Attribute att = new Attribute("name", m_interface);
itf.addAttribute(att);
elem.addElement(itf);
}
for (Map.Entry<String, String> f : m_fields.entrySet()) {
Element field = new Element("Field", "");
Attribute attName = new Attribute("name", f.getKey());
Attribute attType = new Attribute("type", f.getValue());
field.addAttribute(attName);
field.addAttribute(attType);
elem.addElement(field);
}
for (MethodDescriptor method : m_methods) {
elem.addElement(method.getElement());
}
for (Map.Entry<String, List<MethodDescriptor>> inner : m_inners.entrySet()) {
Element element = new Element("Inner", "");
Attribute name = new Attribute("name", extractInnerClassName(toQualifiedName(inner.getKey())));
element.addAttribute(name);
for (MethodDescriptor method : inner.getValue()) {
element.addElement(method.getElement());
}
elem.addElement(element);
}
return elem;
}
/**
* Extracts the inner class simple name from the qualified name. It extracts the part after the `$` character.
* @param clazz the qualified class name
* @return the simple inner class name
*/
public static String extractInnerClassName(String clazz) {
if (!clazz.contains("$")) {
return clazz;
} else {
return clazz.substring(clazz.indexOf("$") +1);
}
}
public Map<String, String> getFields() {
return m_fields;
}
public List<MethodDescriptor> getMethods() {
return m_methods;
}
public Collection<String> getInnerClasses() {
return new ArrayList<String>(m_inners.keySet());
}
public int getClassVersion() {
return m_version;
}
/**
* Adds a method to an inner class.
* @param name the inner class name
* @param md the method descriptor to add
*/
public void addMethodToInnerClass(String name, MethodDescriptor md) {
List<MethodDescriptor> list = m_inners.get(name);
if (list == null) {
list = new ArrayList<MethodDescriptor>();
m_inners.put(name, list);
}
list.add(md);
}
/**
* Analyzes the given inner class.
* @param inner the inner class name
* @param bytecode the bytecode of the inner class
*/
public void prepareInnerClass(String inner, byte[] bytecode) throws IOException {
InputStream is = new ByteArrayInputStream(bytecode);
ClassReader ckReader = new ClassReader(is);
InnerClassChecker ck = new InnerClassChecker(inner, this);
ckReader.accept(ck, ClassReader.SKIP_FRAMES);
is.close();
// The metadata are collected during the visit.
}
/**
* Manipulates the inner class. If the outer class was already manipulated does not re-manipulate the inner class.
* We consider that the manipulation cycle of the outer and inner classes are the same.
* @param inner the inner class name
* @param bytecode input (i.e. original) class
* @return the manipulated class
* @throws IOException the class cannot be read correctly
*/
public byte[] manipulateInnerClass(String inner, byte[] bytecode) throws IOException {
if (!m_alreadyManipulated) {
InputStream is1 = new ByteArrayInputStream(bytecode);
ClassReader cr = new ClassReader(is1);
ClassWriter cw = new ClassLoaderAwareClassWriter(ClassWriter.COMPUTE_FRAMES, inner, null, m_classLoader);
InnerClassAdapter adapter = new InnerClassAdapter(inner, cw, m_className, this);
if (m_version >= Opcodes.V1_6) {
cr.accept(adapter, ClassReader.EXPAND_FRAMES);
} else {
cr.accept(adapter, 0);
}
is1.close();
return cw.toByteArray();
} else {
// Return the unchanged inner class
return bytecode;
}
}
public List<MethodDescriptor> getMethodsFromInnerClass(String innerClassInternalName) {
return m_inners.get(innerClassInternalName);
}
public Map<String, List<MethodDescriptor>> getInnerClassesAndMethods() {
// Transform the map to use the simple name of the inner classes.
Map<String, List<MethodDescriptor>> map = new HashMap<String, List<MethodDescriptor>>();
for (Map.Entry<String, List<MethodDescriptor>> entry : m_inners.entrySet()) {
String name = extractInnerClassName(toQualifiedName(entry.getKey()));
map.put(name, entry.getValue());
}
return map;
}
}