/*
 * 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:
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:
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:
/** * 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(); } } } }