├── src ├── main │ └── java │ │ ├── org │ │ └── jenkinsci │ │ │ └── ant │ │ │ ├── tasks │ │ │ ├── PathElement.java │ │ │ ├── JavacTask.java │ │ │ ├── Path.java │ │ │ └── FileSet.java │ │ │ ├── AntElementProxy.java │ │ │ ├── AntEvent.java │ │ │ ├── AntTask.java │ │ │ ├── BuildListenerAdapter.java │ │ │ ├── AntListener.java │ │ │ ├── ServiceLoader.java │ │ │ └── AntElement.java │ │ └── AntSpyListener.java └── test │ └── java │ └── test │ └── SampleListenerImpl.java ├── README.md └── pom.xml /src/main/java/org/jenkinsci/ant/tasks/PathElement.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant.tasks; 2 | 3 | import org.jenkinsci.ant.AntElementProxy; 4 | 5 | import java.io.File; 6 | 7 | /** 8 | * @author Kohsuke Kawaguchi 9 | */ 10 | public interface PathElement extends AntElementProxy { 11 | File location(); 12 | // path??? 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/tasks/JavacTask.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant.tasks; 2 | 3 | import org.jenkinsci.ant.AntElementProxy; 4 | 5 | import java.io.File; 6 | 7 | /** 8 | * Javac task. 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | public interface JavacTask extends AntElementProxy { 13 | File srcdir(); 14 | Path classpath(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/tasks/Path.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant.tasks; 2 | 3 | import org.jenkinsci.ant.AntElementProxy; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Represents a path-like structure. 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | public interface Path extends AntElementProxy { 13 | List pathelement(); 14 | List fileset(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/tasks/FileSet.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant.tasks; 2 | 3 | import org.jenkinsci.ant.AntElementProxy; 4 | 5 | import java.io.File; 6 | 7 | /** 8 | * Ant file set 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | public interface FileSet extends AntElementProxy { 13 | /** 14 | * the root of the directory tree of this FileSet. 15 | */ 16 | File dir(); 17 | 18 | /** 19 | * shortcut for specifying a single-file fileset 20 | */ 21 | File file(); 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ant Spy 2 | 3 | This library provides a convenient wrapper around Ant build event notification so that programs can inject themselves into Ant invocation and get notified of what targets/tasks are running with what parameters. 4 | 5 | Users would invoke Ant with the `-lib path/to/ant-spy.jar -listener AntSpyListener` command line option. Note that `ANT_ARGS` environment variable is a convenient way to insert these arguments without polluting the command line. 6 | 7 | Your program implements `AntListener` and put a META-INF/services entry for your class. This class also needs to be passed to Ant invocation by the `-lib` option. See a sample code in `src/test/java` for more details. 8 | -------------------------------------------------------------------------------- /src/main/java/AntSpyListener.java: -------------------------------------------------------------------------------- 1 | import org.jenkinsci.ant.AntListener; 2 | import org.jenkinsci.ant.BuildListenerAdapter; 3 | import org.jenkinsci.ant.ServiceLoader; 4 | 5 | /** 6 | * {@link BuildListenerAdapter} that picks up {@link AntListener} from META-INF/services. 7 | * 8 | *

9 | * Because this class is meant to be used in Ant command line option as "-listener AntSpyListener", 10 | * I'm putting this to the root package. 11 | * 12 | * @author Kohsuke Kawaguchi 13 | */ 14 | public class AntSpyListener extends BuildListenerAdapter { 15 | public AntSpyListener() { 16 | for (AntListener l : ServiceLoader.load(getClass().getClassLoader(),AntListener.class)) 17 | addListener(l); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/test/SampleListenerImpl.java: -------------------------------------------------------------------------------- 1 | package test; 2 | 3 | import org.jenkinsci.ant.AntEvent; 4 | import org.jenkinsci.ant.AntListener; 5 | import org.jenkinsci.ant.AntTask; 6 | import org.jenkinsci.ant.tasks.JavacTask; 7 | import org.kohsuke.MetaInfServices; 8 | 9 | import javax.annotation.Nonnull; 10 | 11 | /** 12 | * @author Kohsuke Kawaguchi 13 | */ 14 | @MetaInfServices 15 | public class SampleListenerImpl extends AntListener { 16 | @Override 17 | public void taskFinished(@Nonnull AntEvent event) { 18 | AntTask t = event.getTask(); 19 | if (t.getTaskName().equals("javac")) { 20 | System.out.println("srcdir="+t.getStructure(JavacTask.class).srcdir()); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/AntElementProxy.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant; 2 | 3 | import org.jenkinsci.ant.tasks.JavacTask; 4 | 5 | import java.lang.reflect.Proxy; 6 | 7 | /** 8 | * Base type for the type-safe proxy interface to represent Ant tasks. 9 | * 10 | *

11 | * First, for elements that appear in Ant build script, define interfaces that extends from this marker interface. 12 | * We call them "type-safe proxy interfaces". See {@link JavacTask} for example. 13 | * 14 | *

15 | * A type-safe proxy interface is {@linkplain Proxy implemented at runtime dynamically}. Methods should be 16 | * defined for attributes and child elements. Return types must match the expected datatype of the attributes 17 | * and elements. 18 | * 19 | * @author Kohsuke Kawaguchi 20 | * @see AntElement 21 | */ 22 | public interface AntElementProxy { 23 | /** 24 | * Retrieves the underlying {@link AntElement} that this proxy represents. 25 | * 26 | * @see AntElement#as(Class) 27 | */ 28 | AntElement unwrap(); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/AntEvent.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant; 2 | 3 | import org.apache.tools.ant.BuildEvent; 4 | import org.apache.tools.ant.Project; 5 | import org.apache.tools.ant.Target; 6 | 7 | /** 8 | * Wraps {@link BuildEvent} and provides convenience methods. 9 | * 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | public class AntEvent { 13 | private final BuildEvent event; 14 | private AntTask task; 15 | 16 | public AntEvent(BuildEvent event) { 17 | this.event = event; 18 | } 19 | 20 | public AntTask getTask() { 21 | if (task==null) 22 | task = new AntTask(this,event.getTask()); 23 | return task; 24 | } 25 | 26 | public Project getProject() { 27 | return event.getProject(); 28 | } 29 | 30 | public Target getTarget() { 31 | return event.getTarget(); 32 | } 33 | 34 | public Throwable getException() { 35 | return event.getException(); 36 | } 37 | 38 | public int getPriority() { 39 | return event.getPriority(); 40 | } 41 | 42 | public String getMessage() { 43 | return event.getMessage(); 44 | } 45 | 46 | public String getProperty(String name) { 47 | return getProject().getProperty(name); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/AntTask.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant; 2 | 3 | import org.apache.tools.ant.Location; 4 | import org.apache.tools.ant.Task; 5 | 6 | /** 7 | * Wraps Ant {@link Task} and provides convenience methods. 8 | * 9 | * @author Kohsuke Kawaguchi 10 | */ 11 | public class AntTask { 12 | /** 13 | * This is what we are wrapping. 14 | */ 15 | private final Task core; 16 | 17 | /** 18 | * A life of {@link AntTask} is scoped to one event callback. 19 | */ 20 | private final AntEvent event; 21 | 22 | public AntTask(AntEvent event, Task core) { 23 | this.event = event; 24 | this.core = core; 25 | } 26 | 27 | /** 28 | * Introspect structure of Ant task invocation. 29 | */ 30 | public AntElement getStructure() { 31 | return new AntElement(event,core.getRuntimeConfigurableWrapper()); 32 | } 33 | 34 | public T getStructure(Class type) { 35 | return getStructure().as(type); 36 | } 37 | 38 | public String getTaskName() { 39 | return core.getTaskName(); 40 | } 41 | 42 | public Location getLocation() { 43 | return core.getLocation(); 44 | } 45 | 46 | public String getDescription() { 47 | return core.getDescription(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/BuildListenerAdapter.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant; 2 | 3 | import org.apache.tools.ant.BuildEvent; 4 | import org.apache.tools.ant.BuildListener; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.CopyOnWriteArrayList; 8 | 9 | /** 10 | * Receives build events as {@link BuildListener} 11 | * and forwards them to {@link AntListener}. 12 | * 13 | * @author Kohsuke Kawaguchi 14 | */ 15 | public class BuildListenerAdapter implements BuildListener { 16 | private final List listeners = new CopyOnWriteArrayList(); 17 | 18 | public void addListener(AntListener l) { 19 | listeners.add(l); 20 | } 21 | 22 | public void removeListener(AntListener l) { 23 | listeners.remove(l); 24 | } 25 | 26 | public void buildStarted(BuildEvent event) { 27 | AntEvent ev = new AntEvent(event); 28 | for (AntListener l : listeners) 29 | l.buildStarted(ev); 30 | } 31 | 32 | public void buildFinished(BuildEvent event) { 33 | AntEvent ev = new AntEvent(event); 34 | for (AntListener l : listeners) 35 | l.buildFinished(ev); 36 | } 37 | 38 | public void targetStarted(BuildEvent event) { 39 | AntEvent ev = new AntEvent(event); 40 | for (AntListener l : listeners) 41 | l.targetStarted(ev); 42 | } 43 | 44 | public void targetFinished(BuildEvent event) { 45 | AntEvent ev = new AntEvent(event); 46 | for (AntListener l : listeners) 47 | l.targetFinished(ev); 48 | } 49 | 50 | public void taskStarted(BuildEvent event) { 51 | AntEvent ev = new AntEvent(event); 52 | for (AntListener l : listeners) 53 | l.taskStarted(ev); 54 | } 55 | 56 | public void taskFinished(BuildEvent event) { 57 | AntEvent ev = new AntEvent(event); 58 | for (AntListener l : listeners) 59 | l.taskFinished(ev); 60 | } 61 | 62 | public void messageLogged(BuildEvent event) { 63 | AntEvent ev = new AntEvent(event); 64 | for (AntListener l : listeners) 65 | l.messageLogged(ev); 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/AntListener.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | /** 6 | * Gets notified of the build in progress as {@link AntEvent}s. 7 | * 8 | * @author Kohsuke Kawaguchi 9 | */ 10 | public abstract class AntListener { 11 | /** 12 | * Signals that a build has started. This event 13 | * is fired before any targets have started. 14 | * 15 | * @param event An event with any relevant extra information. 16 | */ 17 | public void buildStarted(@Nonnull AntEvent event) {} 18 | 19 | /** 20 | * Signals that the last target has finished. This event 21 | * will still be fired if an error occurred during the build. 22 | * 23 | * @param event An event with any relevant extra information. 24 | * 25 | * @see AntEvent#getException() 26 | */ 27 | public void buildFinished(@Nonnull AntEvent event) {} 28 | 29 | /** 30 | * Signals that a target is starting. 31 | * 32 | * @param event An event with any relevant extra information. 33 | * 34 | * @see AntEvent#getTarget() 35 | */ 36 | public void targetStarted(@Nonnull AntEvent event) {} 37 | 38 | /** 39 | * Signals that a target has finished. This event will 40 | * still be fired if an error occurred during the build. 41 | * 42 | * @param event An event with any relevant extra information. 43 | * 44 | * @see AntEvent#getException() 45 | */ 46 | public void targetFinished(@Nonnull AntEvent event) {} 47 | 48 | /** 49 | * Signals that a task is starting. 50 | * 51 | * @param event An event with any relevant extra information. 52 | * 53 | * @see AntEvent#getTask() 54 | */ 55 | public void taskStarted(@Nonnull AntEvent event) {} 56 | 57 | /** 58 | * Signals that a task has finished. This event will still 59 | * be fired if an error occurred during the build. 60 | * 61 | * @param event An event with any relevant extra information. 62 | * 63 | * @see AntEvent#getException() 64 | */ 65 | public void taskFinished(@Nonnull AntEvent event) {} 66 | 67 | /** 68 | * Signals a message logging event. 69 | * 70 | * @param event An event with any relevant extra information. 71 | * 72 | * @see AntEvent#getMessage() 73 | * @see AntEvent#getException() 74 | * @see AntEvent#getPriority() 75 | */ 76 | public void messageLogged(@Nonnull AntEvent event) {} 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/ServiceLoader.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.net.URL; 7 | import java.util.ArrayList; 8 | import java.util.Enumeration; 9 | import java.util.List; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | 13 | import static java.util.logging.Level.WARNING; 14 | 15 | /** 16 | * @author Kohsuke Kawaguchi 17 | */ 18 | public class ServiceLoader { 19 | public static List load(ClassLoader cl, Class type) { 20 | List impls = new ArrayList(); 21 | 22 | try { 23 | Enumeration en = cl.getResources("META-INF/services/" + type.getName()); 24 | while (en.hasMoreElements()) { 25 | URL url = en.nextElement(); 26 | BufferedReader r = null; 27 | try { 28 | r = new BufferedReader(new InputStreamReader(url.openStream(),"UTF-8")); 29 | String line; 30 | while ((line=r.readLine())!=null) { 31 | line = line.trim(); 32 | if (line.startsWith("#") || line.length()==0) continue; 33 | 34 | try { 35 | Object impl = cl.loadClass(line).newInstance(); 36 | if (type.isInstance(impl)) 37 | impls.add(type.cast(impl)); 38 | } catch (InstantiationException e) { 39 | LOGGER.log(WARNING, "Failed to instantiate "+line,e); 40 | } catch (IllegalAccessException e) { 41 | LOGGER.log(WARNING, "Failed to instantiate " + line, e); 42 | } catch (ClassNotFoundException e) { 43 | LOGGER.log(WARNING, "Failed to load " + line, e); 44 | } 45 | } 46 | } catch (IOException e) { 47 | LOGGER.log(WARNING, "Failed to load "+url,e); 48 | } finally { 49 | try { 50 | if (r!=null) 51 | r.close(); 52 | } catch (IOException _) { 53 | } 54 | } 55 | } 56 | } catch (IOException e) { 57 | LOGGER.log(WARNING, "Failed to list up /META-INF/services/"+type.getName(),e); 58 | } 59 | 60 | return impls; 61 | } 62 | 63 | private static final Logger LOGGER = Logger.getLogger(ServiceLoader.class.getName()); 64 | } 65 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci 5 | jenkins 6 | 1.24 7 | 8 | 9 | ant-spy 10 | 1.1-SNAPSHOT 11 | 12 | ant-spy 13 | http://jenkins-ci.org/ 14 | 15 | 16 | scm:git:git://github.com/jenkinsci/${project.artifactId}.git 17 | scm:git:ssh://git@github.com/jenkinsci/${project.artifactId}.git 18 | https://github.com/jenkinsci/${project.artifactId} 19 | 20 | 21 | 22 | 23 | 24 | maven-assembly-plugin 25 | 2.3 26 | 27 | 28 | package 29 | 30 | single 31 | 32 | 33 | 34 | jar-with-dependencies 35 | 36 | 37 | 38 | 39 | 40 | 41 | maven-jar-plugin 42 | 43 | 44 | package 45 | 46 | test-jar 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | com.google.code.findbugs 57 | jsr305 58 | 1.3.9 59 | provided 60 | true 61 | 62 | 63 | org.jvnet 64 | tiger-types 65 | 1.4 66 | 67 | 68 | org.apache.ant 69 | ant 70 | 1.7.0 71 | provided 72 | 73 | 74 | 75 | org.kohsuke.metainf-services 76 | metainf-services 77 | 1.4 78 | test 79 | 80 | 81 | junit 82 | junit 83 | 3.8.1 84 | test 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/ant/AntElement.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.ant; 2 | 3 | import org.apache.tools.ant.RuntimeConfigurable; 4 | import org.jvnet.tiger_types.Types; 5 | 6 | import java.io.File; 7 | import java.lang.reflect.InvocationHandler; 8 | import java.lang.reflect.Method; 9 | import java.lang.reflect.Proxy; 10 | import java.util.ArrayList; 11 | import java.util.Enumeration; 12 | import java.util.List; 13 | 14 | /** 15 | * Represents an XML element that constitute an Ant task invocation. 16 | * 17 | * @author Kohsuke Kawaguchi 18 | * @see AntElementProxy 19 | */ 20 | public class AntElement { 21 | /** 22 | * This is what we are wrapping. 23 | */ 24 | private final RuntimeConfigurable core; 25 | 26 | /** 27 | * A life of {@link AntTask} is scoped to one event callback. 28 | */ 29 | private final AntEvent event; 30 | 31 | public AntElement(AntEvent event, RuntimeConfigurable core) { 32 | this.core = core; 33 | this.event = event; 34 | } 35 | 36 | /** 37 | * Gets the element name. 38 | */ 39 | public String getElementName() { 40 | return core.getElementTag(); 41 | } 42 | 43 | public String getStringAttribute(String name) { 44 | return eval((String) core.getAttributeMap().get(name)); 45 | } 46 | 47 | public File getFileAttribute(String name) { 48 | String v = getStringAttribute(name); 49 | if (v == null) return null; 50 | 51 | File f = new File(v); 52 | if (f.isAbsolute()) return f; 53 | 54 | return new File(event.getProperty("basedir"), v); 55 | } 56 | 57 | public AntElement getChildElement(String name) { 58 | Enumeration en = core.getChildren(); 59 | while (en.hasMoreElements()) { 60 | RuntimeConfigurable child = (RuntimeConfigurable)en.nextElement(); 61 | if (child.getElementTag().equals(name)) 62 | return new AntElement(event,child); 63 | } 64 | return null; 65 | } 66 | 67 | public List getChildElements(String name) { 68 | List r = new ArrayList(); 69 | Enumeration en = core.getChildren(); 70 | while (en.hasMoreElements()) { 71 | RuntimeConfigurable child = (RuntimeConfigurable)en.nextElement(); 72 | if (child.getElementTag().equals(name)) 73 | r.add(new AntElement(event,child)); 74 | } 75 | return r; 76 | } 77 | 78 | /** 79 | * Gets all the child elements of the same name, as a type-safe proxy. 80 | */ 81 | public List getChildElements(String name, Class type) { 82 | List r = new ArrayList(); 83 | for (AntElement t : getChildElements(name)) 84 | r.add(t.as(type)); 85 | return r; 86 | } 87 | 88 | 89 | /** 90 | * Evaluate and expands variable references. 91 | */ 92 | private String eval(String val) { 93 | if (val==null) return null; 94 | return event.getProject().replaceProperties(val); 95 | } 96 | 97 | /** 98 | * Wraps this {@link AntElement} into a type-safe proxy. 99 | */ 100 | public T as(Class type) { 101 | return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, new InvocationHandlerImpl())); 102 | } 103 | 104 | private class InvocationHandlerImpl implements InvocationHandler, AntElementProxy { 105 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 106 | if (method.getDeclaringClass()==Object.class) {// handle equals/hashCode methods on AntElement 107 | return method.invoke(AntElement.this,args); 108 | } 109 | 110 | if (method.getDeclaringClass()==AntElement.class) {// handle AntElement methods here 111 | return method.invoke(this,args); 112 | } 113 | 114 | String name = method.getName(); 115 | Class type = method.getReturnType(); 116 | 117 | if (type==String.class) return getStringAttribute(name); 118 | if (type==File.class) return getFileAttribute(name); 119 | if (type==AntElement.class) 120 | return getChildElement(name); 121 | if (AntElementProxy.class.isAssignableFrom(type)) { 122 | AntElement v = getChildElement(name); 123 | return v!=null ? v.as(type) : null; 124 | } 125 | if (type==List.class) { 126 | Class component = Types.erasure(Types.getTypeArgument(method.getGenericReturnType(),0)); 127 | if (component==AntElement.class) 128 | return getChildElements(name); 129 | if (AntElementProxy.class.isAssignableFrom(component)) 130 | return getChildElements(name,component); 131 | } 132 | 133 | throw new IllegalStateException("Unrecognized binding: "+method); 134 | } 135 | 136 | public AntElement unwrap() { 137 | return AntElement.this; 138 | } 139 | } 140 | } 141 | --------------------------------------------------------------------------------