001    /*
002     $Id: AntBuilder.java,v 1.9 2004/12/13 23:48:21 glaforge 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.9 $
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                log.fine("Setting bean property on: " + object + " name: " + name + " value: " + value);
331            }
332    
333            IntrospectionHelper ih = IntrospectionHelper.getHelper(object.getClass());
334    
335            if (value instanceof String) {
336                try {
337                    ih.setAttribute(getAntProject(), object, name.toLowerCase(), (String) value);
338                    return;
339                }
340                catch (Exception e) {
341                    // ignore: not a valid property
342                }
343            }
344    
345            try {
346    
347                ih.storeElement(getAntProject(), object, value, name);
348            }
349            catch (Exception e) {
350    
351                InvokerHelper.setProperty(object, name, value);
352            }
353        }
354    
355        /**
356         * Creates a nested object of the given object with the specified name
357         */
358        protected Object createNestedObject(Object object, String name) {
359            Object dataType = null;
360            if (object != null) {
361                IntrospectionHelper ih = IntrospectionHelper.getHelper(object.getClass());
362    
363                if (ih != null) {
364                    try {
365                        // dk: the line below resolves the deprecation warning but may not work
366                        // properly with namespaces.
367                        String namespaceUri = "";               // todo: how to set this?
368                        UnknownElement unknownElement = null;   // todo: what is expected here?
369                        dataType = ih.getElementCreator(getAntProject(), namespaceUri, object, name.toLowerCase(), unknownElement).create();
370                    }
371                    catch (BuildException be) {
372                        log.log(Level.SEVERE, "Caught: " + be, be);
373                    }
374                }
375            }
376            if (dataType == null) {
377                dataType = createDataType(name);
378            }
379            return dataType;
380        }
381    
382        protected Object createDataType(String name) {
383            Object dataType = null;
384    
385            Class type = (Class) getAntProject().getDataTypeDefinitions().get(name);
386    
387            if (type != null) {
388    
389                Constructor ctor = null;
390                boolean noArg = false;
391    
392                // DataType can have a "no arg" constructor or take a single
393                // Project argument.
394                try {
395                    ctor = type.getConstructor(new Class[0]);
396                    noArg = true;
397                }
398                catch (NoSuchMethodException nse) {
399                    try {
400                        ctor = type.getConstructor(new Class[] { Project.class });
401                        noArg = false;
402                    }
403                    catch (NoSuchMethodException nsme) {
404                        log.log(Level.INFO, "datatype '" + name + "' didn't have a constructor with an Ant Project", nsme);
405                    }
406                }
407    
408                if (noArg) {
409                    dataType = createDataType(ctor, new Object[0], name, "no-arg constructor");
410                }
411                else {
412                    dataType = createDataType(ctor, new Object[] { getAntProject()}, name, "an Ant project");
413                }
414                if (dataType != null) {
415                    ((DataType) dataType).setProject(getAntProject());
416                }
417            }
418    
419            return dataType;
420        }
421    
422        /**
423         * @return an object create with the given constructor and args.
424         * @param ctor a constructor to use creating the object
425         * @param args the arguments to pass to the constructor
426         * @param name the name of the data type being created
427         * @param argDescription a human readable description of the args passed
428         */
429        protected Object createDataType(Constructor ctor, Object[] args, String name, String argDescription) {
430            try {
431                Object datatype = ctor.newInstance(args);
432                return datatype;
433            }
434            catch (InstantiationException ie) {
435                log.log(Level.SEVERE, "datatype '" + name + "' couldn't be created with " + argDescription, ie);
436            }
437            catch (IllegalAccessException iae) {
438                log.log(Level.SEVERE, "datatype '" + name + "' couldn't be created with " + argDescription, iae);
439            }
440            catch (InvocationTargetException ite) {
441                log.log(Level.SEVERE, "datatype '" + name + "' couldn't be created with " + argDescription, ite);
442            }
443            return null;
444        }
445    
446        /**
447         * @param taskName the name of the task to create
448         * @return a newly created task
449         */
450        protected Task createTask(String taskName) {
451            return createTask(taskName, (Class) getAntProject().getTaskDefinitions().get(taskName));
452        }
453    
454        protected Task createTask(String taskName, Class taskType) {
455            if (taskType == null) {
456                return null;
457            }
458            try {
459                Object o = taskType.newInstance();
460                Task task = null;
461                if (o instanceof Task) {
462                    task = (Task) o;
463                }
464                else {
465                    TaskAdapter taskA = new TaskAdapter();
466                    taskA.setProxy(o);
467                    task = taskA;
468                }
469    
470                task.setProject(getAntProject());
471                task.setTaskName(taskName);
472    
473                return task;
474            }
475            catch (Exception e) {
476                log.log(Level.WARNING, "Could not create task: " + taskName + ". Reason: " + e, e);
477                return null;
478            }
479        }
480    
481        protected Task getParentTask() {
482            Object current = getCurrent();
483            if (current instanceof Task) {
484                return (Task) current;
485            }
486            return null;
487        }
488    }