Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Clojure.pdf
Скачиваний:
17
Добавлен:
09.05.2015
Размер:
12.92 Mб
Скачать

ADDING ANT PROJECTS AND TASKS TO LANCET 105

You have seen how easy it is to access Java from Clojure. Now let’s put that to a real test by adding Ant support to Lancet.

3.5 Adding Ant Projects and Tasks to Lancet

Ant ships with dozens of built-in tasks, plus there are hundreds of third-party tasks. There is no need to reinvent this wheel, so Lancet will call Ant tasks directly.

Lancet requires two Ant JAR files: ant.jar and ant-launcher.jar. The sample code for the book includes these files in the lib directory, and they are added to the classpath automatically when you launch the REPL with bin/repl.sh or bin\repl.bat.

To test that Ant is on your classpath, make sure you can instantiate an Ant Mkdir task. Assign it to a var named mkdir-task:

(def mkdir-task (org.apache.tools.ant.taskdefs.Mkdir.))

#'user/mkdir-task

If the variable assigns correctly, Ant is on your classpath.

If instantiating a task is that easy, maybe you are almost done. Reflect against Mkdir’s method names to see whether you can spot the one that runs the task. You can use Java reflection to go from an instance to a class to methods to method names, like so:

(map #(.getName %) (.getMethods (class mkdir-task)))

...lots of names here...

One of the method names is execute. That sounds pretty good, so let’s try it:

(.execute mkdir-task)

| dir attribute is required

That doesn’t seem to have worked, but the error message is a good pointer. Ant attributes, aka Java properties, are usually set with methods named like setXXX( ), so try calling setDir to set the dir attribute of mkdir-task:

(.setDir mkdir-task "sample-dir")

java.lang.ClassCastException

It appears that the dir attribute is not a string.

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

ADDING ANT PROJECTS AND TASKS TO LANCET 106

To see what type dir expects, reflect against the methods again, this time filtering down to just methods whose name is setDir:

(filter #(= "setDir" (.getName %)) (.getMethods (class mkdir-task)))

(#<Method public void ... Mkdir.setDir(java.io.File)>)

As you can see from setDir’s signature, it expects a File, so give it one:

(.setDir mkdir-task (java.io.File. "sample-dir"))

nil

Now, you should be able to execute the mkdir-task and create a directory:

(.execute mkdir-task)

| Created dir: /lancet/examples/sample-dir

Invoking tasks successfully is a good start. However, Lancet needs a few more things from Ant. By reading the Ant source code and experimenting with various tasks, I discovered that Lancet would also need to use Ant’s Project class. Lancet does not need Ant’s project metaphor, but it does need various APIs that Ant hangs on a Project instance, including the logger.

Create a Project object at the REPL, and bind it to project:

(def project (org.apache.tools.ant.Project.))

#'user/project

The project object will want a logger. Ant’s NoBannerLogger is a good choice, because it omits unnecessary information about empty targets. Create a NoBannerLogger bound to logger:

(def logger (org.apache.tools.ant.NoBannerLogger.))

#'user/logger

There are several steps to configure these objects and connect them. The logger needs a log level, output stream, and error stream. The project needs to initialize and add the logger as a build listener. You could call all of these methods one at a time, like this:

(.setOutputPrintStream logger System/out)

nil

(.setErrorPrintStream logger System/err)

nil

;etc.

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

ADDING ANT PROJECTS AND TASKS TO LANCET 107

A more readable approach is to use doto to group the configuration calls for each object. Use the following code to create an Ant project:

Download lancet/step_1_repl.clj

Line 1 (def

-#^{:doc "Dummy ant project to keep Ant tasks happy" }

-ant-project

-(let [proj (org.apache.tools.ant.Project.)

5 logger (org.apache.tools.ant.NoBannerLogger.)]

-(doto logger

-(.setMessageOutputLevel org.apache.tools.ant.Project/MSG_INFO)

-(.setOutputPrintStream System/out)

-(.setErrorPrintStream System/err)) 10 (doto proj

-(.init)

-(.addBuildListener logger))))

Line 2 creates a documentation string. Since ant-project is a var, you create the documentation string directly in metadata. (Most documentation strings are attached to functions, where defn provides a layer of abstraction above the metadata.)

Line 3 names the var. ant-project will be shared through an entire Lancet process, unless some need arises for a specific project instance.

Line 4 binds new Project and NoBannerLogger instances to proj and logger.

Starting on line 6, a doto form configures the logger.

Starting on line 10, a doto form initializes the proj and sets its logger.

The proj on line 10 becomes the return value of the doto and the let and binds to ant-project.

If the definition of ant-project executes correctly, you will then have an instance of Project that you can access at the REPL:

ant-project

#<org.apache.tools.ant.Project@637550b3>

As you can see, the REPL representation of a Project does not tell you much. When you are interactively testing a Java object such as an Ant Project from Clojure, the bean function provides a quick way to peek inside the object:

(bean ant-project)

... 100s of lines follow! ...

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

ADDING ANT PROJECTS AND TASKS TO LANCET 108

Unfortunately, the insides of an Ant project are pretty big. You can use keys to get the ant-project’s property names:

(keys (bean ant-project))

(:userProperties :references :class :filters :defaultInputStream\ :name :description :properties :taskDefinitions :buildListeners\ :dataTypeDefinitions :inputHandler :coreLoader :baseDir\ :globalFilterSet :keepGoingMode :defaultTarget :targets :executor)

Once you know the keys, you can use them to poke around further inside the bean. Your project should have a logger as a build listener, so use the :buildListeners key to verify that the listener was installed correctly:

(:buildListeners (bean ant-project))

#=(java.util.Vector.[#<org. ... .NoBannerLogger@3583a303>])

There is the logger, right where you put it. Clojure’s easy, reflective access to Java makes it a good language for exploring existing Java code.

Now that you have an Ant project handy at the REPL, let’s revisit how Lancet should create Ant tasks. Earlier you created a mkdir-task directly:

(def mkdir-task (org.apache.tools.ant.taskdefs.Mkdir.))

#'user/mkdir-task

Project provides a better way to create tasks, with the createTask method. One advantage of this approach is that you do not need to know the class name of a task; you need to know only its short name as it would appear in an Ant build script. Use your ant-project to create an echotask:

(def echo-task (.createTask ant-project "echo"))

#'user/echo-task

The Ant echo task takes a message attribute. Call setMessage to set a message, and execute the task to verify its behavior:

(.setMessage echo-task "hello ant")

nil

(.execute echo-task) [echo] hello ant

Well-behaved tasks need a few more things. All tasks have an init method that Ant calls internally. You have been lucky so far, because mkdir and echo do not use init. But Lancet will need to call init on all tasks. Also, tasks maintain a reference to their projects.

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)

ADDING ANT PROJECTS AND TASKS TO LANCET 109

Create an instantiate-task function that correctly performs all these steps:

Download lancet/step_1_repl.clj

(defn instantiate-task [project name] (let [task (.createTask project name)]

(doto task (.init)

(.setProject project))))

Create and execute a task to make sure that instantiate-task works as you expect:

(def echo-task (instantiate-task ant-project "echo"))

#'user/echo-task

(.setMessage echo-task "echo from instantiate-task")

nil

(.execute echo-task)

[echo] echo from instantiate-task

There is one slight problem with instantiate-task. If you try to create a task using a name that doesn’t exist, ant-project does not throw an exception. It just returns nil, which leads to a confusing error message:

(instantiate-task ant-project "sisyphus")

java.lang.NullPointerException

Create a safe-instantiate-task that adds a nil check, throwing an IllegalArgumentException if a task name does not exist:

Download lancet/step_1_repl.clj

(use '[clojure.contrib.except :only (throw-if)]) (defn safe-instantiate-task [project name]

(let [task (.createTask project name)] (throw-if (nil? task)

IllegalArgumentException (str "No task named " name))

(doto task (.init)

(.setProject project))))

Now the error message is much better:

(safe-instantiate-task ant-project "sisyphus")

java.lang.IllegalArgumentException: No task named sisyphus

The completed code for this section is listed in Section 3.5, Lancet Step 1: Ant Projects and Tasks, on the next page. In this section, you have given Lancet the ability to call Ant tasks. You have plumbed in an Ant project object, and you have wrapped correct initialization of tasks and projects in helper functions.

Prepared exclusively for WG Custom Motorcycles

Report erratum

this copy is (P1.0 printing, May 2009)