/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2009-2011, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.bytecode.buildtime.spi;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.hibernate.bytecode.spi.ByteCodeHelper;
import org.hibernate.bytecode.spi.ClassTransformer;
Provides the basic templating of how instrumentation should occur.
Author: Steve Ebersole
/**
* Provides the basic templating of how instrumentation should occur.
*
* @author Steve Ebersole
*/
public abstract class AbstractInstrumenter implements Instrumenter {
private static final int ZIP_MAGIC = 0x504B0304;
private static final int CLASS_MAGIC = 0xCAFEBABE;
protected final Logger logger;
protected final Options options;
Creates the basic instrumentation strategy.
Params: - logger – The bridge to the environment's logging system.
- options – User-supplied options.
/**
* Creates the basic instrumentation strategy.
*
* @param logger The bridge to the environment's logging system.
* @param options User-supplied options.
*/
public AbstractInstrumenter(Logger logger, Options options) {
this.logger = logger;
this.options = options;
}
Given the bytecode of a java class, retrieve the descriptor for that class.
Params: - byecode – The class bytecode.
Throws: - Exception – Indicates problems access the bytecode.
Returns: The class's descriptor
/**
* Given the bytecode of a java class, retrieve the descriptor for that class.
*
* @param byecode The class bytecode.
*
* @return The class's descriptor
*
* @throws Exception Indicates problems access the bytecode.
*/
protected abstract ClassDescriptor getClassDescriptor(byte[] byecode) throws Exception;
Create class transformer for the class.
Params: - descriptor – The descriptor of the class to be instrumented.
- classNames – The names of all classes to be instrumented; the "pipeline" if you will.
Returns: The transformer for the given class; may return null to indicate that transformation should
be skipped (ala already instrumented).
/**
* Create class transformer for the class.
*
* @param descriptor The descriptor of the class to be instrumented.
* @param classNames The names of all classes to be instrumented; the "pipeline" if you will.
*
* @return The transformer for the given class; may return null to indicate that transformation should
* be skipped (ala already instrumented).
*/
protected abstract ClassTransformer getClassTransformer(ClassDescriptor descriptor, Set classNames);
The main instrumentation entry point. Given a set of files, perform instrumentation on each discovered class
file.
Params: - files – The files.
/**
* The main instrumentation entry point. Given a set of files, perform instrumentation on each discovered class
* file.
*
* @param files The files.
*/
public void execute(Set<File> files) {
final Set<String> classNames = new HashSet<String>();
if ( options.performExtendedInstrumentation() ) {
logger.debug( "collecting class names for extended instrumentation determination" );
try {
for ( Object file1 : files ) {
final File file = (File) file1;
collectClassNames( file, classNames );
}
}
catch ( ExecutionException ee ) {
throw ee;
}
catch ( Exception e ) {
throw new ExecutionException( e );
}
}
logger.info( "starting instrumentation" );
try {
for ( File file : files ) {
processFile( file, classNames );
}
}
catch ( ExecutionException ee ) {
throw ee;
}
catch ( Exception e ) {
throw new ExecutionException( e );
}
}
Extract the names of classes from file, adding them to the classNames collection.
IMPL NOTE : file here may be either a class file or a jar. If a jar, all entries in the jar file are
processed.
Params: - file – The file from which to extract class metadata (descriptor).
- classNames – The collected class name collection.
Throws: - Exception – indicates problems accessing the file or its contents.
/**
* Extract the names of classes from file, adding them to the classNames collection.
* <p/>
* IMPL NOTE : file here may be either a class file or a jar. If a jar, all entries in the jar file are
* processed.
*
* @param file The file from which to extract class metadata (descriptor).
* @param classNames The collected class name collection.
*
* @throws Exception indicates problems accessing the file or its contents.
*/
private void collectClassNames(File file, final Set<String> classNames) throws Exception {
if ( isClassFile( file ) ) {
final byte[] bytes = ByteCodeHelper.readByteCode( file );
final ClassDescriptor descriptor = getClassDescriptor( bytes );
classNames.add( descriptor.getName() );
}
else if ( isJarFile( file ) ) {
final ZipEntryHandler collector = new ZipEntryHandler() {
public void handleEntry(ZipEntry entry, byte[] byteCode) throws Exception {
if ( !entry.isDirectory() ) {
// see if the entry represents a class file
final DataInputStream din = new DataInputStream( new ByteArrayInputStream( byteCode ) );
if ( din.readInt() == CLASS_MAGIC ) {
classNames.add( getClassDescriptor( byteCode ).getName() );
}
}
}
};
final ZipFileProcessor processor = new ZipFileProcessor( collector );
processor.process( file );
}
}
Does this file represent a compiled class?
Params: - file – The file to check.
Throws: - IOException – Indicates problem access the file.
Returns: True if the file is a class; false otherwise.
/**
* Does this file represent a compiled class?
*
* @param file The file to check.
*
* @return True if the file is a class; false otherwise.
*
* @throws IOException Indicates problem access the file.
*/
protected final boolean isClassFile(File file) throws IOException {
return checkMagic( file, CLASS_MAGIC );
}
Does this file represent a zip file of some format?
Params: - file – The file to check.
Throws: - IOException – Indicates problem access the file.
Returns: True if the file is n archive; false otherwise.
/**
* Does this file represent a zip file of some format?
*
* @param file The file to check.
*
* @return True if the file is n archive; false otherwise.
*
* @throws IOException Indicates problem access the file.
*/
protected final boolean isJarFile(File file) throws IOException {
return checkMagic( file, ZIP_MAGIC );
}
protected final boolean checkMagic(File file, long magic) throws IOException {
final DataInputStream in = new DataInputStream( new FileInputStream( file ) );
try {
final int m = in.readInt();
return magic == m;
}
finally {
in.close();
}
}
Actually process the file by applying instrumentation transformations to any classes it contains.
Again, just like with collectClassNames
this method can handle both class and archive files. Params: - file – The file to process.
- classNames – The 'pipeline' of classes to be processed. Only actually populated when the user specifies to perform
extended
instrumentation.
Throws: - Exception – Indicates an issue either access files or applying the transformations.
/**
* Actually process the file by applying instrumentation transformations to any classes it contains.
* <p/>
* Again, just like with {@link #collectClassNames} this method can handle both class and archive files.
*
* @param file The file to process.
* @param classNames The 'pipeline' of classes to be processed. Only actually populated when the user
* specifies to perform {@link org.hibernate.bytecode.buildtime.spi.Instrumenter.Options#performExtendedInstrumentation() extended} instrumentation.
*
* @throws Exception Indicates an issue either access files or applying the transformations.
*/
protected void processFile(File file, Set<String> classNames) throws Exception {
if ( isClassFile( file ) ) {
logger.debug( "processing class file : " + file.getAbsolutePath() );
processClassFile( file, classNames );
}
else if ( isJarFile( file ) ) {
logger.debug( "processing jar file : " + file.getAbsolutePath() );
processJarFile( file, classNames );
}
else {
logger.debug( "ignoring file : " + file.getAbsolutePath() );
}
}
Process a class file. Delegated to from processFile
in the case of a class file. Params: - file – The class file to process.
- classNames – The 'pipeline' of classes to be processed. Only actually populated when the user specifies to perform
extended
instrumentation.
Throws: - Exception – Indicates an issue either access files or applying the transformations.
/**
* Process a class file. Delegated to from {@link #processFile} in the case of a class file.
*
* @param file The class file to process.
* @param classNames The 'pipeline' of classes to be processed. Only actually populated when the user
* specifies to perform {@link org.hibernate.bytecode.buildtime.spi.Instrumenter.Options#performExtendedInstrumentation() extended} instrumentation.
*
* @throws Exception Indicates an issue either access files or applying the transformations.
*/
protected void processClassFile(File file, Set<String> classNames) throws Exception {
final byte[] bytes = ByteCodeHelper.readByteCode( file );
final ClassDescriptor descriptor = getClassDescriptor( bytes );
final ClassTransformer transformer = getClassTransformer( descriptor, classNames );
if ( transformer == null ) {
logger.debug( "no trasformer for class file : " + file.getAbsolutePath() );
return;
}
logger.info( "processing class : " + descriptor.getName() + "; file = " + file.getAbsolutePath() );
final byte[] transformedBytes = transformer.transform(
getClass().getClassLoader(),
descriptor.getName(),
null,
null,
descriptor.getBytes()
);
final OutputStream out = new FileOutputStream( file );
try {
out.write( transformedBytes );
out.flush();
}
finally {
try {
out.close();
}
catch ( IOException ignore) {
// intentionally empty
}
}
}
Process an archive file. Delegated to from processFile
in the case of an archive file. Params: - file – The archive file to process.
- classNames – The 'pipeline' of classes to be processed. Only actually populated when the user specifies to perform
extended
instrumentation.
Throws: - Exception – Indicates an issue either access files or applying the transformations.
/**
* Process an archive file. Delegated to from {@link #processFile} in the case of an archive file.
*
* @param file The archive file to process.
* @param classNames The 'pipeline' of classes to be processed. Only actually populated when the user
* specifies to perform {@link org.hibernate.bytecode.buildtime.spi.Instrumenter.Options#performExtendedInstrumentation() extended} instrumentation.
*
* @throws Exception Indicates an issue either access files or applying the transformations.
*/
protected void processJarFile(final File file, final Set<String> classNames) throws Exception {
final File tempFile = File.createTempFile(
file.getName(),
null,
new File( file.getAbsoluteFile().getParent() )
);
try {
final FileOutputStream fout = new FileOutputStream( tempFile, false );
try {
final ZipOutputStream out = new ZipOutputStream( fout );
final ZipEntryHandler transformer = new ZipEntryHandler() {
public void handleEntry(ZipEntry entry, byte[] byteCode) throws Exception {
logger.debug( "starting zip entry : " + entry.toString() );
if ( !entry.isDirectory() ) {
// see if the entry represents a class file
final DataInputStream din = new DataInputStream( new ByteArrayInputStream( byteCode ) );
if ( din.readInt() == CLASS_MAGIC ) {
final ClassDescriptor descriptor = getClassDescriptor( byteCode );
final ClassTransformer transformer = getClassTransformer( descriptor, classNames );
if ( transformer == null ) {
logger.debug( "no transformer for zip entry : " + entry.toString() );
}
else {
logger.info( "processing class : " + descriptor.getName() + "; entry = " + file.getAbsolutePath() );
byteCode = transformer.transform(
getClass().getClassLoader(),
descriptor.getName(),
null,
null,
descriptor.getBytes()
);
}
}
else {
logger.debug( "ignoring zip entry : " + entry.toString() );
}
}
final ZipEntry outEntry = new ZipEntry( entry.getName() );
outEntry.setMethod( entry.getMethod() );
outEntry.setComment( entry.getComment() );
outEntry.setSize( byteCode.length );
if ( outEntry.getMethod() == ZipEntry.STORED ){
final CRC32 crc = new CRC32();
crc.update( byteCode );
outEntry.setCrc( crc.getValue() );
outEntry.setCompressedSize( byteCode.length );
}
out.putNextEntry( outEntry );
out.write( byteCode );
out.closeEntry();
}
};
final ZipFileProcessor processor = new ZipFileProcessor( transformer );
processor.process( file );
out.close();
}
finally{
fout.close();
}
if ( file.delete() ) {
final File newFile = new File( tempFile.getAbsolutePath() );
if( ! newFile.renameTo( file ) ) {
throw new IOException( "can not rename " + tempFile + " to " + file );
}
}
else {
throw new IOException( "can not delete " + file );
}
}
finally {
if ( ! tempFile.delete() ) {
logger.info( "Unable to cleanup temporary jar file : " + tempFile.getAbsolutePath() );
}
}
}
Allows control over what exactly to transform.
/**
* Allows control over what exactly to transform.
*/
protected class CustomFieldFilter implements FieldFilter {
private final ClassDescriptor descriptor;
private final Set classNames;
public CustomFieldFilter(ClassDescriptor descriptor, Set classNames) {
this.descriptor = descriptor;
this.classNames = classNames;
}
public boolean shouldInstrumentField(String className, String fieldName) {
if ( descriptor.getName().equals( className ) ) {
logger.trace( "accepting transformation of field [" + className + "." + fieldName + "]" );
return true;
}
else {
logger.trace( "rejecting transformation of field [" + className + "." + fieldName + "]" );
return false;
}
}
public boolean shouldTransformFieldAccess(
String transformingClassName,
String fieldOwnerClassName,
String fieldName) {
if ( descriptor.getName().equals( fieldOwnerClassName ) ) {
logger.trace( "accepting transformation of field access [" + fieldOwnerClassName + "." + fieldName + "]" );
return true;
}
else if ( options.performExtendedInstrumentation() && classNames.contains( fieldOwnerClassName ) ) {
logger.trace( "accepting extended transformation of field access [" + fieldOwnerClassName + "." + fieldName + "]" );
return true;
}
else {
logger.trace( "rejecting transformation of field access [" + fieldOwnerClassName + "." + fieldName + "]; caller = " + transformingClassName );
return false;
}
}
}
General strategy contract for handling entries in an archive file.
/**
* General strategy contract for handling entries in an archive file.
*/
private static interface ZipEntryHandler {
Apply strategy to the given archive entry.
Params: - entry – The archive file entry.
- byteCode – The bytes making up the entry
Throws: - Exception – Problem handling entry
/**
* Apply strategy to the given archive entry.
*
* @param entry The archive file entry.
* @param byteCode The bytes making up the entry
*
* @throws Exception Problem handling entry
*/
public void handleEntry(ZipEntry entry, byte[] byteCode) throws Exception;
}
Applies ZipEntryHandler
strategies to the entries of an archive file. /**
* Applies {@link ZipEntryHandler} strategies to the entries of an archive file.
*/
private static class ZipFileProcessor {
private final ZipEntryHandler entryHandler;
public ZipFileProcessor(ZipEntryHandler entryHandler) {
this.entryHandler = entryHandler;
}
public void process(File file) throws Exception {
final ZipInputStream zip = new ZipInputStream( new FileInputStream( file ) );
try {
ZipEntry entry;
while ( (entry = zip.getNextEntry()) != null ) {
final byte[] bytes = ByteCodeHelper.readByteCode( zip );
entryHandler.handleEntry( entry, bytes );
zip.closeEntry();
}
}
finally {
zip.close();
}
}
}
}