# JythonBook / appendixB.rst

 Josh Juneau d9b2bb8 2010-02-01   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 Appendix B: Jython Cookbook - A compilation of community submitted code examples ================================================================================= There are a plethora of examples for using Jython that can be found on the web. This appendix is a compilation of some of the most useful examples that we have found. There are hundreds of examples available on the web. These that were chosen are focused on topics that are not widely covered elsewhere on the web. Unless otherwise noted, each of these examples have been originally authored for working on versions of Jython prior to 2.5.x but we have tested each of them using Jython 2.5.1 and function as advertised. Logging ------- Using log4j With Jython - Josh Juneau ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Are you still using the Jython print command to show your errors? How about in a production environment, are you using any formal logging? If not, you should be doing so...and the Apache log4j API makes it easy to do so. Many Java developers have grown to love the log4j API and it is utilized throughout much of the community. That is great news for Jython developers since we've got direct access to Java libraries! **Setting Up Your Environment** The most difficult part about using log4j with Jython is the setup. You must ensure that the log4j.jar archive resides somewhere within your Jython PATH (usually this entails setting the CLASSPATH to include necessary files). You then set up a properties file for use with log4j. Within the properties file, you can include appender information, where logs should reside, and much more. *Example properties file:* :: log4j.rootLogger=debug, stdout, R log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout # Pattern to output the caller's file name and line number. log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n log4j.appender.R=org.apache.log4j.RollingFileAppender log4j.appender.R.File=C:\\Jython\\testlog4j.log log4j.appender.R.MaxFileSize=100KB # Keep one backup file log4j.appender.R.MaxBackupIndex=1 log4j.appender.R.layout=org.apache.log4j.PatternLayout log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n You are now ready to use log4j in your Jython application. As you can see, if you've ever used log4j with Java, it is pretty much the same. **Using log4j in a Jython Application** Once again, using log4j within a Jython application is very similar to it's usage in the Java world. First, you must import the log4j packages: :: from org.apache.log4j import * Second, you obtain a new logger for your class or module and set up a PropertyConfigurator: :: logger = Logger.getLogger("myClass") # Assume that the log4j properties resides within a folder named "utilities" PropertyConfigurator.configure(sys.path[0] + "/utilities/log4j.properties") Lastly, use log4j: :: # Example module within the class: def submitDocument(self, event): try: # Assume we perform some SQL here except SQLException, ex: self.logger.error("docPanel#submitDocument ERROR: %s" % (ex)) Your logging will now take place within the file you specified in the properties file for log4j.appender.R.File. **Using log4j in Jython Scripts** Many may ask, why in the world would you be interested in logging information about your scripts? Most of the time a script is executed interactively via the command line. However, there are plenty of instances where it makes sense to have the system invoke a script for you. As you probably know, this technique is used quite often within an environment to run nightly tasks, or even daily tasks which are automatically invoked on a scheduled basis. For these cases, it can be extremely useful to log errors or information using log4j. Some may even wish to create a separate automated task to email these log files after the tasks complete. The overall implementation is the same as above, the most important thing to remember is that you must have the log4j.jar archive and properties file within your Jython path. Once this is ready to go you can use log4j in your script. :: from org.apache.log4j import * logger = Logger.getLogger("scriptname") PropertyConfigurator.configure("C:\path_to_properties\log4j.properties") logger.info("Test the logging") Author: Josh Juneau URL: http://wiki.python.org/jython/JythonMonthly/Articles/August2006/1 Another log4j Example - Greg Moore ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This example require several things. - log4j on the classpath - log4j.properties (below) in the same directory as the example - example.xml (below) in the same directory as the example below - And of course Jython **log4j Example** This is a simple example to show how easy it is to use log4j in your own scripts. The source is well documented but if you have any questions or want to more info use your favorite search engine and type in log4j. :: from org.apache.log4j import * class logtest: def __init__(self): log.info("start of Logtest") log.debug('just before file read') try: log.warn('file read proceding to processing') xmlStringData = open('example.xml').read() except: #yes, more could have been done here but this is just an example log.error('file read FAILURE') log.info('file read proceding to processing') # since this is just an example processing would go here. log.warn('its just an example, OK?') pi = 3.141592681 msg = 'do you like?' + str(pi) log.info(msg) log.debug('lets try to parse the string') if '[CDATA' in xmlStringData: log.warn('No CDATA section.') #say good bye and close the log file. log.info('That all. The End. Good Bye') log.shutdown() if __name__ == '__main__': # loggingTest is just a string that identifies this log. log = Logger.getLogger("loggingTest") #use the config data in the properties file PropertyConfigurator.configure('log4j.properties') log.info('This is the start of the log file') logit = logtest() print '\n\nif you change the log level in the properties' print "file you'll get varing amouts of log data." **log4j.properties** This file is required by the code above. it need to be in the same directory as the example however It can be anywhere as log as you provide a full path to the file. It configures how log4j operates. If it is not found it defaults to a default logging level. Since this is for example purposes the file below is larger then really needed. :: #define loging level and output log4j.rootLogger=debug, stdout, LOGFILE #log4j.rootLogger=info, LOGFILE # this 2 lines tie the apache logging into log4j #log4j.logger.org.apache.axis.SOAPPart=DEBUG #log4j.logger.httpclient.wire.header=info #log4j.logger.org.apache.commons.httpclient=DEBUG # where is the logging going. # This is for std out and defines the log output format log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} | %p | [%c] %m%n %t #log it to a file as well. and define a filename, max file size and number of backups log4j.appender.LOGFILE=org.apache.log4j.RollingFileAppender log4j.appender.LOGFILE.File=jythonTest.log log4j.appender.LOGFILE.MaxFileSize=100KB # Keep one backup file log4j.appender.LOGFILE.MaxBackupIndex=1 log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout # Pattern for logfile - only diff is that date is added log4j.appender.LOGFILE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} | %p | [%c] %m%n # Other Examples: only time, loglog level, loggerName #log4j.appender.LOGFILE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss},%p,%c %m%n #above plus filename, linenumber, Class Name, method name #log4j.appender.LOGFILE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss},%p,%c,%F,%L,%C{1},%M %m%n **Example xml file** This is here for completeness. Any text file could be use with the example above by changing the 'open' line in the above line. :: W I TU Cathrine Knight 34-5424-77 10/12/1938
