Merge pull request #4735 from cg110/fix_web_server_mem_leak
[vuplus_xbmc] / tools / codegenerator / Helper.groovy
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21 import groovy.xml.XmlUtil
22 import org.apache.commons.lang.StringEscapeUtils
23
24 import groovy.text.SimpleTemplateEngine
25 import groovy.text.SimpleTemplateEngine
26 import java.util.regex.Pattern
27
28 /**
29  * This class contains a series of helper methods for parsing a xbmc addon spec xml file. It is intended to be
30  * used from a bindings template.
31  * 
32  * @author jim
33  *
34  */
35 public class Helper
36 {
37    private static List classes
38    private static Map outTypemap = [:]
39    private static def defaultOutTypeConversion = null
40    private static Map inTypemap = [:]
41    private static def defaultInTypeConversion = null
42    private static def doxygenXmlDir = null
43    public static String newline = System.getProperty("line.separator");
44    public static File curTemplateFile = null;
45
46    public static void setTempateFile(File templateFile) { curTemplateFile = templateFile }
47
48    /**
49     * In order to use any of the typemap helper features, the Helper class needs to be initialized with
50     * this information.
51     * @param pclasses is the list of all class nodes from the module
52     * @param poutTypemap is the typemap table for output return values to the scripting language
53     * @param defaultOutTypemap is the default typemap to use when the type conversion is unknown
54     * @param pinTypemap is the typemap table for input parameters from the scripting language
55     * @param defaultInTypemap is the default typemap for the input parameters from the scripting language
56     */
57     public static void setup(def template,List pclasses, Map poutTypemap, def defaultOutTypemap,
58                              Map pinTypemap, def defaultInTypemap)
59     {
60       setTempateFile(template.binding.templateFile)
61       classes = pclasses ? pclasses : []
62       if (poutTypemap) outTypemap.putAll(poutTypemap)
63       if (defaultOutTypemap) defaultOutTypeConversion = defaultOutTypemap
64       if (pinTypemap) inTypemap.putAll(pinTypemap)
65       if (defaultInTypemap) defaultInTypeConversion = defaultInTypemap
66     }
67
68    public static class Sequence
69    {
70      private long cur = 0;
71
72      public long increment() { return ++cur }
73    }
74
75   private static ThreadLocal<Sequence> curSequence = new ThreadLocal<Sequence>();
76
77   public static void setDoxygenXmlDir(File dir) { doxygenXmlDir = dir }
78
79   private static String retrieveDocStringFromDoxygen(Node methodOrClass)
80   { 
81     if (doxygenXmlDir == null)
82       return null
83
84     Node doc = null
85     def ret = ''
86
87     // make the class name or namespace
88     String doxygenId = findFullClassName(methodOrClass,'_1_1')
89     boolean isInClass = doxygenId != null
90     if (!doxygenId)
91       doxygenId = findNamespace(methodOrClass,'_1_1',false)
92     doxygenId = (isInClass ? 'class' : 'namespace') + doxygenId
93
94     String doxygenFilename = doxygenId + '.xml'
95     File doxygenXmlFile = new File(doxygenXmlDir,doxygenFilename)
96     if (! doxygenXmlFile.exists())
97     {
98       System.out.println("WARNING: Cannot find doxygen file for ${methodOrClass.toString()} which should be \"${doxygenXmlFile}\"")
99       return null
100     }
101
102     Node docspec = (new XmlParser().parse(doxygenXmlFile))
103     if (methodOrClass.name() == 'class')
104       doc = docspec.compounddef[0].detaileddescription[0]
105     else // it's a method of some sort ... or it better be
106     {
107       Node memberdef = docspec.depthFirst().find { 
108         return (it instanceof String) ? false :
109           ((it.name() == 'memberdef' && (it.@kind == 'function' || it.@kind == 'variable') && it.@id.startsWith(doxygenId)) &&
110            (it.name != null && it.name.text().trim() == methodOrClass.@sym_name))
111       }
112
113       doc = memberdef != null ? memberdef.detaileddescription[0] : null
114     }
115
116     if (doc != null)
117     { 
118       def indent = '    '
119       def curIndent = ''
120       def prevIndent = ''
121
122       def handleDoc
123       handleDoc = { 
124         if (it instanceof String)
125           ret += it
126         else // it's a Node
127         {
128           if (it.name() == 'detaileddescription')
129             it.children().each handleDoc
130           else if (it.name() == 'para')
131           {
132             it.children().each handleDoc
133             ret += (it.parent()?.name() == 'listitem') ? newline : (newline + newline)
134           }
135           else if (it.name() == 'ref' || it.name() == "ulink")
136             ret += (it.text() + ' ')
137           else if (it.name() == 'verbatim')
138             ret += it.text().denormalize()
139           else if (it.name() == 'itemizedlist')
140           {
141             ret += newline
142             prevIndent = curIndent
143             curIndent += indent
144             it.children().each handleDoc
145             curIndent = prevIndent
146           }
147           else if (it.name() == 'listitem')
148           {
149             ret += (curIndent + '- ')
150             it.children().each handleDoc
151           }
152           else if (it.name() == 'linebreak')
153             ret += newline
154           else if (it.name() == 'ndash')
155             ret += "--"
156           else if (it.name() == 'emphasis')
157           {
158             ret += '*'
159             it.children().each handleDoc
160           }
161           else
162             System.out.println("WARNING: Cannot parse the following as part of the doxygen processing:" + XmlUtil.serialize(it))
163         }
164       }
165
166       doc.children().each handleDoc
167     }
168
169     return ret.denormalize()
170   }
171
172    /**
173     * <p>This method uses the previously set outTypemap and defaultOutTypemap to produce the chunk of code
174     * that will be used to return the method invocation result to the scripting language. For example, in 
175     * python, if the return type from the method is a long, then the OutConversion could look something like:</p>
176     * <code>
177     *    result = PyInt_FromLong(thingReturnedFromMethod);
178     * </code>
179     * <p>This could have resulted from a mini-template stored as the way to handle 'long's in the outTypemap:</p>
180     * <code>
181     *    ${result} = PyInt_FromLong(${api});
182     * </code>
183     * @param apiType - is the Swig typecode that describes the return type from the native method
184     * @param method - is the node from the module xml that contains the method description
185     * @return the code chunk as a string ready to be placed into the generated code.
186     */
187    public static String getOutConversion(String apiType, String apiName, Node method, Map overrideBindings = null, boolean recurse = true)
188    {
189       def convertTemplate = outTypemap[apiType]
190
191       //    String apiLType = SwigTypeParser.convertTypeToLType(apiType)
192       //    if (convertTemplate == null) convertTemplate = outTypemap[apiLType]
193
194       // is the returns a pointer to a known class
195       String className = null
196       if (convertTemplate == null && apiType.startsWith('p.'))
197       {
198         Node classNode = findClassNodeByName(parents(method)[0], SwigTypeParser.getRootType(apiType),method)
199         if (classNode)
200         {
201           className = findFullClassName(classNode)
202           convertTemplate = defaultOutTypeConversion
203         }
204       }
205
206       if (convertTemplate == null)
207       {
208         // Look for Pattern for keys that might fit
209         convertTemplate = outTypemap.find({ key, value -> (key instanceof Pattern && key.matcher(apiType).matches()) })?.value
210       }
211
212       if (!convertTemplate)
213       {
214         String knownApiType = isKnownApiType(apiType,method)
215         if (knownApiType)
216         {
217           convertTemplate = defaultOutTypeConversion
218           className = knownApiType
219         }
220       }
221
222       if (!convertTemplate)
223       {
224         // check the typedef resolution
225         String apiTypeResolved = SwigTypeParser.SwigType_resolve_all_typedefs(apiType)
226         if (!apiTypeResolved.equals(apiType))
227           return getOutConversion(apiTypeResolved, apiName, method, overrideBindings, recurse)
228
229         if (recurse)
230           return getOutConversion(SwigTypeParser.SwigType_ltype(apiType),apiName,method,overrideBindings,false)
231         else if (!isKnownApiType(apiType,method))
232            throw new RuntimeException("WARNING: Cannot convert the return value of swig type ${apiType} for the call ${Helper.findFullClassName(method) + '::' + Helper.callingName(method)}")
233       }
234
235       boolean seqSetHere = false
236       Sequence seq = curSequence.get()
237       if (seq == null)
238       {
239         seqSetHere = true
240         seq = new Sequence()
241         curSequence.set(seq)
242       }
243
244       Map bindings = ['result' : apiName, 'api' : 'apiResult', 'type' : "${apiType}",
245                       'method' : method, 'helper' : Helper.class, 
246                       'swigTypeParser' : SwigTypeParser.class,
247                       'sequence' : seq ]
248       if (className) bindings['classname'] = className
249
250       if (overrideBindings) bindings.putAll(overrideBindings)
251
252       if (convertTemplate instanceof List) /// then we expect the template string/file to be the first entry
253       {
254         Map additionalBindings = convertTemplate.size() > 1 ? convertTemplate[1] : [:]
255         bindings.putAll(additionalBindings)
256         convertTemplate = convertTemplate[0]
257       }
258
259       if (File.class.isAssignableFrom(convertTemplate.getClass()))
260       {
261         File cur = (File)convertTemplate
262         if (!cur.exists()) // see if the file is relative to the template file
263         { 
264           File parent = curTemplateFile.getParentFile()
265           // find the relative path to the convertTemplate
266           File cwd = new File('.').getCanonicalFile()
267           String relative = cwd.getAbsoluteFile().toURI().relativize(convertTemplate.getAbsoluteFile().toURI()).getPath()
268           convertTemplate = new File(parent,relative)
269
270           // This is a fallback case which is hit occationally on OSX as a result
271           // of case mismatches between the two paths in the relativize call above.
272           if (!convertTemplate.exists())
273             convertTemplate = new File(parent,cur.toString())
274         }
275       }
276
277       if (seqSetHere) curSequence.set(null)
278       return new SimpleTemplateEngine().createTemplate(convertTemplate).make(bindings).toString()
279    }
280
281    /**
282     * <p>This method uses the previously set inTypemap and defaultInTypemap to produce the chunk of code
283     * that will be used to convert input parameters from the scripting language to the native method invocation
284     * parameters. For example, if the native call takes a 'String' then the InConversion could look something like:</p>
285     * <code>
286     *    if (pythonStringArgument) PyXBMCGetUnicodeString(cArgument,pythonStringArgument,"cArgumentName");
287     * </code>
288     * <p>This could have resulted from a mini-template stored as the way to handle 'long's in the outTypemap:</p>
289     * <code>
290     *    if (${slarg}) PyXBMCGetUnicodeString(${api},${slarg},"${api}");
291     * </code>
292     * @param apiType - is the Swig typecode that describes the parameter type from the native method
293     * @param apiName - is the name of the parameter from the method parameter list in the api
294     * @param slName - is the name of the varialbe that holds the parameter from the scripting language
295     * @param method - is the node from the module xml that contains the method description
296     * @return the code chunk as a string ready to be placed into the generated code.
297     */
298    public static String getInConversion(String apiType, String apiName, String slName, Node method, Map overrideBindings = null)
299    {
300      return getInConversion(apiType, apiName, apiName, slName, method, overrideBindings);
301    }
302
303    /**
304     * <p>This method uses the previously set inTypemap and defaultInTypemap to produce the chunk of code
305     * that will be used to convert input parameters from the scripting language to the native method invocation
306     * parameters. For example, if the native call takes a 'String' then the InConversion could look something like:</p>
307     * <code>
308     *    if (pythonStringArgument) PyXBMCGetUnicodeString(cArgument,pythonStringArgument,"cArgumentName");
309     * </code>
310     * <p>This could have resulted from a mini-template stored as the way to handle 'long's in the outTypemap:</p>
311     * <code>
312     *    if (${slarg}) PyXBMCGetUnicodeString(${api},${slarg},"${api}");
313     * </code>
314     * @param apiType - is the Swig typecode that describes the parameter type from the native method
315     * @param apiName - is the name of the varialbe that holds the api parameter
316     * @param paramName - is the name of the parameter from the method parameter list in the api
317     * @param slName - is the name of the varialbe that holds the parameter from the scripting language
318     * @param method - is the node from the module xml that contains the method description
319     * @return the code chunk as a string ready to be placed into the generated code.
320     */
321    public static String getInConversion(String apiType, String apiName, String paramName, String slName, Node method, Map overrideBindings = null)
322    {
323       def convertTemplate = inTypemap[apiType]
324
325       String apiLType = SwigTypeParser.convertTypeToLTypeForParam(apiType)
326
327       if (convertTemplate == null) convertTemplate = inTypemap[apiLType]
328
329       // is the returns a pointer to a known class
330       if (convertTemplate == null && apiType.startsWith('p.'))
331       {
332          // strip off rval qualifiers
333          String thisNamespace = Helper.findNamespace(method)
334          Node clazz = classes.find { Helper.findFullClassName(it) == apiLType.substring(2) ||
335             (it.@sym_name == apiLType.substring(2) && thisNamespace == Helper.findNamespace(it)) }
336          if (clazz != null) convertTemplate = defaultInTypeConversion
337       }
338
339       // Look for Pattern for keys that might fit
340       if (convertTemplate == null)
341         convertTemplate = inTypemap.find({ key, value -> (key instanceof Pattern && key.matcher(apiType).matches()) })?.value
342
343       // Try the LType
344       if (convertTemplate == null)
345         convertTemplate = inTypemap.find({ key, value -> (key instanceof Pattern && key.matcher(apiLType).matches()) })?.value
346
347       if (!convertTemplate)
348       {
349          // check the typedef resolution
350          String apiTypeResolved = SwigTypeParser.SwigType_resolve_all_typedefs(apiType)
351          if (!apiTypeResolved.equals(apiType))
352            return getInConversion(apiTypeResolved, apiName, paramName, slName, method, overrideBindings)
353
354          // it's ok if this is a known type
355          if (!isKnownApiType(apiType,method) && !isKnownApiType(apiLType,method))
356            System.out.println("WARNING: Unknown parameter type: ${apiType} (or ${apiLType}) for the call ${Helper.findFullClassName(method) + '::' + Helper.callingName(method)}")
357          convertTemplate = defaultInTypeConversion
358       }
359
360       if (convertTemplate)
361       {
362          boolean seqSetHere = false
363          Sequence seq = curSequence.get()
364          if (seq == null)
365          {
366            seqSetHere = true
367            seq = new Sequence()
368            curSequence.set(seq)
369          }
370
371          Map bindings = [
372                   'type': "${apiType}", 'ltype': "${apiLType}",
373                   'slarg' : "${slName}", 'api' : "${apiName}",
374                   'param' : "${paramName}",
375                   'method' : method, 'helper' : Helper.class, 
376                   'swigTypeParser' : SwigTypeParser.class,
377                   'sequence' : seq
378                ]
379
380          if (overrideBindings) bindings.putAll(overrideBindings)
381
382          if (convertTemplate instanceof List) /// then we expect the template string/file to be the first entry
383          {
384             Map additionalBindings = convertTemplate.size() > 1 ? convertTemplate[1] : [:]
385             bindings.putAll(additionalBindings)
386             convertTemplate = convertTemplate[0]
387          }
388
389          if (File.class.isAssignableFrom(convertTemplate.getClass()))
390          {
391            File cur = (File)convertTemplate
392            if (!cur.exists()) // see if the file is relative to the template file
393            { 
394              File parent = curTemplateFile.getParentFile()
395              // find the relative path to the convertTemplate
396              File cwd = new File('.').getCanonicalFile()
397              String relative = cwd.getAbsoluteFile().toURI().relativize(convertTemplate.getAbsoluteFile().toURI()).getPath()
398              convertTemplate = new File(parent,relative)
399
400              // This is a fallback case which is hit occationally on OSX as a result
401              // of case mismatches between the two paths in the relativize call above.
402              if (!convertTemplate.exists())
403                convertTemplate = new File(parent,cur.toString())
404            }
405          }
406
407          if (seqSetHere) curSequence.set(null);
408          return new SimpleTemplateEngine().createTemplate(convertTemplate).make(bindings).toString()
409       }
410
411       return ''
412    }
413
414    static def ignoreAttributes = ['classes', 'symtab', 'sym_symtab',
415       'sym_overname', 'options', 'sym_nextSibling', 'csym_nextSibling',
416       'sym_previousSibling' ]
417
418    /**
419     * <p>Transform a Swig generated xml file into something more manageable. For the most part this method will:</p>
420     * 
421     * <li>1) Make all pertinent 'attributelist' elements actually be attributes of the parent element while
422     * an attribute with the name 'name' will become that parent element name.</li>
423     * <li>2) Filter out unused attributes</li>
424     * <li>3) Filter out the automatically included 'swig'swg'</li>
425     * <li>4) Flatten out the remaining 'include' elements</li>
426     * <li>5) Removes extraneous default argument function/method Node</li> 
427     * <li>6) Converts all type tables to a single entry under the main module node removing all 1-1 mappings.</li>
428     * <li>7) Removes all non-public non-constructor methods.</li>
429     * @param swigxml is the raw swig output xml document
430     * @return the transformed document
431     */
432    public static Node transformSwigXml(Node swigxml)
433    {
434       Node node = transform(swigxml,
435             {
436                // do not include the 'include' entry that references the default swig.swg file.
437                !(it.name() == 'include' &&
438                      // needs to also contain an attribute list with an attribute 'name' that matches the swig.swg file
439                      it.find {
440                         it.name() == 'attributelist' && it.find {
441                            it.name() == 'attribute' && it.@name == 'name' && it.@value =~ /swig\.swg$/
442                         }
443                      } ||
444                      // also don't include any typescope entries
445                      it.name() == 'typescopesitem' || it.name() == 'typetabsitem'
446                      )
447             },{ key, value -> !ignoreAttributes.contains(key) })
448
449       // now make sure the outer most node is an include and there's only one
450       assert node.include?.size() == 1 && node.include[0].module?.size() == 1 && node.include[0].module[0]?.@name != null,
451       "Invalid xml doc result. Expected a single child node of the root node call 'include' with a single 'module' child node but got " + XmlUtil.serialize(node)
452
453       // create an outermost 'module' node with the correct name
454       Node ret = new Node(null, 'module', [ 'name':node.include[0].module[0].@name] )
455       node.include[0].children().each { if (it.name() != 'module') ret.append(it) }
456
457       // flatten out all other 'include' elements, parmlists, and typescopes
458       flatten(ret,['include', 'parmlist', 'typescope' ])
459
460       // remove any function nodes with default arguments
461       List tmpl = []
462       tmpl.addAll(ret.depthFirst())
463       for (Node cur : tmpl)
464       {
465          if ((cur.name() == 'function' || cur.name() == 'constructor') && cur.@defaultargs != null)
466             cur.parent().remove(cur)
467       }
468
469       // now validate that no other methods are overloaded since we can't handle those right now.
470       functionNodesByOverloads(ret).each { key, value -> assert value.size() == 1, "Cannot handle overloaded methods unless simply using defaulting: " + value }
471
472       // now gather up all of the typetabs and add a nice single
473       // typetab entry with a better format in the main module
474       List allTypetabs = ret.depthFirst().findAll { it.name() == 'typetab' }
475       Node typenode = new Node(ret,'typetab')
476       allTypetabs.each {
477          it.attributes().each { key, value ->
478             if (key != 'id' && key != value)
479             {
480                Node typeentry = new Node(null,'entry')
481                String namespace = findNamespace(it)
482                typeentry.@namespace = namespace != null ? namespace.trim() : ''
483                typeentry.@type = key
484                typeentry.@basetype = value
485
486                if (typenode.find({ it.@basetype == typeentry.@basetype && it.@namespace == typeentry.@namespace }) == null)
487                   typenode.append(typeentry)
488             }
489          }
490          it.parent().remove(it)
491       }
492       
493       // now remove all non-public methods, but leave constructors
494       List allMethods = ret.depthFirst().findAll({ it.name() == 'function' || it.name() == 'destructor' || it.name() == 'constructor'})
495       allMethods.each {
496          if (it.@access != null && it.@access != 'public' && it.name() != 'constructor')
497             it.parent().remove(it)
498          else
499          {
500            def doc = retrieveDocStringFromDoxygen(it)
501            if (doc != null && doc != '' && doc.trim() != ' ')
502              new Node(it,'doc',['value' : doc])
503          }
504       }
505       
506       // now remove all non-public variables
507       List allVariables = ret.depthFirst().findAll({ it.name() == 'variable' })
508       allVariables.each {
509          if (it.@access != null && it.@access != 'public')
510             it.parent().remove(it)
511          else
512          {
513            def doc = retrieveDocStringFromDoxygen(it)
514            if (doc != null && doc != '' && doc.trim() != ' ')
515              new Node(it,'doc',['value' : doc])
516          }
517       }
518
519       // add the doc string to the classes
520       List allClasses = ret.depthFirst().findAll({ it.name() == 'class'})
521       allClasses.each {
522         def doc = retrieveDocStringFromDoxygen(it)
523         if (doc != null && doc != '' && doc.trim() != ' ')
524           new Node(it,'doc',['value' : doc])
525       }
526
527       return ret
528    }
529
530    /**
531     * @return true if the class node has a defined constructor. false otherwise.
532     */
533    public static boolean hasDefinedConstructor(Node clazz)
534    {
535       return (clazz.constructor != null && clazz.constructor.size() > 0)
536    }
537
538    /**
539     * @return true id this Node has a docstring associated with it.
540     */
541    public static boolean hasDoc(Node methodOrClass)
542    {
543      return methodOrClass.doc != null && methodOrClass.doc[0] != null && methodOrClass.doc[0].@value != null
544    }
545
546    /**
547     * @return true of the class node has a constructor but it's hidden (not 'public'). false otherwise.
548     */
549    public static boolean hasHiddenConstructor(Node clazz)
550    {
551       return (hasDefinedConstructor(clazz) && clazz.constructor[0].@access != null && clazz.constructor[0].@access != 'public')
552    }
553    
554    /**
555     * <p>This will look through the entire module and look up a class node by name. It will return null if
556     * that class node isn't found. It's meant to be used to look up base classes from a base class list
557     * so it's fairly robust. It goes through the following rules:</p>
558     * 
559     * <li>Does the FULL classname (considering the namespace) match the name provided.</li>
560     * <li>Does the FULL classname match the reference nodes namespace + '::' + the provided classname.</li>
561     * <li>Does the class node's name (which may contain the full classname) match the classname provided.</li>
562     * 
563     * <p>Note, this method is not likely to find the classnode if you just pass a simple name and
564     * no referenceNode in the case where namespaces are used extensively.</p> 
565     */
566    public static Node findClassNodeByName(Node module, String classname, Node referenceNode = null)
567    {
568       return module.depthFirst().findAll({ it.name() == 'class' }).find {
569          // first check to see if this FULL class name matches
570          if (findFullClassName(it).trim() == classname.trim()) return true
571             
572          // now check to see if it matches the straight name considering the reference node
573          if (referenceNode != null && (findNamespace(referenceNode) + classname) == findFullClassName(it)) return true
574          
575          // now just see if it matches the straight name
576          if (it.@name == classname) return true
577          
578          return false
579       }
580    }
581    
582    /**
583     * Find me the class node that this node either is, or is within.
584     * If this node is not within a class node then it will return null.
585     */
586    public static Node findClassNode(Node node)
587    {
588       if (node.name() == 'class') return node
589       return node.parent() == null ? null : findClassNode(node.parent())
590    }
591    
592    /**
593     * If this node is a class node, or a child of a class name (for example, a method) then
594     * the full classname, with the namespace will be returned. Otherwise, null.
595     */
596    public static String findFullClassName(Node node, String separator = '::')
597    {
598       String ret = null
599       List rents = parents(node, { it.name() == 'class' })
600       if (node.name() == 'class') rents.add(node)
601       rents.each {
602          if (ret == null)
603             ret = it.@sym_name
604          else
605             ret += separator + it.@sym_name
606       }
607
608       return ret ? findNamespace(node,separator) + ret : null
609    }
610
611    /**
612     * Given the Node this method looks to see if it occurs within namespace and returns
613     * the namespace as a String. It includes the trailing '::'
614     */
615    public static String findNamespace(Node node, String separator = '::', boolean endingSeparator = true)
616    {
617       String ret = null
618       parents(node, { it.name() == 'namespace' }).each {
619          if (ret == null)
620             ret = it.@name
621          else
622             ret += separator + it.@name
623       }
624
625       return ret == null ? '' : (ret + (endingSeparator ? separator : ''))
626    }
627
628    /**
629     * Gather up all of the parent nodes in a list ordered from the top node, down to the
630     * node that's passed as a parameter.
631     */
632    public static List parents(Node node, Closure filter = null, List ret = null)
633    {
634       Node parent = node.parent()
635       if (parent != null)
636       {
637          ret = parents(parent,filter,ret)
638          if (filter == null || filter.call(parent))
639             ret += parent
640       }
641       else if (ret == null)
642          ret = []
643
644       return ret
645    }
646
647    /**
648     * Group together overloaded methods into a map keyed by the first method's id. Each
649     * entry in this map contains a list of nodes that represent overloaded versions of 
650     * the same method. 
651     */
652    public static Map functionNodesByOverloads(Node module)
653    {
654       // find function nodes
655       Map ret = [:]
656       module.depthFirst().each {
657          if (it.name() == 'function' || it.name() == 'constructor' || it.name() == 'destructor')
658          {
659             String id = it.@sym_overloaded != null ? it.@sym_overloaded : it.@id
660             if (ret[id] == null) ret[id] = [it]
661             else ret[id] += it
662          }
663       }
664       return ret
665    }
666
667    /**
668     * Because the return type is a combination of the function 'decl' and the
669     * function 'type,' this method will construct a valid Swig typestring from 
670     * the two.
671     */
672    public static String getReturnSwigType(Node method)
673    {
674       // we're going to take a shortcut here because it appears only the pointer indicator
675       // ends up attached to the decl.
676       String prefix = (method.@decl != null && method.@decl.endsWith('.p.')) ? 'p.' : ''
677       return method.@type != null ? prefix + method.@type : 'void'
678    }
679
680    /**
681     * Given the method node this will produce the actual name of the method as if
682     * it's being called. In the case of a constructor it will do a 'new.' In the
683     * case of a destructor it will produce a 'delete.'
684     */
685    public static String callingName(Node method)
686    {
687       // if we're not in a class we need the fully qualified name
688       String clazz = findFullClassName(method)
689       // if we're in a class then we are going to assume we have a 'self' pointer
690       // that we are going to invoke this on.
691       if (clazz == null)
692          return method.@name
693
694       if (method.name() == 'constructor')
695          return "new ${findNamespace(method)}${method.@sym_name}"
696
697       if (method.name() == 'destructor')
698          return 'delete'
699
700       // otherwise it's just a call on a class being invoked on an instance
701       return method.@name
702    }
703
704    /**
705     * Swig has 'insert' nodes in it's parse tree that contain code chunks that are 
706     * meant to be inserted into various positions in the generated code. This method
707     * will extract the nodes that refer to the specific section asked for. See the
708     * Swig documentation for more information.
709     */
710    public static List getInsertNodes(Node module, String section)
711    {
712       return module.insert.findAll { section == it.@section || (section == 'header' && it.@section == null) }
713    }
714
715    public static String unescape(Node insertSection) { return unescape(insertSection.@code) }
716
717    public static String unescape(String insertSection) { return StringEscapeUtils.unescapeHtml(insertSection) }
718
719    public static boolean isDirector(Node method)
720    { 
721      Node clazz = findClassNode(method)
722      if (!clazz || !clazz.@feature_director)
723        return false
724      if (method.name() == 'destructor')
725        return false
726      if (method.name() == 'constructor')
727        return false
728      return method.@storage && method.@storage == 'virtual'
729    }
730
731    /**
732     * This method will search from the 'searchFrom' Node up to the root
733     *  looking for a %feature("knownbasetypes") declaration that the given 'type' is 
734     *  known for 'searchFrom' Node.
735     */
736    public static boolean isKnownBaseType(String type, Node searchFrom) 
737    {
738      return hasFeatureSetting(type,searchFrom,'feature_knownbasetypes',{ it.split(',').find({ it.trim() == type }) != null })
739    }
740
741    /**
742     * This method will search from the 'searchFrom' Node up to the root
743     *  looking for a %feature("knownapitypes") declaration that the given 'type' is 
744     *  known for 'searchFrom' Node.
745     */
746    public static String isKnownApiType(String type, Node searchFrom)
747    {
748      String rootType = SwigTypeParser.getRootType(type)
749      String namespace = findNamespace(searchFrom,'::',false)
750      String lastMatch = null
751      hasFeatureSetting(type,searchFrom,'feature_knownapitypes',{ it.split(',').find(
752        { 
753          if (it.trim() == rootType)
754          {
755            lastMatch = rootType
756            return true
757          }
758          // we assume the 'type' is defined within namespace and 
759          //  so we can walk up the namespace appending the type until 
760          //  we find a match.
761          while (namespace != '')
762          {
763 //           System.out.println('checking ' + (namespace + '::' + rootType))
764            if ((namespace + '::' + rootType) == it.trim())
765            {
766              lastMatch = it.trim()
767              return true
768            }
769            // truncate the last namespace
770            int chop = namespace.lastIndexOf('::')
771            namespace = (chop > 0) ? namespace.substring(0,chop) : ''
772          }
773          return false
774        }) != null })
775      return lastMatch
776    }
777
778    private static String hasFeatureSetting(String type, Node searchFrom, String feature, Closure test)
779    {
780      if (!searchFrom)
781        return null
782
783      Object attr = searchFrom.attribute(feature)
784      if (attr && test.call(attr))
785        return attr.toString()
786
787      return hasFeatureSetting(type,searchFrom.parent(),feature,test)
788    }
789
790    private static void flatten(Node node, List elementsToRemove)
791    {
792       for (boolean done = false; !done;)
793       {
794          done = true
795          for (Node child : node.breadthFirst())
796          {
797             if (elementsToRemove.contains(child.name()))
798             {
799                Node parent = child.parent()
800                parent.remove(child)
801                child.each { parent.append(it) }
802                done = false
803                break
804             }
805          }
806       }
807    }
808
809    private static Node transform(Node node, Closure nodeFilter, Closure attributeFilter)
810    {
811       // need to create a map and a list of nodes (which will be children) from the
812       // attribute list.
813       Map attributes = [:]
814       List nodes = []
815       node.each {
816          if (nodeFilter == null || nodeFilter.call(it) == true)
817          {
818             if (it.name() == 'attributelist')
819             {
820                Tuple results = transformSwigAttributeList(it)
821                attributes.putAll(results[0].findAll(attributeFilter))
822                List childNodes = results[1]
823                childNodes.each {
824                   if (nodeFilter != null && nodeFilter.call(it) == true)
825                      nodes.add(transform(it,nodeFilter,attributeFilter))
826                }
827             }
828             else
829                nodes.add(transform(it,nodeFilter,attributeFilter))
830          }
831       }
832
833       // transfer the addr attribute from the original node over to the 'id' attribute of the
834       // new node by adding it to the attributes map
835       if (node.@addr)
836       {
837         // copy over the other attributes
838         node.attributes().findAll { key,value -> if (key != 'addr' && key != 'id') attributes[key] = value }
839         attributes['id'] = node.@addr
840       }
841
842       // In the case when the Node is a cdecl, we really want to replace the node name
843       // with the 'kind' attribute value.
844       Node ret
845       if (node.name() == 'cdecl' && attributes.containsKey('kind'))
846          ret = new Node(null, attributes['kind'], attributes.findAll({key, value -> key != 'kind' } ))
847       else
848          ret = new Node(null, node.name(), attributes)
849       nodes.each { ret.append(it) }
850       return ret
851    }
852
853    private static Tuple transformSwigAttributeList(Node attributeList)
854    {
855       Map attributes = [:]
856       List nodes = []
857       attributeList.each {
858          if (it.name() == 'attribute')
859             attributes[it.@name] = it.@value
860          else
861             nodes.add(it)
862       }
863       return [attributes, nodes]
864    }
865 }
866