| /* |
| * 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 groovy.ant; |
| |
| import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; |
| import groovy.namespace.QName; |
| import groovy.util.BuilderSupport; |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.BuildLogger; |
| import org.apache.tools.ant.DemuxInputStream; |
| import org.apache.tools.ant.DemuxOutputStream; |
| import org.apache.tools.ant.Location; |
| import org.apache.tools.ant.MagicNames; |
| import org.apache.tools.ant.NoBannerLogger; |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.ProjectHelper; |
| import org.apache.tools.ant.RuntimeConfigurable; |
| import org.apache.tools.ant.Target; |
| import org.apache.tools.ant.Task; |
| import org.apache.tools.ant.UnknownElement; |
| import org.apache.tools.ant.dispatch.DispatchUtils; |
| import org.apache.tools.ant.helper.AntXMLContext; |
| import org.apache.tools.ant.helper.ProjectHelper2; |
| import org.codehaus.groovy.ant.FileScanner; |
| import org.codehaus.groovy.reflection.ReflectionUtils; |
| import org.codehaus.groovy.runtime.DefaultGroovyMethodsSupport; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.Locator; |
| import org.xml.sax.SAXParseException; |
| import org.xml.sax.helpers.AttributesImpl; |
| |
| import java.io.InputStream; |
| import java.io.PrintStream; |
| import java.lang.reflect.Method; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Vector; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| /** |
| * Allows <a href="https://ant.apache.org/manual/tasklist.html">Ant tasks</a> to |
| * be used with a Groovy builder-style markup. Requires that <code>ant.jar</code> is on your classpath which will |
| * happen automatically if you are using the Groovy distribution but will be up |
| * to you to organize if you are embedding Groovy. If you wish to use the |
| * <a href="https://ant.apache.org/manual/install.html#optionalTasks">optional tasks</a> |
| * you will need to add one or more additional jars from the ant distribution to |
| * your classpath - see the <a href="https://ant.apache.org/manual/install.html#librarydependencies">library |
| * dependencies</a> for more details. |
| */ |
| public class AntBuilder extends BuilderSupport { |
| |
| private final Logger log = Logger.getLogger(getClass().getName()); |
| private final Project project; |
| private final AntXMLContext antXmlContext; |
| private final ProjectHelper2.ElementHandler antElementHandler = new ProjectHelper2.ElementHandler(); |
| private final ProjectHelper2.TargetHandler antTargetHandler = new ProjectHelper2.TargetHandler(); |
| private final Target collectorTarget; |
| private final Target implicitTarget; |
| private Target definingTarget; |
| private Object lastCompletedNode; |
| // true when inside a task so special ant.target handling occurs just at top level |
| boolean insideTask; |
| |
| private boolean saveStreams = true; |
| private static Integer streamCount = 0; |
| private static InputStream savedIn; |
| private static PrintStream savedErr; |
| private static PrintStream savedOut; |
| private static DemuxInputStream demuxInputStream; |
| private static DemuxOutputStream demuxOutputStream; |
| private static DemuxOutputStream demuxErrorStream; |
| private static InputStream savedProjectInputStream; |
| |
| public AntBuilder() { |
| this(createProject()); |
| } |
| |
| public AntBuilder(final Project project) { |
| this(project, new Target()); |
| } |
| |
| public AntBuilder(final Project project, final Target owningTarget) { |
| this.project = project; |
| |
| /* |
| * GROOVY-4524: The following is not needed anymore as an ant Project already by default has inputhandler |
| * set to DefaultInputHandler. And if it is again set here, it mistakenly overrides the custom input handler |
| * if set using -inputhandler switch. |
| */ |
| //this.project.setInputHandler(new DefaultInputHandler()); |
| |
| collectorTarget = owningTarget; |
| antXmlContext = new AntXMLContext(project); |
| collectorTarget.setProject(project); |
| antXmlContext.setCurrentTarget(collectorTarget); |
| antXmlContext.setLocator(new AntBuilderLocator()); |
| antXmlContext.setCurrentTargets(new HashMap<String, Target>()); |
| |
| implicitTarget = new Target(); |
| implicitTarget.setProject(project); |
| implicitTarget.setName(""); |
| antXmlContext.setImplicitTarget(implicitTarget); |
| |
| // FileScanner is a Groovy utility |
| project.addDataTypeDefinition("fileScanner", FileScanner.class); |
| } |
| |
| public AntBuilder(final Task parentTask) { |
| this(parentTask.getProject(), parentTask.getOwningTarget()); |
| |
| // define "owning" task as wrapper to avoid having tasks added to the target |
| // but it needs to be an UnknownElement and no access is available from |
| // task to its original UnknownElement |
| final UnknownElement ue = new UnknownElement(parentTask.getTaskName()); |
| ue.setProject(parentTask.getProject()); |
| ue.setTaskType(parentTask.getTaskType()); |
| ue.setTaskName(parentTask.getTaskName()); |
| ue.setLocation(parentTask.getLocation()); |
| ue.setOwningTarget(parentTask.getOwningTarget()); |
| ue.setRuntimeConfigurableWrapper(parentTask.getRuntimeConfigurableWrapper()); |
| parentTask.getRuntimeConfigurableWrapper().setProxy(ue); |
| antXmlContext.pushWrapper(parentTask.getRuntimeConfigurableWrapper()); |
| } |
| |
| /** |
| * # |
| * Gets the Ant project in which the tasks are executed |
| * |
| * @return the project |
| */ |
| public Project getProject() { |
| return project; |
| } |
| |
| /** |
| * Gets the xml context of Ant used while creating tasks |
| * |
| * @return the Ant xml context |
| */ |
| public AntXMLContext getAntXmlContext() { |
| return antXmlContext; |
| } |
| |
| /** |
| * Whether stdin, stdout, stderr streams are saved. |
| * |
| * @return true if we are saving streams |
| * @see #setSaveStreams(boolean) |
| */ |
| public boolean isSaveStreams() { |
| return saveStreams; |
| } |
| |
| /** |
| * Indicates that we save stdin, stdout, stderr and replace them |
| * while AntBuilder is executing tasks with |
| * streams that funnel the normal streams into Ant's logs. |
| * |
| * @param saveStreams set to false to disable this behavior |
| */ |
| public void setSaveStreams(boolean saveStreams) { |
| this.saveStreams = saveStreams; |
| } |
| |
| /** |
| * @return Factory method to create new Project instances |
| */ |
| protected static Project createProject() { |
| final Project project = new Project(); |
| |
| final ProjectHelper helper = ProjectHelper.getProjectHelper(); |
| project.addReference(MagicNames.REFID_PROJECT_HELPER, helper); |
| helper.getImportStack().addElement("AntBuilder"); // import checks that stack is not empty |
| |
| final BuildLogger logger = new NoBannerLogger(); |
| |
| logger.setMessageOutputLevel(org.apache.tools.ant.Project.MSG_INFO); |
| logger.setOutputPrintStream(System.out); |
| logger.setErrorPrintStream(System.err); |
| |
| project.addBuildListener(logger); |
| |
| project.init(); |
| project.getBaseDir(); |
| return project; |
| } |
| |
| @Override |
| protected void setParent(Object parent, Object child) { |
| } |
| |
| /** |
| * We don't want to return the node as created in {@link #createNode(Object, Map, Object)} |
| * but the one made ready by {@link #nodeCompleted(Object, Object)} |
| * |
| * @see groovy.util.BuilderSupport#doInvokeMethod(java.lang.String, java.lang.Object, java.lang.Object) |
| */ |
| @Override |
| protected Object doInvokeMethod(String methodName, Object name, Object args) { |
| super.doInvokeMethod(methodName, name, args); |
| |
| |
| // return the completed node |
| return lastCompletedNode; |
| } |
| |
| /** |
| * Determines, when the ANT Task that is represented by the "node" should perform. |
| * Node must be an ANT Task or no "perform" is called. |
| * If node is an ANT Task, it performs right after complete construction. |
| * If node is nested in a TaskContainer, calling "perform" is delegated to that |
| * TaskContainer. |
| * |
| * @param parent note: null when node is root |
| * @param node the node that now has all its children applied |
| */ |
| @Override |
| @SuppressFBWarnings(value = "DLS_DEAD_LOCAL_STORE", justification = "False positive: bytecode needlessly but harmlessly assigns local to JVM register") |
| protected void nodeCompleted(final Object parent, final Object node) { |
| if (parent == null) insideTask = false; |
| antElementHandler.onEndElement(null, null, antXmlContext); |
| |
| lastCompletedNode = node; |
| if (parent != null && !(parent instanceof Target)) { |
| log.finest("parent is not null: no perform on nodeCompleted"); |
| return; // parent will care about when children perform |
| } |
| if (definingTarget != null && definingTarget == parent && node instanceof Task) return; // inside defineTarget |
| if (definingTarget == node) { |
| definingTarget = null; |
| } |
| |
| // as in Target.execute() |
| if (node instanceof Task) { |
| Task task = (Task) node; |
| final String taskName = task.getTaskName(); |
| |
| if ("antcall".equals(taskName) && parent == null) { |
| throw new BuildException("antcall not supported within AntBuilder, consider using 'ant.project.executeTarget('targetName')' instead."); |
| } |
| |
| if (saveStreams) { |
| // save original streams |
| synchronized (AntBuilder.class) { |
| int currentStreamCount = streamCount++; // SuppressFBWarnings: DLS_DEAD_LOCAL_STORE |
| if (currentStreamCount == 0) { |
| // we are first, save the streams |
| savedProjectInputStream = project.getDefaultInputStream(); |
| savedIn = System.in; |
| savedErr = System.err; |
| savedOut = System.out; |
| |
| if (!(savedIn instanceof DemuxInputStream)) { |
| project.setDefaultInputStream(savedIn); |
| demuxInputStream = new DemuxInputStream(project); |
| System.setIn(demuxInputStream); |
| } |
| demuxOutputStream = new DemuxOutputStream(project, false); |
| System.setOut(printStream(demuxOutputStream)); |
| demuxErrorStream = new DemuxOutputStream(project, true); |
| System.setErr(printStream(demuxErrorStream)); |
| } |
| } |
| } |
| |
| try { |
| lastCompletedNode = performTask(task); |
| } finally { |
| if (saveStreams) { |
| synchronized (AntBuilder.class) { |
| int currentStreamCount = --streamCount; |
| if (currentStreamCount == 0) { |
| // last to leave, turn out the lights: restore original streams |
| project.setDefaultInputStream(savedProjectInputStream); |
| System.setOut(savedOut); |
| System.setErr(savedErr); |
| if (demuxInputStream != null) { |
| System.setIn(savedIn); |
| DefaultGroovyMethodsSupport.closeQuietly(demuxInputStream); |
| demuxInputStream = null; |
| } |
| DefaultGroovyMethodsSupport.closeQuietly(demuxOutputStream); |
| DefaultGroovyMethodsSupport.closeQuietly(demuxErrorStream); |
| demuxOutputStream = null; |
| demuxErrorStream = null; |
| } |
| } |
| } |
| } |
| |
| // restore dummy collector target |
| if ("import".equals(taskName)) { |
| antXmlContext.setCurrentTarget(collectorTarget); |
| } |
| } else if (node instanceof Target) { |
| // restore dummy collector target |
| antXmlContext.setCurrentTarget(collectorTarget); |
| } else { |
| final RuntimeConfigurable r = (RuntimeConfigurable) node; |
| r.maybeConfigure(project); |
| } |
| } |
| |
| @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "We are only wrapping stdout/stderr which are likely created with default encoding") |
| private PrintStream printStream(DemuxOutputStream outputStream) { |
| // we could make a saveStreamEncoding but seems like a rare scenario |
| return new PrintStream(outputStream); |
| } |
| |
| // Copied from org.apache.tools.ant.Task, since we need to get a real thing before it gets nulled in DispatchUtils.execute |
| |
| private Object performTask(Task task) { |
| |
| Throwable reason = null; |
| try { |
| // Have to call fireTestStared/fireTestFinished via reflection as they unfortunately have protected access in Project |
| final Method fireTaskStarted = Project.class.getDeclaredMethod("fireTaskStarted", Task.class); |
| ReflectionUtils.trySetAccessible(fireTaskStarted); |
| fireTaskStarted.invoke(project, task); |
| |
| Object realThing; |
| realThing = task; |
| task.maybeConfigure(); |
| if (task instanceof UnknownElement) { |
| realThing = ((UnknownElement) task).getRealThing(); |
| } |
| |
| DispatchUtils.execute(task); |
| |
| return realThing != null ? realThing : task; |
| } catch (BuildException ex) { |
| if (ex.getLocation() == Location.UNKNOWN_LOCATION) { |
| ex.setLocation(task.getLocation()); |
| } |
| reason = ex; |
| throw ex; |
| } catch (Exception ex) { |
| reason = ex; |
| BuildException be = new BuildException(ex); |
| be.setLocation(task.getLocation()); |
| throw be; |
| } catch (Error ex) { |
| reason = ex; |
| throw ex; |
| } finally { |
| try { |
| final Method fireTaskFinished = Project.class.getDeclaredMethod("fireTaskFinished", Task.class, Throwable.class); |
| ReflectionUtils.trySetAccessible(fireTaskFinished); |
| fireTaskFinished.invoke(project, task, reason); |
| } catch (Exception e) { |
| BuildException be = new BuildException(e); |
| be.setLocation(task.getLocation()); |
| throw be; |
| } |
| } |
| } |
| |
| @Override |
| protected Object createNode(Object tagName) { |
| return createNode(tagName, Collections.EMPTY_MAP); |
| } |
| |
| @Override |
| protected Object createNode(Object name, Object value) { |
| Object task = createNode(name); |
| setText(task, value.toString()); |
| return task; |
| } |
| |
| @Override |
| protected Object createNode(Object name, Map attributes, Object value) { |
| Object task = createNode(name, attributes); |
| setText(task, value.toString()); |
| return task; |
| } |
| |
| /** |
| * Builds an {@link Attributes} from a {@link Map} |
| * |
| * @param attributes the attributes to wrap |
| * @return the wrapped attributes |
| */ |
| protected static Attributes buildAttributes(final Map attributes) { |
| final AttributesImpl attr = new AttributesImpl(); |
| for (Object o : attributes.entrySet()) { |
| final Map.Entry entry = (Map.Entry) o; |
| final String attributeName = (String) entry.getKey(); |
| final String attributeValue = String.valueOf(entry.getValue()); |
| attr.addAttribute(null, attributeName, attributeName, "CDATA", attributeValue); |
| } |
| return attr; |
| } |
| |
| @Override |
| protected Object createNode(final Object name, final Map attributes) { |
| |
| final Attributes attrs = buildAttributes(attributes); |
| String tagName = name.toString(); |
| String ns = ""; |
| |
| if (name instanceof QName) { |
| QName q = (QName) name; |
| tagName = q.getLocalPart(); |
| ns = q.getNamespaceURI(); |
| } |
| |
| // import can be used only as top level element |
| if ("import".equals(name)) { |
| antXmlContext.setCurrentTarget(implicitTarget); |
| } else if ("target".equals(name) && !insideTask) { |
| return onStartTarget(attrs, tagName, ns); |
| } else if ("defineTarget".equals(name) && !insideTask) { |
| return onDefineTarget(attrs, "target", ns); |
| } |
| |
| try { |
| antElementHandler.onStartElement(ns, tagName, tagName, attrs, antXmlContext); |
| } catch (final SAXParseException e) { |
| log.log(Level.SEVERE, "Caught: " + e, e); |
| } |
| |
| insideTask = true; |
| final RuntimeConfigurable wrapper = antXmlContext.getWrapperStack().lastElement(); |
| return wrapper.getProxy(); |
| } |
| |
| private Target onDefineTarget(final Attributes attrs, String tagName, String ns) { |
| try { |
| antTargetHandler.onStartElement(ns, tagName, tagName, attrs, antXmlContext); |
| final Target newTarget = getProject().getTargets().get(attrs.getValue("name")); |
| antXmlContext.setCurrentTarget(newTarget); |
| definingTarget = newTarget; |
| return newTarget; |
| } catch (final SAXParseException e) { |
| log.log(Level.SEVERE, "Caught: " + e, e); |
| return null; |
| } |
| } |
| |
| private Target onStartTarget(final Attributes attrs, String tagName, String ns) { |
| try { |
| antTargetHandler.onStartElement(ns, tagName, tagName, attrs, antXmlContext); |
| final Target newTarget = getProject().getTargets().get(attrs.getValue("name")); |
| |
| // execute dependencies (if any) |
| final Vector<Target> targets = new Vector<Target>(); |
| for (final Enumeration<String> deps = newTarget.getDependencies(); deps.hasMoreElements(); ) { |
| final String targetName = deps.nextElement(); |
| targets.add(project.getTargets().get(targetName)); |
| } |
| getProject().executeSortedTargets(targets); |
| |
| antXmlContext.setCurrentTarget(newTarget); |
| return newTarget; |
| } catch (final SAXParseException e) { |
| log.log(Level.SEVERE, "Caught: " + e, e); |
| return null; |
| } |
| } |
| |
| protected void setText(Object task, String text) { |
| final char[] characters = text.toCharArray(); |
| try { |
| antElementHandler.characters(characters, 0, characters.length, antXmlContext); |
| } catch (final SAXParseException e) { |
| log.log(Level.WARNING, "SetText failed: " + task + ". Reason: " + e, e); |
| } |
| } |
| |
| public Project getAntProject() { |
| return project; |
| } |
| } |
| |
| /** |
| * Would be nice to retrieve location information (from AST?). |
| * In a first time, without info |
| */ |
| class AntBuilderLocator implements Locator { |
| @Override |
| public int getColumnNumber() { |
| return 0; |
| } |
| |
| @Override |
| public int getLineNumber() { |
| return 0; |
| } |
| |
| @Override |
| public String getPublicId() { |
| return ""; |
| } |
| |
| @Override |
| public String getSystemId() { |
| return ""; |
| } |
| } |