001    /*
002     $Id: AntBuilder.java,v 1.10 2005/09/13 11:36:29 dierk Exp $
003    
004     Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005    
006     Redistribution and use of this software and associated documentation
007     ("Software"), with or without modification, are permitted provided
008     that the following conditions are met:
009    
010     1. Redistributions of source code must retain copyright
011        statements and notices.  Redistributions must also contain a
012        copy of this document.
013    
014     2. Redistributions in binary form must reproduce the
015        above copyright notice, this list of conditions and the
016        following disclaimer in the documentation and/or other
017        materials provided with the distribution.
018    
019     3. The name "groovy" must not be used to endorse or promote
020        products derived from this Software without prior written
021        permission of The Codehaus.  For written permission,
022        please contact info@codehaus.org.
023    
024     4. Products derived from this Software may not be called "groovy"
025        nor may "groovy" appear in their names without prior written
026        permission of The Codehaus. "groovy" is a registered
027        trademark of The Codehaus.
028    
029     5. Due credit should be given to The Codehaus -
030        http://groovy.codehaus.org/
031    
032     THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
033     ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
034     NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
035     FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
036     THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
037     INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
039     SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
040     HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
041     STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
042     ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
043     OF THE POSSIBILITY OF SUCH DAMAGE.
044    
045     */
046    package groovy.util;
047    
048    
049    import java.lang.reflect.Constructor;
050    import java.lang.reflect.InvocationTargetException;
051    import java.lang.reflect.Method;
052    import java.util.Collections;
053    import java.util.Iterator;
054    import java.util.Map;
055    import java.util.logging.Level;
056    import java.util.logging.Logger;
057    
058    import org.apache.tools.ant.*;
059    import org.apache.tools.ant.types.DataType;
060    import org.codehaus.groovy.ant.FileScanner;
061    import org.codehaus.groovy.runtime.InvokerHelper;
062    
063    /**
064     * Allows Ant tasks to be used with GroovyMarkup 
065     * 
066     * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>, changes by Dierk Koenig (dk)
067     * @version $Revision: 1.10 $
068     */
069    public class AntBuilder extends BuilderSupport {
070    
071        private static final Class[] addTaskParamTypes = { String.class };
072    
073        private Logger log = Logger.getLogger(getClass().getName());
074        private Project project;
075    
076        public AntBuilder() {
077            this.project = createProject();
078        }
079    
080        public AntBuilder(Project project) {
081            this.project = project;
082        }
083    
084        // dk: introduced for convenience in subclasses
085        protected Project getProject() {
086            return project;
087        }
088    
089        /**
090         * @return Factory method to create new Project instances
091         */
092        protected Project createProject() {
093            Project project = new Project();
094            BuildLogger logger = new NoBannerLogger();
095    
096            logger.setMessageOutputLevel(org.apache.tools.ant.Project.MSG_INFO);
097            logger.setOutputPrintStream(System.out);
098            logger.setErrorPrintStream(System.err);
099    
100            project.addBuildListener(logger);
101    
102            project.init();
103            project.getBaseDir();
104            return project;
105        }
106    
107        protected void setParent(Object parent, Object child) {
108        }
109    
110        /**
111         * Determines, when the ANT Task that is represented by the "node" should perform.
112         * Node must be an ANT Task or no "perform" is called.
113         * If node is an ANT Task, it performs right after complete contstruction.
114         * If node is nested in a TaskContainer, calling "perform" is delegated to that
115         * TaskContainer.
116         * @param parent note: null when node is root
117         * @param node the node that now has all its children applied
118         */
119        protected void nodeCompleted(Object parent, Object node) {
120            if (parent instanceof TaskContainer) {
121                log.finest("parent is TaskContainer: no perform on nodeCompleted");
122                return; // parent will care about when children perform
123            }
124            if (node instanceof Task) {
125                Task task = (Task) node;
126                task.perform();
127            }
128        }
129    
130        protected Object createNode(Object tagName) {
131            return createNode(tagName.toString(), Collections.EMPTY_MAP);
132        }
133    
134        protected Object createNode(Object name, Object value) {
135            Object task = createNode(name);
136            setText(task, value.toString());
137            return task;
138        }
139    
140        protected Object createNode(Object name, Map attributes, Object value) {
141            Object task = createNode(name, attributes);
142            setText(task, value.toString());
143            return task;
144        }
145        
146        protected Object createNode(Object name, Map attributes) {
147    
148            if (name.equals("fileScanner")) {
149                return new FileScanner(project);
150            }
151            
152            String tagName = name.toString();
153            Object answer = null;
154    
155            Object parentObject = getCurrent();
156            Object parentTask = getParentTask();
157    
158            // lets assume that Task instances are not nested inside other Task instances
159            // for example <manifest> inside a <jar> should be a nested object, where as 
160            // if the parent is not a Task the <manifest> should create a ManifestTask
161            //
162            // also its possible to have a root Ant tag which isn't a task, such as when
163            // defining <fileset id="...">...</fileset>
164    
165            Object nested = null;
166            if (parentObject != null && !(parentTask instanceof TaskContainer)) {
167                nested = createNestedObject(parentObject, tagName);
168            }
169    
170            Task task = null;
171            if (nested == null) {
172                task = createTask(tagName);
173                if (task != null) {
174                    if (log.isLoggable(Level.FINE)) {
175                        log.fine("Creating an ant Task for name: " + tagName);
176                    }
177    
178                    // the following algorithm follows the lifetime of a tag
179                    // http://jakarta.apache.org/ant/manual/develop.html#writingowntask
180                    // kindly recommended by Stefan Bodewig
181    
182                    // create and set its project reference
183                    if (task instanceof TaskAdapter) {
184                        answer = ((TaskAdapter) task).getProxy();
185                    }
186                    else {
187                        answer = task;
188                    }
189    
190                    // set the task ID if one is given
191                    Object id = attributes.remove("id");
192                    if (id != null) {
193                        project.addReference((String) id, task);
194                    }
195    
196                    // now lets initialize
197                    task.init();
198    
199                    // now lets set any attributes of this tag...
200                    setBeanProperties(task, attributes);
201    
202                    // dk: TaskContainers have their own adding logic
203                    if (parentObject instanceof TaskContainer){
204                        ((TaskContainer)parentObject).addTask(task);
205                    }
206                }
207            }
208    
209            if (task == null) {
210                if (nested == null) {
211                    if (log.isLoggable(Level.FINE)) {
212                        log.fine("Trying to create a data type for tag: " + tagName);
213                    }
214                    nested = createDataType(tagName);
215                }
216                else {
217                    if (log.isLoggable(Level.FINE)) {
218                        log.fine("Created nested property tag: " + tagName);
219                    }
220                }
221    
222                if (nested != null) {
223                    answer = nested;
224    
225                    // set the task ID if one is given
226                    Object id = attributes.remove("id");
227                    if (id != null) {
228                        project.addReference((String) id, nested);
229                    }
230    
231                    try {
232                        InvokerHelper.setProperty(nested, "name", tagName);
233                    }
234                    catch (Exception e) {
235                    }
236    
237                    // now lets set any attributes of this tag...
238                    setBeanProperties(nested, attributes);
239    
240                    // now lets add it to its parent
241                    if (parentObject != null) {
242                        IntrospectionHelper ih = IntrospectionHelper.getHelper(parentObject.getClass());
243                        try {
244                            if (log.isLoggable(Level.FINE)) {
245                                log.fine(
246                                    "About to set the: "
247                                        + tagName
248                                        + " property on: "
249                                        + parentObject
250                                        + " to value: "
251                                        + nested
252                                        + " with type: "
253                                        + nested.getClass());
254                            }
255    
256                            ih.storeElement(project, parentObject, nested, tagName);
257                        }
258                        catch (Exception e) {
259                            log.log(Level.WARNING, "Caught exception setting nested: " + tagName, e);
260                        }
261    
262                        // now try to set the property for good measure
263                        // as the storeElement() method does not
264                        // seem to call any setter methods of non-String types
265                        try {
266                            InvokerHelper.setProperty(parentObject, tagName, nested);
267                        }
268                        catch (Exception e) {
269                            log.fine("Caught exception trying to set property: " + tagName + " on: " + parentObject);
270                        }
271                    }
272                }
273                else {
274                    log.log(Level.WARNING, "Could not convert tag: " + tagName + " into an Ant task, data type or property. Maybe the task is not on the classpath?");
275                }
276            }
277    
278            return answer;
279        }
280    
281        protected void setText(Object task, String text) {
282            // now lets set the addText() of the body content, if its applicaable
283            Method method = getAccessibleMethod(task.getClass(), "addText", addTaskParamTypes);
284            if (method != null) {
285                Object[] args = { text };
286                try {
287                    method.invoke(task, args);
288                }
289                catch (Exception e) {
290                    log.log(Level.WARNING, "Cannot call addText on: " + task + ". Reason: " + e, e);
291                }
292            }
293        }
294    
295        protected Method getAccessibleMethod(Class theClass, String name, Class[] paramTypes) {
296            while (true) {
297                try {
298                    Method answer = theClass.getDeclaredMethod(name, paramTypes);
299                    if (answer != null) {
300                        return answer;
301                    }
302                }
303                catch (Exception e) {
304                    // ignore
305                }
306                theClass = theClass.getSuperclass();
307                if (theClass == null) {
308                    return null;
309                }
310            }
311        }
312    
313        public Project getAntProject() {
314            return project;
315        }
316    
317        // Implementation methods
318        //-------------------------------------------------------------------------
319        protected void setBeanProperties(Object object, Map map) {
320            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) {
321                Map.Entry entry = (Map.Entry) iter.next();
322                String name = (String) entry.getKey();
323                Object value = entry.getValue();
324                setBeanProperty(object, name, ((value == null) ? null : value.toString()));
325            }
326        }
327    
328        protected void setBeanProperty(Object object, String name, Object value) {
329            if (log.isLoggable(Level.FINE)) {
330                String objStr;
331                try {
332                    objStr = object.toString(); // e.g. Fileset may throw Exception here when 'dir' is not yet set
333                } catch (Exception e) {
334                    objStr = object.getClass().getName();
335                }
336                log.fine("Setting bean property on: " + objStr + " name: " + name + " value: " + value);
337            }
338    
339            IntrospectionHelper ih = IntrospectionHelper.getHelper(object.getClass());
340    
341            if (value instanceof String) {
342                try {
343                    ih.setAttribute(getAntProject(), object, name.toLowerCase(), (String) value);
344                    return;
345                }
346                catch (Exception e) {
347                    // ignore: not a valid property
348                }
349            }
350    
351            try {
352    
353                ih.storeElement(getAntProject(), object, value, name);
354            }
355            catch (Exception e) {
356    
357                InvokerHelper.setProperty(object, name, value);
358            }
359        }
360    
361        /**
362         * Creates a nested object of the given object with the specified name
363         */
364        protected Object createNestedObject(Object object, String name) {
365            Object dataType = null;
366            if (object != null) {
367                IntrospectionHelper ih = IntrospectionHelper.getHelper(object.getClass());
368    
369                if (ih != null) {
370                    try {
371                        // dk: the line below resolves the deprecation warning but may not work
372                        // properly with namespaces.
373                        String namespaceUri = "";               // todo: how to set this?
374                        UnknownElement unknownElement = null;   // todo: what is expected here?
375                        dataType = ih.getElementCreator(getAntProject(), namespaceUri, object, name.toLowerCase(), unknownElement).create();
376                    }
377                    catch (BuildException be) {
378                        log.log(Level.SEVERE, "Caught: " + be, be);
379                    }
380                }
381            }
382            if (dataType == null) {
383                dataType = createDataType(name);
384            }
385            return dataType;
386        }
387    
388        protected Object createDataType(String name) {
389            Object dataType = null;
390    
391            Class type = (Class) getAntProject().getDataTypeDefinitions().get(name);
392    
393            if (type != null) {
394    
395                Constructor ctor = null;
396                boolean noArg = false;
397    
398                // DataType can have a "no arg" constructor or take a single
399                // Project argument.
400                try {
401                    ctor = type.getConstructor(new Class[0]);
402                    noArg = true;
403                }
404                catch (NoSuchMethodException nse) {
405                    try {
406                        ctor = type.getConstructor(new Class[] { Project.class });
407                        noArg = false;
408                    }
409                    catch (NoSuchMethodException nsme) {
410                        log.log(Level.INFO, "datatype '" + name + "' didn't have a constructor with an Ant Project", nsme);
411                    }
412                }
413    
414                if (noArg) {
415                    dataType = createDataType(ctor, new Object[0], name, "no-arg constructor");
416                }
417                else {
418                    dataType = createDataType(ctor, new Object[] { getAntProject()}, name, "an Ant project");
419                }
420                if (dataType != null) {
421                    ((DataType) dataType).setProject(getAntProject());
422                }
423            }
424    
425            return dataType;
426        }
427    
428        /**
429         * @return an object create with the given constructor and args.
430         * @param ctor a constructor to use creating the object
431         * @param args the arguments to pass to the constructor
432         * @param name the name of the data type being created
433         * @param argDescription a human readable description of the args passed
434         */
435        protected Object createDataType(Constructor ctor, Object[] args, String name, String argDescription) {
436            try {
437                Object datatype = ctor.newInstance(args);
438                return datatype;
439            }
440            catch (InstantiationException ie) {
441                log.log(Level.SEVERE, "datatype '" + name + "' couldn't be created with " + argDescription, ie);
442            }
443            catch (IllegalAccessException iae) {
444                log.log(Level.SEVERE, "datatype '" + name + "' couldn't be created with " + argDescription, iae);
445            }
446            catch (InvocationTargetException ite) {
447                log.log(Level.SEVERE, "datatype '" + name + "' couldn't be created with " + argDescription, ite);
448            }
449            return null;
450        }
451    
452        /**
453         * @param taskName the name of the task to create
454         * @return a newly created task
455         */
456        protected Task createTask(String taskName) {
457            return createTask(taskName, (Class) getAntProject().getTaskDefinitions().get(taskName));
458        }
459    
460        protected Task createTask(String taskName, Class taskType) {
461            if (taskType == null) {
462                return null;
463            }
464            try {
465                Object o = taskType.newInstance();
466                Task task = null;
467                if (o instanceof Task) {
468                    task = (Task) o;
469                }
470                else {
471                    TaskAdapter taskA = new TaskAdapter();
472                    taskA.setProxy(o);
473                    task = taskA;
474                }
475    
476                task.setProject(getAntProject());
477                task.setTaskName(taskName);
478    
479                return task;
480            }
481            catch (Exception e) {
482                log.log(Level.WARNING, "Could not create task: " + taskName + ". Reason: " + e, e);
483                return null;
484            }
485        }
486    
487        protected Task getParentTask() {
488            Object current = getCurrent();
489            if (current instanceof Task) {
490                return (Task) current;
491            }
492            return null;
493        }
494    }