| 1 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> |
|---|
| 2 | <!-- |
|---|
| 3 | Licensed to the Apache Software Foundation (ASF) under one |
|---|
| 4 | or more contributor license agreements. See the NOTICE file |
|---|
| 5 | distributed with this work for additional information |
|---|
| 6 | regarding copyright ownership. The ASF licenses this file |
|---|
| 7 | to you under the Apache License, Version 2.0 (the |
|---|
| 8 | "License"); you may not use this file except in compliance |
|---|
| 9 | with the License. You may obtain a copy of the License at |
|---|
| 10 | |
|---|
| 11 | http://www.apache.org/licenses/LICENSE-2.0 |
|---|
| 12 | |
|---|
| 13 | Unless required by applicable law or agreed to in writing, |
|---|
| 14 | software distributed under the License is distributed on an |
|---|
| 15 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
|---|
| 16 | KIND, either express or implied. See the License for the |
|---|
| 17 | specific language governing permissions and limitations |
|---|
| 18 | under the License. |
|---|
| 19 | --> |
|---|
| 20 | <html> |
|---|
| 21 | <head> |
|---|
| 22 | <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=ISO-8859-1"> |
|---|
| 23 | <script type="text/javascript">var xookiConfig = {level: 1};</script> |
|---|
| 24 | <script type="text/javascript" src="../xooki/xooki.js"></script> |
|---|
| 25 | </head> |
|---|
| 26 | <body> |
|---|
| 27 | <textarea id="xooki-source"> |
|---|
| 28 | <h1>How To write a module for easyant</h1> |
|---|
| 29 | |
|---|
| 30 | <p>A module in Easyant is a logical unit that provides additional pluggable functionality to your build set up. You may choose to use or ignore such a plugin when running the build. A module is composed, in the least, of a ant script associated with a ivy specs file. |
|---|
| 31 | So let's write a Hello World plugin.</p> |
|---|
| 32 | |
|---|
| 33 | <h2>Generating plugin skeleton</h2> |
|---|
| 34 | First we need to create a plugin structure. To ease plugin development easyant came with a skeleton for plugins. |
|---|
| 35 | <code type="shell">> easyant skeleton:create -Dskeleton.org=org.apache.easyant.skeletons -Dskeleton.module=std-ant-plugin -Dskeleton.rev=0.1</code> |
|---|
| 36 | It will then ask you a few questions |
|---|
| 37 | <code type="shell"> |
|---|
| 38 | [input] Organisation name of YOUR project |
|---|
| 39 | org.mycompany |
|---|
| 40 | [input] Module name of YOUR project |
|---|
| 41 | myplugin |
|---|
| 42 | [input] Revision number of YOUR project [0.1] |
|---|
| 43 | 0.1 |
|---|
| 44 | </code> |
|---|
| 45 | That's all ! |
|---|
| 46 | We've a ready to use plugin structure. |
|---|
| 47 | <code type="shell"> |
|---|
| 48 | <!--replace me by an image --> |
|---|
| 49 | |-- module.ivy |
|---|
| 50 | `-- src |
|---|
| 51 | |-- main |
|---|
| 52 | | `-- resources |
|---|
| 53 | | `-- myplugin.ant |
|---|
| 54 | `-- test |
|---|
| 55 | `-- antunit |
|---|
| 56 | |-- common |
|---|
| 57 | | `-- test-utils.ant |
|---|
| 58 | `-- myplugin-test.xml |
|---|
| 59 | |
|---|
| 60 | </code> |
|---|
| 61 | |
|---|
| 62 | <h2>Ant script</h2> |
|---|
| 63 | The skeleton has generated the plugin main script in src/main/resources/[MYPLUGIN].ant |
|---|
| 64 | <code type="xml"> |
|---|
| 65 | <project name="org.mycompany;myplugin" |
|---|
| 66 | xmlns:ivy="antlib:org.apache.ivy.ant" |
|---|
| 67 | xmlns:ea="antlib:org.apache.easyant"> |
|---|
| 68 | |
|---|
| 69 | <!-- Force compliance with easyant-core to 0.7 or higher --> |
|---|
| 70 | <!-- <ea:core-version requiredrevision="[0.7,+]" /> --> |
|---|
| 71 | |
|---|
| 72 | <!-- Sample init target --> |
|---|
| 73 | <target name=":init" phase="validate"> |
|---|
| 74 | <!-- you should remove this echo message --> |
|---|
| 75 | <echo level="debug">This is the init target of myplugin</echo> |
|---|
| 76 | </target> |
|---|
| 77 | |
|---|
| 78 | <!-- define a generic default target for this plugin --> |
|---|
| 79 | <target name="doit" depends="validate"/> |
|---|
| 80 | </project> |
|---|
| 81 | </code> |
|---|
| 82 | By convention, projectname of the plugin should be formed like |
|---|
| 83 | <code type="xml"> |
|---|
| 84 | [organisation]#[module] |
|---|
| 85 | </code> |
|---|
| 86 | Example: |
|---|
| 87 | <code type="xml"> |
|---|
| 88 | org.mycompany#myplugin |
|---|
| 89 | </code> |
|---|
| 90 | |
|---|
| 91 | <h3>Understanding Phases</h3> |
|---|
| 92 | Phases are high-level build activities, like "package" or "documentation". Plugins typically add low-level tasks to one or more phases. For example, a plugin might add a "build jar" task to the "package" phase, or a "generate javadoc" task to the "documentation" phase. Less typically, a plugin can also define new phases for other plugins to use. |
|---|
| 93 | In standard build types the project-lifecycle is defined by a plugin named <a href="../ref/plugins/phases-std.html">phases-std</a>. |
|---|
| 94 | |
|---|
| 95 | <h3>Pre conditions</h3> |
|---|
| 96 | A build module should always check that a set of pre conditions is met in the validate phase (for static pre conditions) or at execution (for dynamic pre conditions). |
|---|
| 97 | By convention, this target should be named ":init" and associated to the "validate" phase. |
|---|
| 98 | |
|---|
| 99 | Pre conditions, including for example - checking the existence of a file or a directory, could be performed inside this target. Additionally, this target is a great place to do global initializations that are needed for the rest of the build. This could include a taskdef initialization. |
|---|
| 100 | Pre conditions can be performed by using <a href="../ref/anttasks/Parametertask.html">parameter task</a>. |
|---|
| 101 | Example : |
|---|
| 102 | <code type="xml"> |
|---|
| 103 | <target name=":init" phase="validate"> |
|---|
| 104 | <!-- Our plugin need at least the existance of "validate" phase" --> |
|---|
| 105 | <ea:parameter phase="validate" /> |
|---|
| 106 | <ea:parameter property="username" required="false" description="the username used to display en 'hello Username' by calling :hello target"/> |
|---|
| 107 | </target> |
|---|
| 108 | </code> |
|---|
| 109 | |
|---|
| 110 | <h3>Target Naming Conventions</h3> |
|---|
| 111 | There is a conventional difference in the way public and private targets are named in Easyant. A <i>public target</i> is one that makes sense for the end user to be aware of, while a <i>private target</i> should be hidden from the end user. |
|---|
| 112 | |
|---|
| 113 | Conventionally, a public target should always have an associated 'description' attribute. Further, it's name should always begin with a ':'. |
|---|
| 114 | |
|---|
| 115 | Example : |
|---|
| 116 | <code type="xml"> |
|---|
| 117 | <target name=":helloworld" depends="validate" description="display an hello world"> |
|---|
| 118 | <echo>hello world !</echo> |
|---|
| 119 | </target> |
|---|
| 120 | |
|---|
| 121 | <target name=":hello" depends="validate" depends="-check-username" description="display an hello to current user"> |
|---|
| 122 | <echo mess="Hello ${username}"/> |
|---|
| 123 | </target> |
|---|
| 124 | </code> |
|---|
| 125 | |
|---|
| 126 | Whereas a private target name should begin with '-'. |
|---|
| 127 | |
|---|
| 128 | Example : |
|---|
| 129 | <code type="xml"> |
|---|
| 130 | <!-- this target initialize username property if it's not already set --> |
|---|
| 131 | <target name="-check-username" unless="username"> |
|---|
| 132 | <echo>You can also add a "-Dusername=YOU" on the commandline to display a more personal hello message</echo> |
|---|
| 133 | <property name="username" value="${user.name}"/> |
|---|
| 134 | </target> |
|---|
| 135 | </code> |
|---|
| 136 | |
|---|
| 137 | <h3>The 'doit' Target</h3> |
|---|
| 138 | Each module should have a target called <i>doit</i>. This is an important convention. This target should perform the essential purpose of the module when invoked independently. |
|---|
| 139 | Example: |
|---|
| 140 | <code type="xml"> |
|---|
| 141 | <target name="doit" depends=":helloworld"/> |
|---|
| 142 | </code> |
|---|
| 143 | |
|---|
| 144 | <h3>What a build module should document</h3> |
|---|
| 145 | <ul> |
|---|
| 146 | <li>phases on which it relies</li> |
|---|
| 147 | <li>parameters (properties, resource collections, paths). For each parameter specify name, description, whether it is required, and optionally a default value.</li> |
|---|
| 148 | <li>expected environment (files in a directory, a server up and running, ...)</li> |
|---|
| 149 | <li>results produced</li> |
|---|
| 150 | </ul> |
|---|
| 151 | |
|---|
| 152 | A build module should always check that the set of pre conditions is met in the validate phase (for static pre conditions) or at execution (for dynamic pre conditions). |
|---|
| 153 | |
|---|
| 154 | If ever what is considered static pre condition by a module is actually generated by another one, it is still possible to assign the build module validate phase to a phase triggered after the execution of the other build module (using phase mapping with the 'use' task). |
|---|
| 155 | |
|---|
| 156 | <h2>Publishing your plugin</h2> |
|---|
| 157 | You can easily publish your plugin to an easyant repository using the standard phases <i>publish-shared (for snapshot)</i> or <i>release</i> |
|---|
| 158 | <code type="shell">> easyant publish-shared</code> |
|---|
| 159 | <code type="shell">> easyant release</code> |
|---|
| 160 | |
|---|
| 161 | By default plugins are published to a repository named <i>easyant-shared-modules</i> stored in $USER_HOME/.easyant/repository/easyant-shared-modules/. |
|---|
| 162 | |
|---|
| 163 | You can specify the repository name using one of the following property |
|---|
| 164 | <ul> |
|---|
| 165 | <li>release.resolver</li> |
|---|
| 166 | <li>snapshot.resolver</li> |
|---|
| 167 | </ul> |
|---|
| 168 | <div id="note">Note: Repository must exist in easyant ivy instance. See <a href="../ref/ConfigureEasyantIvySettings.html">configure easyant ivy instance</a> man page for more informations.</div> |
|---|
| 169 | |
|---|
| 170 | <h2>Using your plugin in your project</h2> |
|---|
| 171 | Considering that you published your plugin in a easyant repository, you could use it in your project. |
|---|
| 172 | <code type="xml"> |
|---|
| 173 | <ivy-module version="2.0" xmlns:ea="http://www.easyant.org"> |
|---|
| 174 | <info organisation="org.mycompany" module="myproject" |
|---|
| 175 | status="integration" revision="0.1"> |
|---|
| 176 | <ea:build module="build-std-java" revision="0.2"> |
|---|
| 177 | <ea:plugin organisation="org.mycompany" module="myplugin" revision="0.1" as="myplugin"/> |
|---|
| 178 | </ea:build> |
|---|
| 179 | </info> |
|---|
| 180 | <publications> |
|---|
| 181 | <artifact name="myplugin" type="ant"/> |
|---|
| 182 | </publications> |
|---|
| 183 | </ivy-module> |
|---|
| 184 | </code> |
|---|
| 185 | And now running |
|---|
| 186 | <code type="shell">> easyant -p </code> |
|---|
| 187 | We should see myplugin's target. |
|---|
| 188 | <code type="shell"> |
|---|
| 189 | Main targets: |
|---|
| 190 | ... |
|---|
| 191 | mygplugin:hello display an hello to current user |
|---|
| 192 | mygplugin:helloworld display an hello world |
|---|
| 193 | ... |
|---|
| 194 | </code> |
|---|
| 195 | |
|---|
| 196 | <h2>Getting further</h2> |
|---|
| 197 | |
|---|
| 198 | <h3>Adding additional files to your module</h3> |
|---|
| 199 | Sometimes, we need to have a .properties files related to a given plugin. |
|---|
| 200 | Sometimes it could be an additional file (an .xsl file for example). |
|---|
| 201 | |
|---|
| 202 | Before using it we must declare the new file in the plugin module descriptor. |
|---|
| 203 | Open the module.ivy at the root level of plugin structure. |
|---|
| 204 | <code type="xml"> |
|---|
| 205 | <ivy-module version="2.0" xmlns:ea="http://www.easyant.org"> |
|---|
| 206 | <info organisation="org.mycompany" module="myplugin" |
|---|
| 207 | status="integration" revision="0.1"> |
|---|
| 208 | <!-- here we use build-std-ant-plugin build type that provide everything we need for plugin development --> |
|---|
| 209 | <ea:build module="build-std-ant-plugin" revision="0.1"/> |
|---|
| 210 | </info> |
|---|
| 211 | <configurations> |
|---|
| 212 | <conf name="default" visibility="public" description="runtime dependencies artifact can be used with this conf"/> |
|---|
| 213 | <conf name="test" visibility="private" description="this scope indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases."/> |
|---|
| 214 | <conf name="provided" visibility="public" description="this is much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive."/> |
|---|
| 215 | </configurations> |
|---|
| 216 | <publications> |
|---|
| 217 | <!--Defines the plugin main script --> |
|---|
| 218 | <artifact name="myplugin" type="ant"/> |
|---|
| 219 | <!--Defines a property file --> |
|---|
| 220 | <artifact name="myplugin" type="properties"/> |
|---|
| 221 | <artifact name="myfile" type="xsl"/> |
|---|
| 222 | </publications> |
|---|
| 223 | </ivy-module> |
|---|
| 224 | </code> |
|---|
| 225 | Here we defined that our plugin is composed of 3 files : |
|---|
| 226 | <ul> |
|---|
| 227 | <li>myplugin.ant (if name argument is not specified the module name will be used)</li> |
|---|
| 228 | <li>myplugin.properties</li> |
|---|
| 229 | <li>myfile.xsl</li> |
|---|
| 230 | </ul> |
|---|
| 231 | |
|---|
| 232 | Now we will see how we can use those files in our plugin script. |
|---|
| 233 | Considering that a plugin must be generic and can be retrieved from different repository (filesystem, url, ftp, etc...) we should take care of how we reference those additional files in our script. |
|---|
| 234 | To avoid any problems due to repository layout configuration, easyant gives you gives you access to properties containing the absolute path of a declared artifact. Those properties are composed with the following syntax : |
|---|
| 235 | <code type="xml"> |
|---|
| 236 | [organisation].[module].[artifact].[type].file |
|---|
| 237 | </code> |
|---|
| 238 | Example: |
|---|
| 239 | <code type="xml"> |
|---|
| 240 | org.mycompany.myplugin.myfile.xsl.file |
|---|
| 241 | </code> |
|---|
| 242 | |
|---|
| 243 | The '.artifact' is optional when module name and artifact name are the same. |
|---|
| 244 | <code type="xml"> |
|---|
| 245 | [organisation].[module].[type].file |
|---|
| 246 | </code> |
|---|
| 247 | Example: |
|---|
| 248 | <code type="xml"> |
|---|
| 249 | org.mycompany#myplugin.properties.file |
|---|
| 250 | </code> |
|---|
| 251 | |
|---|
| 252 | So loading a property file could be easy as : |
|---|
| 253 | <code type="xml"> |
|---|
| 254 | <property file="${org.mycompany#myplugin.properties.file}" /> |
|---|
| 255 | </code> |
|---|
| 256 | |
|---|
| 257 | If you want to copy / use an additional file |
|---|
| 258 | <code type="xml"> |
|---|
| 259 | <copy file="${org.mycompany.myplugin.myfile.xsl.file}" tofile="..."/> |
|---|
| 260 | </code> |
|---|
| 261 | |
|---|
| 262 | <h3>Using third party libraries</h3> |
|---|
| 263 | Most of the time when we write plugins we want to use third party ant tasks. |
|---|
| 264 | |
|---|
| 265 | <h4>Declaring dependencies in module.ivy</h4> |
|---|
| 266 | First we need to declare the dependency in the plugin module.ivy. |
|---|
| 267 | <code type="xml"> |
|---|
| 268 | <ivy-module version="2.0" xmlns:ea="http://www.easyant.org"> |
|---|
| 269 | <info organisation="org.mycompany" module="myplugin" |
|---|
| 270 | status="integration" revision="0.1"> |
|---|
| 271 | <ea:build module="build-std-ant-plugin" revision="0.1"/> |
|---|
| 272 | </info> |
|---|
| 273 | <configurations> |
|---|
| 274 | <conf name="default" visibility="public" description="runtime dependencies artifact can be used with this conf"/> |
|---|
| 275 | <conf name="test" visibility="private" description="this scope indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases."/> |
|---|
| 276 | <conf name="provided" visibility="public" description="this is much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive."/> |
|---|
| 277 | </configurations> |
|---|
| 278 | <publications> |
|---|
| 279 | <artifact name="myplugin" type="ant"/> |
|---|
| 280 | </publications> |
|---|
| 281 | |
|---|
| 282 | <dependencies> |
|---|
| 283 | <!-- your plugin dependencies goes here --> |
|---|
| 284 | <dependency org="foobar" name="amazingAntTask" rev="4.4" conf="default->default" /> |
|---|
| 285 | <dependency org="foobar" name="myOtherAntTask" rev="4.4" conf="default->default" /> |
|---|
| 286 | </dependencies> |
|---|
| 287 | </ivy-module> |
|---|
| 288 | </code> |
|---|
| 289 | Here we depend on amazingAntTask and myOtherAntTask provided by foobar organisation. |
|---|
| 290 | <h4>Using dependency in your plugin ant script?</h4> |
|---|
| 291 | Easyant automatically creates a classpath specific for each plugin, this classpath contains all the required dependency <i>.jars</i>. |
|---|
| 292 | |
|---|
| 293 | The classpath is named |
|---|
| 294 | <code tyep="xml"> |
|---|
| 295 | [organisation]#[module].classpath |
|---|
| 296 | </code> |
|---|
| 297 | Example: |
|---|
| 298 | <code type="xml"> |
|---|
| 299 | org.mycompany#myplugin.classpath |
|---|
| 300 | </code> |
|---|
| 301 | Since this classpath is auto-created you can use it to reference your taskdef. |
|---|
| 302 | <code type="xml"> |
|---|
| 303 | <target name=":init" phase="validate"> |
|---|
| 304 | <ea:parameter phase="validate"/> |
|---|
| 305 | ... |
|---|
| 306 | <taskdef resource="amazingAntTask.properties" classpathref="org.mycompany#myplugin.classpath" /> |
|---|
| 307 | <taskdef resource="anotherAntTask.properties" classpathref="org.mycompany#myplugin.classpath" /> |
|---|
| 308 | </target> |
|---|
| 309 | </code> |
|---|
| 310 | |
|---|
| 311 | <h3>Compatibilty with core revision</h3> |
|---|
| 312 | A module can be dependent on features available in Easyant core. As such, it is possible for a module to be functional with particular versions of Easyant only. |
|---|
| 313 | Easyant provides a way for modules to explicitly specify their dependency on core revisions. |
|---|
| 314 | A module may use the ea:core-version task to specify such a dependency. |
|---|
| 315 | A task may depend on: |
|---|
| 316 | <ul> |
|---|
| 317 | <li>static version (Example : 0.5)</li> |
|---|
| 318 | <li>dynamic version (Example : latest.revision) even if we do not recommand to use it</li> |
|---|
| 319 | <li>listed version (Example : (0.1,0.3,0.5) )</li> |
|---|
| 320 | <li>range version (Example : [0.5,0.8] means from 0.5 to 0.8. Example2 : [0.5,+] means all version superior to 0.5)</li> |
|---|
| 321 | </ul> |
|---|
| 322 | <code type="xml"> |
|---|
| 323 | <project name="org.mycompany;myplugin" |
|---|
| 324 | xmlns:ivy="antlib:org.apache.ivy.ant" |
|---|
| 325 | xmlns:ea="antlib:org.apache.easyant"> |
|---|
| 326 | |
|---|
| 327 | <!-- Force compliance with easyant-core to 0.7 or higher --> |
|---|
| 328 | <ea:core-version requiredrevision="[0.7,+]" /> |
|---|
| 329 | |
|---|
| 330 | <!-- Sample init target --> |
|---|
| 331 | <target name=":init" phase="validate"> |
|---|
| 332 | <!-- you should remove this echo message --> |
|---|
| 333 | <echo level="debug">This is the init target of myplugin</echo> |
|---|
| 334 | </target> |
|---|
| 335 | |
|---|
| 336 | ... |
|---|
| 337 | |
|---|
| 338 | <!-- define a generic default target for this plugin --> |
|---|
| 339 | <target name="doit" depends="validate"/> |
|---|
| 340 | </project> |
|---|
| 341 | </code> |
|---|
| 342 | |
|---|
| 343 | <h3>Writing plugin test case</h3> |
|---|
| 344 | By default the skeleton has generated a antunit test file in src/test/antunit/[module]-test.ant. |
|---|
| 345 | |
|---|
| 346 | So in our case let's open "src/test/antunit/myplugin-test.xml" |
|---|
| 347 | <code type="xml"> |
|---|
| 348 | <project name="org.mycompany;myplugin-test" xmlns:au="antlib:org.apache.ant.antunit"> |
|---|
| 349 | |
|---|
| 350 | <!-- Mocking required phase --> |
|---|
| 351 | <phase name="validate"/> |
|---|
| 352 | |
|---|
| 353 | <!-- Import your plugin --> |
|---|
| 354 | <import file="../../main/resources/myplugin.ant"/> |
|---|
| 355 | |
|---|
| 356 | <!-- Defines a setUp / tearDown (before each test) that cleans the environment --> |
|---|
| 357 | <target name="clean" description="remove stale build artifacts before / after each test"> |
|---|
| 358 | <delete dir="${basedir}" includeemptydirs="true"> |
|---|
| 359 | <include name="**/target/**"/> |
|---|
| 360 | <include name="**/lib/**"/> |
|---|
| 361 | </delete> |
|---|
| 362 | </target> |
|---|
| 363 | |
|---|
| 364 | <target name="setUp" depends="clean"/> |
|---|
| 365 | <target name="tearDown" depends="clean"/> |
|---|
| 366 | |
|---|
| 367 | <!-- init test case --> |
|---|
| 368 | <target name="testInit"> |
|---|
| 369 | <antcall target=":init"/> |
|---|
| 370 | <au:assertLogContains level="debug" text="This is the init target of myplugin"/> |
|---|
| 371 | </target> |
|---|
| 372 | |
|---|
| 373 | </project> |
|---|
| 374 | </code> |
|---|
| 375 | Considering that our plugin relies on an externally defined phase (validate in our example) we must mock it in our test. |
|---|
| 376 | Then we : |
|---|
| 377 | <ul> |
|---|
| 378 | <li>import the plugin</li> |
|---|
| 379 | <li>define a generic tearDown, setUp method that cleans the target and lib directories.</li> |
|---|
| 380 | <li>define a test case for the init target that check that the output log contains "This is the init target of myplugin"</li> |
|---|
| 381 | </ul> |
|---|
| 382 | |
|---|
| 383 | All targets prefixed by "test" will be executed as a test case (similar to junit 3 behavior). |
|---|
| 384 | |
|---|
| 385 | Now we will write a test case for our ":helloworld" target. |
|---|
| 386 | <code type="xml"> |
|---|
| 387 | <target name="testHelloWorld"> |
|---|
| 388 | <antcall target=":helloworld"/> |
|---|
| 389 | <au:assertLogContains text="hello world !"/> |
|---|
| 390 | </target> |
|---|
| 391 | </code> |
|---|
| 392 | |
|---|
| 393 | Tests can be executed by running : |
|---|
| 394 | <code type="shell">> easyant test</code> |
|---|
| 395 | |
|---|
| 396 | You can access test-report at "target/antunit/html/index.html" or if you prefer the brut result "target/antunit/xml/TEST-src.test.antunit.myplugin-test_xml.xml".</textarea> |
|---|
| 397 | <script type="text/javascript">xooki.postProcess();</script> |
|---|
| 398 | </body> |
|---|
| 399 | </html> |
|---|