/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 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.build.gradle.inject;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstantAttribute;
import javassist.bytecode.FieldInfo;
import org.gradle.api.Action;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Steve Ebersole
*/
public class InjectionAction implements Action<Task> {
private static final Logger log = LoggerFactory.getLogger( InjectionAction.class );
private final Project project;
private List<Injection> injections = new ArrayList<Injection>();
private LoaderClassPath loaderClassPath;
private ClassPool classPool;
public InjectionAction(Project project) {
this.project = project;
}
void addInjection(Injection injection) {
injections.add( injection );
}
@Override
public void execute(Task task) {
final ClassLoader runtimeScopeClassLoader = buildRuntimeScopeClassLoader();
loaderClassPath = new LoaderClassPath( runtimeScopeClassLoader );
classPool = new ClassPool( true );
classPool.appendClassPath( loaderClassPath );
try {
performInjections();
}
finally {
loaderClassPath.close();
}
}
private ClassLoader buildRuntimeScopeClassLoader() {
final ArrayList<URL> classPathUrls = new ArrayList<URL>();
final SourceSet mainSourceSet = project
.getConvention()
.getPlugin( JavaPluginConvention.class )
.getSourceSets()
.findByName( SourceSet.MAIN_SOURCE_SET_NAME );
for ( File file : mainSourceSet.getRuntimeClasspath() ) {
try {
classPathUrls.add( file.toURI().toURL() );
}
catch (MalformedURLException e) {
throw new InjectionException( "Could not determine artifact URL [" + file.getPath() + "]", e );
}
}
return new URLClassLoader( classPathUrls.toArray( new URL[classPathUrls.size()] ), getClass().getClassLoader() );
}
private void performInjections() {
for ( Injection injection : injections ) {
for ( TargetMember targetMember : injection.getTargetMembers() ) {
resolveInjectionTarget( targetMember ).inject( injection.getExpression() );
}
}
}
private InjectionTarget resolveInjectionTarget(TargetMember targetMember) {
try {
final CtClass ctClass = classPool.get( targetMember.getClassName() );
// see if it is a field...
try {
CtField field = ctClass.getField( targetMember.getMemberName() );
return new FieldInjectionTarget( targetMember, ctClass, field );
}
catch( NotFoundException ignore ) {
}
// see if it is a method...
for ( CtMethod method : ctClass.getMethods() ) {
if ( method.getName().equals( targetMember.getMemberName() ) ) {
return new MethodInjectionTarget( targetMember, ctClass, method );
}
}
// finally throw an exception
throw new InjectionException( "Unknown member [" + targetMember.getQualifiedName() + "]" );
}
catch ( Throwable e ) {
throw new InjectionException( "Unable to resolve class [" + targetMember.getClassName() + "]", e );
}
}
/**
* Strategy for performing an injection
*/
private static interface InjectionTarget {
/**
* Inject the given value per this target's strategy.
*
* @param value The value to inject.
*
* @throws org.hibernate.build.gradle.inject.InjectionException Indicates a problem performing the injection.
*/
public void inject(String value);
}
private abstract class BaseInjectionTarget implements InjectionTarget {
@SuppressWarnings( {"UnusedDeclaration"})
private final TargetMember targetMember;
private final CtClass ctClass;
private final File classFileLocation;
protected BaseInjectionTarget(TargetMember targetMember, CtClass ctClass) {
this.targetMember = targetMember;
this.ctClass = ctClass;
try {
classFileLocation = new File( loaderClassPath.find( targetMember.getClassName() ).toURI() );
}
catch ( Throwable e ) {
throw new InjectionException( "Unable to resolve class file path", e );
}
}
@Override
public void inject(String value) {
doInjection( value );
writeOutChanges();
}
protected abstract void doInjection(String value);
protected void writeOutChanges() {
log.info( "writing injection changes back [" + classFileLocation.getAbsolutePath() + "]" );
long timeStamp = classFileLocation.lastModified();
ClassFile classFile = ctClass.getClassFile();
classFile.compact();
try {
DataOutputStream out = new DataOutputStream( new BufferedOutputStream( new FileOutputStream( classFileLocation ) ) );
try {
classFile.write( out );
out.flush();
if ( ! classFileLocation.setLastModified( System.currentTimeMillis() ) ) {
log.info( "Unable to manually update class file timestamp" );
}
}
finally {
out.close();
classFileLocation.setLastModified( timeStamp );
}
}
catch ( IOException e ) {
throw new InjectionException( "Unable to write out modified class file", e );
}
}
}
private class FieldInjectionTarget extends BaseInjectionTarget {
private final CtField ctField;
private FieldInjectionTarget(TargetMember targetMember, CtClass ctClass, CtField ctField) {
super( targetMember, ctClass );
this.ctField = ctField;
if ( ! Modifier.isStatic( ctField.getModifiers() ) ) {
throw new InjectionException( "Field is not static [" + targetMember.getQualifiedName() + "]" );
}
}
@Override
protected void doInjection(String value) {
final FieldInfo ctFieldInfo = ctField.getFieldInfo();
ctFieldInfo.addAttribute(
new ConstantAttribute(
ctFieldInfo.getConstPool(),
ctFieldInfo.getConstPool().addStringInfo( value )
)
);
}
}
private class MethodInjectionTarget extends BaseInjectionTarget {
private final CtMethod ctMethod;
private MethodInjectionTarget(TargetMember targetMember, CtClass ctClass, CtMethod ctMethod) {
super( targetMember, ctClass );
this.ctMethod = ctMethod;
}
@Override
protected void doInjection(String value) {
try {
ctMethod.setBody( "{return \"" + value + "\";}" );
}
catch ( Throwable t ) {
throw new InjectionException( "Unable to replace method body", t );
}
}
}
}