4780 Centerville Saint Paul, MN 55127
' entry.description = description entries.add(entry) feed.entries = entries ############################### # Publish the XML ############################### writer = FileWriter(self.fileName) output = SyndFeedOutput() output.output(feed,writer) writer.close() print('The feed has been written to the file') ok = True except Exception, e: print 'There has been an exception raised',e if ok == False: print 'Feed Not Printed' if __name__== "__main__": #################################### # You must change his file location # if not using Windows environment #################################### writer = FeedWriter('rss_2.0','C:\\TEMP\\testRss.xml') writer.writeFeed() print '****************Command Complete...RSS XML has been created*****************' After you have created the XML, you'll obviously need to place it on a web server somewhere so that others can use your feed. The FeedWriter.py module would probably be one module amongst many in an application for creating and managing RSS Feeds, but you get the idea. **Conclusion** As you can see, using the ROME library to work with RSS feeds is quite easy. Using the ROME library within a Jython application is straight forward. As you have now seen how easy it is to create and parse feeds, you can apply these technologies to a more complete RSS management application if you'd like. The world of RSS communication is at your fingertips! Author: Josh Juneau URL: http://wiki.python.org/jython/JythonMonthly/Articles/October2007/1 Using the CLASSPATH - Steve Langer ---------------------------------- **Introduction** During Oct-Nov 2006 there was a thread in the jython-users group titled "adding JARs to sys.path". More accurately the objective there was to add JARs to the sys.path at runtime. Several people asked the question, "Why would you want to do that?" Well there are at least 2 good reasons. First, if you want to distribute a jython or Java package that includes non-standard Jars in it. Perhaps you want to make life easier for the target user and not demand that they know how to set environment variables. A second even more compelling reason is when there is no normal user account to provide environment variables. "What?", you ask. Well, in my case I came upon this problem in the following way. I am working on an open source IHE Image Archive Actor and needed a web interface. I'm using AJAX on the client side to route database calls through CGI to a jython-JDBC enabled API. Testing the jython-JDBC API from the command line worked fine, I had the PostGres driver in my CLASSPATH. But, when called via the web interface I got "zxJDBC error, postGres driver not found" errors. Why? Because APACHE was calling the API and APACHE is not a normal account with environment variables. **What to do?** The jython-users thread had many suggestions but none were found to work. For books, Chapter 11 of O'Reilly's "Jython Essentials" mentions under "System and File Modules" that "... to load a class at runtime one also needs an appropriate class loader." Of course, no mention is made beyond that. After a while, it occured to me that perhaps someone in the Java world had found a similar problem and had solved it. Then all that would be required is to translate that solution. And that is exactly what happened. **Method** For brevity we will not repeat the original Java code here. This is how I call the Jython class (note that one can use either addFile or addURL depending on whether the Jar is on a locally accesable file system or remote server). :: import sys from com.ziclix.python.sql import zxJDBC d,u,p,v = "jdbc:postgresql://localhost/img_arc2","postgres","","org.postgresql.Driver" try : # if called from command line with .login CLASSPATH setup right,this works db = zxJDBC.connect(d, u, p, v) except: # if called from Apache or account where the .login has not set CLASSPATH # need to use run-time CLASSPATH Hacker try : jarLoad = classPathHacker() a = jarLoad.addFile("/usr/share/java/postgresql-jdbc3.jar") db = zxJDBC.connect(d, u, p, v) except : sys.exit ("still failed \n%s" % (sys.exc_info() )) And here is the class "classPathHacker" which is what the original author called his solution. In fact, you can simply Google on "classPathHacker" to find the Java solution. :: class classPathHacker : ########################################################## # from http://forum.java.sun.com/thread.jspa?threadID=300557 # # Author: SG Langer Jan 2007 translated the above Java to this # Jython class # Purpose: Allow runtime additions of new Class/jars either from # local files or URL ###################################################### import java.lang.reflect.Method import java.io.File import java.net.URL import java.net.URLClassLoader import jarray def addFile (self, s): ############################################# # Purpose: If adding a file/jar call this first # with s = path_to_jar ############################################# module = "utils:classPathHacker: addFile" # make a URL out of 's' f = self.java.io.File (s) u = f.toURL () a = self.addURL (u) return a def addURL (self, u): ################################## # Purpose: Call this with u= URL for # the new Class/jar to be loaded ################################# module = "utils:classPathHacker: addURL" parameters = self.jarray.array([self.java.net.URL], self.java.lang.Class) sysloader = self.java.lang.ClassLoader.getSystemClassLoader() sysclass = self.java.net.URLClassLoader method = sysclass.getDeclaredMethod("addURL", parameters) a = method.setAccessible(1) jar_a = self.jarray.array([u], self.java.lang.Object) b = method.invoke(sysloader, jar_a) return u **Conclusions** That's it. Depressingly short for what it does, but then that's another proof of the power of this language. I hope you find this as powerful and useful as I have. It allows the possibility of distributing jython packages with all their file dependencies within the installation directory, freeing the user or developer from the need to alter user environment variables, which should lead to more programmer control and thus higher reliabliity. Author: Steve Langer URL: http://wiki.python.org/jython/JythonMonthly/Articles/January2007/3 Ant --- **The following Ant example works with Jython version 2.2.1 and earlier only due to the necessary jythonc usage. Jythonc is no longer distributed with Jython as of 2.5.0. This example could be re-written using object factories to work with current versions of Jython.** Writing Ant Tasks With Jython - Ed Takema ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ant is the current tool of choice for java builds. This is so partially because it was the first java oriented build tool on the scene and because the reigning champion *Make* was getting long in the tooth and had fallen out of favour with the java crowd. But Java builds are getting more and more difficult and these days there is general dissatisfaction with ant1. Note particularly Bruce Eckel's Comments and Martin Fowler's further comments. The comments to Bruce Eckels's posting show similar fustrations. Fowler summarizes the issues like this: ... Simple builds are easy to express as a series of tasks and dependencies. For such builds the facilities of ant/make work well. But more complex builds require conditional logic, and that requires more general programming language constructs - and that's where ant/make fall down. Ken Arnold's article The Sum of Ant led me to Jonathon Simon's article Scripting with Jython Instead of XML and got me thinking about extending ant with Jython. Simon's article presents a technique to drive Ant tasks, testing, etc all from Jython. What I am presenting is a technique to embed Jython scripts into Ant which is admittedly backwards from Simon's approach, but hopefully adds power and flexibility to ant builds. My experience working with large builds automated through ant is not dissimilar to what Fowler is referring to. Eventually, builds need to do either a lot of odd conditional logic in the xml file and ends up burying the logic in scripts, or in a large number of custom tasks written in java. This is particularly the case if your builds include non-java source that ant just isn't smart about building. In one case in particular, the set of custom tasks for the build is really its own system with maintenance and staff costs that are quite substantial. A large number of scripts can quickly become a problem for enterprise build systems as they are difficult to standardize and cross platform issues are always looming. Fortunately, all is not lost. Ant continues to evolve and version 1.6 was a significant step forward for large build systems. Mike Spille, in his article ANT's Finally a Real Build Tool, demonstrates that the new tag now allows build managers to write truly modular and standardized build systems based on Ant! As Ant grows up, more and more of these issues will get resolved. One of the strengths that Make always had was the ability to easily call scripts and command utilities. This is something that is definitely possible with Ant script/exec tasks, but it feels very un-java. What we need is an elegant way to add adhoc behaviour to Ant builds ... in a java-ish way. Writing Custom Ant Tasks ~~~~~~~~~~~~~~~~~~~~~~~~ What I think can do the job is to take a more considered approach to using a scripting tool inside an ant build. Rather than just create a mishmash of scripts that are called from exec or script tasks, I suggest that we write custom ant build tasks in a high level scripting language...in this case, Jython. Writing custom ant tasks allows a build manager to leverage the huge number of already written tasks in their builds while writing what naturally belongs in a more flexible tool in custom ant tasks that can themselves then be reused, are as cross platform as java itself, and wholly integrated into Ant. Because Ant uses java introspection to determine the capabilities of custom tasks, Jython is the perfect tool to accomplish this. All we need to do is ensure that the methods that Ant expects are present in the Jython classes and Ant won't notice the difference. What we will implement is the perennial SimpleTask which is nothing more than a 'Hello World' for ant. It should be sufficient to demonstrate the key steps. **Setup Development Environment** To compile the jython source in this article you will need to add the ant.jar file to your classpath. This will make it available to Jython to extend which we'll do below. To do that define your classpath: :: set CLASSPATH=c:\path\to\ant\lib\ant.jar :: export CLASSPATH=/path/to/ant/lib/ant.jar **SimpleTask Jython Class** The following is a very simple Ant task written in Jython(python). Save this as SimpleTask.py :: from org.apache.tools.ant import Task class SimpleTask(Task): message = "" def execute(self): """@sig public void execute()""" Task.log(self, "Message: " + self.message) def setMessage(this, aMessage): """@sig public void setMessage(java.lang.String str)""" this.message = aMessage This simple Jython class extends the ant Task superclass. For each of the properties we want to support for this task, we write a setXXXXX method where XXXXX corresponds to the property we are going to set in the ant build file. Ant creates an object from the class, calls the setXXXXX methods to setup the properties and then calls the execute method (actually, it calls the perform method on the Task superclass which calls the execute() method). So lets try it out. **Compiling Jython Code To A Jar** To build this into a jar file for use in Ant, do the following: :: jythonc -a -c -d -j myTasks.jar SimpleTask.py This will produce a jar file myTasks.jar and include the jython core support classes in the jar. Copy this jar file into your ant installation's lib directory. In my case I copy it to c:\tools\ant\lib. **Build.XML file to use the Task** Once you've got that working, here is a very simple test ant build file to test your custom jython task. :: **A Task Container Task** All right, that is a pretty simple task. What else can we do? Well, the sky is the limit really. Here is an example of a task container. In this case, the task holds references to a set of other tasks (SimpleTask tasks in this case): :: from org.apache.tools.ant import Task from org.apache.tools.ant import TaskContainer class SimpleContainer(TaskContainer): subtasks = [] def execute(this): """@sig public void execute()""" for task in this.subtasks: task.perform() def createSimpleTask(self): """@sig public java.lang.Object createSimpleTask()""" task = SimpleTask() self.subtasks.append(task) return task class SimpleTask(Task): message = "" def execute(self): """@sig public void execute()""" Task.log(self, "Message: " + self.message) def setMessage(this, aMessage): """@sig public void setMessage(java.lang.String str)""" this.message = aMessage The SimpleContainer extends the TaskContainer java class. Its createSimpleTask method creates a SimpleTask object and returns it to Ant so its properties can be set. Then when all the tasks have been added to the container and their properties set, the execute method on the SimpleContainer class is called which in turn calls the perform method on each of the contained tasks. Note that the perform method is inherited from the Task superclass and it in turn calls the the execute method which we have overriden. **Build.XML file to use the TaskContainer** Here is a ant build file to test your custom jython task container. Note that you don't need to include a task definition for the contained SimpleTask unless you want to use it directly. The createSimpleTask factory method does it for you. :: **Things To Look Out For** As I learned this technique I discovered that the magic doc strings are really necessary to force Jython to put the right methods in the generated java classes. For example: :: """@sig public void execute()""" This is primarily due to Ant's introspection that looks for those specific methods and signatures. These docstrings are required or Ant won't recognize the classes as Ant tasks. I also learned that for Jython to extend a java class, it must specifically import the java classes using this syntax: :: from org.apache.tools.ant import Task from org.apache.tools.ant import TaskContainer class MyTask(Task): ... You can not use this syntax: import org.apache.tools.ant.Task import org.apache.tools.ant.TaskContainer class MyTask(org.apache.tools.ant.Task): ... This is because, for some reason, Jython doesn't figure out that MyTask is extending this java class and so doesn't generate the right Java wrapper classes. You will know that this working right when you see output like the following when you run the jythonc compiler: :: processing SimpleTask Required packages: org.apache.tools.ant Creating adapters: Creating .java files: SimpleTask module SimpleTask extends org.apache.tools.ant.Task <<< Summary ~~~~~~~ So there you have it. Here is a quick summary then of why this is a helpful technique. First, it is a lot faster to write ant tasks that integrate with third party tools and systems using a glue language and python/jython is excellent at that. That is really my prime motivation for trying out this technique. Secondly, Jython has the advantage over other scripting languages (which could be run using Ant's exec or script tasks) because it can be tightly integrated with Ant (i.e. use the same logging methods, same settings, etc). This makes it easier to build a standardized build environment. Finally, and related to the last point, Jython can be compiled to java byte code which runs like any java class file. This means you don't have to have jython installed to use the custom tasks and your custom task, if written well, can run on a wide variety of platforms. I think this is a reasonable way to add flexibility and additional integration points to Ant builds. Author: Ed Taekema URL: http://www.fishandcross.com/articles/AntTasksWithJython.html