Revert "temporarily empty repository"

This reverts commit 27cbb9cb0940f8148ed41db6c875d78efd4e11b1.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7d7d014
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.settings
+.classpath
+.project
+target
+.DS_Store
+.springBeans
+/datanucleus.log
diff --git a/.opf.yml b/.opf.yml
new file mode 100644
index 0000000..fe9558c
--- /dev/null
+++ b/.opf.yml
@@ -0,0 +1,12 @@
+name: Taverna Server
+vendor: The University of Manchester
+maintainer:
+	name: Donal Fellows
+	email: donal.k.fellows( AT )manchester.ac.uk
+platform: "Java 6+, Tomcat 6, POSIX"
+download: http://www.taverna.org.uk/download/server/
+# Really the documentation page as the overall home page is shared with the workbench but will do for now
+homepage: http://dev.mygrid.org.uk/wiki/display/taverna/Taverna+Server+2.4
+issues: http://dev.mygrid.org.uk/issues/browse/TAVSERV
+# Note, no anti-spam armouring required for this one
+support-contact: support@mygrid.org.uk
diff --git a/CITATION b/CITATION
new file mode 100644
index 0000000..fd17bfc
--- /dev/null
+++ b/CITATION
@@ -0,0 +1,18 @@
+Wolstencroft, K., Haines, R., Fellows, D., Williams, A., Withers, D.,
+  Owen, S., Soiland-Reyes, S., Dunlop, I., Nenadic, A., Fisher, P., Bhagat, J.,
+  Belhajjame, K., Bacall, F., Hardisty, A., Nieva de la Hidalga, A.,
+  Balcazar Vargas, M.P., Sufi, S., and Goble, C.  2013.
+The Taverna workflow suite: designing and executing workflows of Web Services
+on the desktop, web or in the cloud.  Nucl. Acids Res. gkt328v1
+http://doi.org/doi:10.1093/nar/gkt328
+
+@article{Wolstencroft02052013,
+  author = {Wolstencroft, Katherine and Haines, Robert and Fellows, Donal and Williams, Alan and Withers, David and Owen, Stuart and Soiland-Reyes, Stian and Dunlop, Ian and Nenadic, Aleksandra and Fisher, Paul and Bhagat, Jiten and Belhajjame, Khalid and Bacall, Finn and Hardisty, Alex and Nieva de la Hidalga, Abraham and Balcazar Vargas, Maria P. and Sufi, Shoaib and Goble, Carole}, 
+  title = {The {Taverna} workflow suite: designing and executing workflows of {Web Services} on the desktop, web or in the cloud},
+  year = {2013}, 
+  doi = {10.1093/nar/gkt328}, 
+  abstract ={The Taverna workflow tool suite (http://www.taverna.org.uk) is designed to combine distributed Web Services and/or local tools into complex analysis pipelines. These pipelines can be executed on local desktop machines or through larger infrastructure (such as supercomputers, Grids or cloud environments), using the Taverna Server. In bioinformatics, Taverna workflows are typically used in the areas of high-throughput omics analyses (for example, proteomics or transcriptomics), or for evidence gathering methods involving text mining or data mining. Through Taverna, scientists have access to several thousand different tools and resources that are freely available from a large range of life science institutions. Once constructed, the workflows are reusable, executable bioinformatics protocols that can be shared, reused and repurposed. A repository of public workflows is available at http://www.myexperiment.org. This article provides an update to the Taverna tool suite, highlighting new features and developments in the workbench and the Taverna Server.}, 
+  URL = {http://nar.oxfordjournals.org/content/early/2013/05/02/nar.gkt328.abstract}, 
+  eprint = {http://nar.oxfordjournals.org/content/early/2013/05/02/nar.gkt328.full.pdf+html}, 
+  journal = {Nucleic Acids Research} 
+}
diff --git a/LICENCE b/LICENCE
new file mode 100644
index 0000000..53afb04
--- /dev/null
+++ b/LICENCE
@@ -0,0 +1,504 @@
+		  GNU LESSER GENERAL PUBLIC LICENSE

+		       Version 2.1, February 1999

+

+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.

+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

+ Everyone is permitted to copy and distribute verbatim copies

+ of this license document, but changing it is not allowed.

+

+[This is the first released version of the Lesser GPL.  It also counts

+ as the successor of the GNU Library Public License, version 2, hence

+ the version number 2.1.]

+

+			    Preamble

+

+  The licenses for most software are designed to take away your

+freedom to share and change it.  By contrast, the GNU General Public

+Licenses are intended to guarantee your freedom to share and change

+free software--to make sure the software is free for all its users.

+

+  This license, the Lesser General Public License, applies to some

+specially designated software packages--typically libraries--of the

+Free Software Foundation and other authors who decide to use it.  You

+can use it too, but we suggest you first think carefully about whether

+this license or the ordinary General Public License is the better

+strategy to use in any particular case, based on the explanations below.

+

+  When we speak of free software, we are referring to freedom of use,

+not price.  Our General Public Licenses are designed to make sure that

+you have the freedom to distribute copies of free software (and charge

+for this service if you wish); that you receive source code or can get

+it if you want it; that you can change the software and use pieces of

+it in new free programs; and that you are informed that you can do

+these things.

+

+  To protect your rights, we need to make restrictions that forbid

+distributors to deny you these rights or to ask you to surrender these

+rights.  These restrictions translate to certain responsibilities for

+you if you distribute copies of the library or if you modify it.

+

+  For example, if you distribute copies of the library, whether gratis

+or for a fee, you must give the recipients all the rights that we gave

+you.  You must make sure that they, too, receive or can get the source

+code.  If you link other code with the library, you must provide

+complete object files to the recipients, so that they can relink them

+with the library after making changes to the library and recompiling

+it.  And you must show them these terms so they know their rights.

+

+  We protect your rights with a two-step method: (1) we copyright the

+library, and (2) we offer you this license, which gives you legal

+permission to copy, distribute and/or modify the library.

+

+  To protect each distributor, we want to make it very clear that

+there is no warranty for the free library.  Also, if the library is

+modified by someone else and passed on, the recipients should know

+that what they have is not the original version, so that the original

+author's reputation will not be affected by problems that might be

+introduced by others.

+

+  Finally, software patents pose a constant threat to the existence of

+any free program.  We wish to make sure that a company cannot

+effectively restrict the users of a free program by obtaining a

+restrictive license from a patent holder.  Therefore, we insist that

+any patent license obtained for a version of the library must be

+consistent with the full freedom of use specified in this license.

+

+  Most GNU software, including some libraries, is covered by the

+ordinary GNU General Public License.  This license, the GNU Lesser

+General Public License, applies to certain designated libraries, and

+is quite different from the ordinary General Public License.  We use

+this license for certain libraries in order to permit linking those

+libraries into non-free programs.

+

+  When a program is linked with a library, whether statically or using

+a shared library, the combination of the two is legally speaking a

+combined work, a derivative of the original library.  The ordinary

+General Public License therefore permits such linking only if the

+entire combination fits its criteria of freedom.  The Lesser General

+Public License permits more lax criteria for linking other code with

+the library.

+

+  We call this license the "Lesser" General Public License because it

+does Less to protect the user's freedom than the ordinary General

+Public License.  It also provides other free software developers Less

+of an advantage over competing non-free programs.  These disadvantages

+are the reason we use the ordinary General Public License for many

+libraries.  However, the Lesser license provides advantages in certain

+special circumstances.

+

+  For example, on rare occasions, there may be a special need to

+encourage the widest possible use of a certain library, so that it becomes

+a de-facto standard.  To achieve this, non-free programs must be

+allowed to use the library.  A more frequent case is that a free

+library does the same job as widely used non-free libraries.  In this

+case, there is little to gain by limiting the free library to free

+software only, so we use the Lesser General Public License.

+

+  In other cases, permission to use a particular library in non-free

+programs enables a greater number of people to use a large body of

+free software.  For example, permission to use the GNU C Library in

+non-free programs enables many more people to use the whole GNU

+operating system, as well as its variant, the GNU/Linux operating

+system.

+

+  Although the Lesser General Public License is Less protective of the

+users' freedom, it does ensure that the user of a program that is

+linked with the Library has the freedom and the wherewithal to run

+that program using a modified version of the Library.

+

+  The precise terms and conditions for copying, distribution and

+modification follow.  Pay close attention to the difference between a

+"work based on the library" and a "work that uses the library".  The

+former contains code derived from the library, whereas the latter must

+be combined with the library in order to run.

+

+		  GNU LESSER GENERAL PUBLIC LICENSE

+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

+

+  0. This License Agreement applies to any software library or other

+program which contains a notice placed by the copyright holder or

+other authorized party saying it may be distributed under the terms of

+this Lesser General Public License (also called "this License").

+Each licensee is addressed as "you".

+

+  A "library" means a collection of software functions and/or data

+prepared so as to be conveniently linked with application programs

+(which use some of those functions and data) to form executables.

+

+  The "Library", below, refers to any such software library or work

+which has been distributed under these terms.  A "work based on the

+Library" means either the Library or any derivative work under

+copyright law: that is to say, a work containing the Library or a

+portion of it, either verbatim or with modifications and/or translated

+straightforwardly into another language.  (Hereinafter, translation is

+included without limitation in the term "modification".)

+

+  "Source code" for a work means the preferred form of the work for

+making modifications to it.  For a library, complete source code means

+all the source code for all modules it contains, plus any associated

+interface definition files, plus the scripts used to control compilation

+and installation of the library.

+

+  Activities other than copying, distribution and modification are not

+covered by this License; they are outside its scope.  The act of

+running a program using the Library is not restricted, and output from

+such a program is covered only if its contents constitute a work based

+on the Library (independent of the use of the Library in a tool for

+writing it).  Whether that is true depends on what the Library does

+and what the program that uses the Library does.

+

+  1. You may copy and distribute verbatim copies of the Library's

+complete source code as you receive it, in any medium, provided that

+you conspicuously and appropriately publish on each copy an

+appropriate copyright notice and disclaimer of warranty; keep intact

+all the notices that refer to this License and to the absence of any

+warranty; and distribute a copy of this License along with the

+Library.

+

+  You may charge a fee for the physical act of transferring a copy,

+and you may at your option offer warranty protection in exchange for a

+fee.

+

+  2. You may modify your copy or copies of the Library or any portion

+of it, thus forming a work based on the Library, and copy and

+distribute such modifications or work under the terms of Section 1

+above, provided that you also meet all of these conditions:

+

+    a) The modified work must itself be a software library.

+

+    b) You must cause the files modified to carry prominent notices

+    stating that you changed the files and the date of any change.

+

+    c) You must cause the whole of the work to be licensed at no

+    charge to all third parties under the terms of this License.

+

+    d) If a facility in the modified Library refers to a function or a

+    table of data to be supplied by an application program that uses

+    the facility, other than as an argument passed when the facility

+    is invoked, then you must make a good faith effort to ensure that,

+    in the event an application does not supply such function or

+    table, the facility still operates, and performs whatever part of

+    its purpose remains meaningful.

+

+    (For example, a function in a library to compute square roots has

+    a purpose that is entirely well-defined independent of the

+    application.  Therefore, Subsection 2d requires that any

+    application-supplied function or table used by this function must

+    be optional: if the application does not supply it, the square

+    root function must still compute square roots.)

+

+These requirements apply to the modified work as a whole.  If

+identifiable sections of that work are not derived from the Library,

+and can be reasonably considered independent and separate works in

+themselves, then this License, and its terms, do not apply to those

+sections when you distribute them as separate works.  But when you

+distribute the same sections as part of a whole which is a work based

+on the Library, the distribution of the whole must be on the terms of

+this License, whose permissions for other licensees extend to the

+entire whole, and thus to each and every part regardless of who wrote

+it.

+

+Thus, it is not the intent of this section to claim rights or contest

+your rights to work written entirely by you; rather, the intent is to

+exercise the right to control the distribution of derivative or

+collective works based on the Library.

+

+In addition, mere aggregation of another work not based on the Library

+with the Library (or with a work based on the Library) on a volume of

+a storage or distribution medium does not bring the other work under

+the scope of this License.

+

+  3. You may opt to apply the terms of the ordinary GNU General Public

+License instead of this License to a given copy of the Library.  To do

+this, you must alter all the notices that refer to this License, so

+that they refer to the ordinary GNU General Public License, version 2,

+instead of to this License.  (If a newer version than version 2 of the

+ordinary GNU General Public License has appeared, then you can specify

+that version instead if you wish.)  Do not make any other change in

+these notices.

+

+  Once this change is made in a given copy, it is irreversible for

+that copy, so the ordinary GNU General Public License applies to all

+subsequent copies and derivative works made from that copy.

+

+  This option is useful when you wish to copy part of the code of

+the Library into a program that is not a library.

+

+  4. You may copy and distribute the Library (or a portion or

+derivative of it, under Section 2) in object code or executable form

+under the terms of Sections 1 and 2 above provided that you accompany

+it with the complete corresponding machine-readable source code, which

+must be distributed under the terms of Sections 1 and 2 above on a

+medium customarily used for software interchange.

+

+  If distribution of object code is made by offering access to copy

+from a designated place, then offering equivalent access to copy the

+source code from the same place satisfies the requirement to

+distribute the source code, even though third parties are not

+compelled to copy the source along with the object code.

+

+  5. A program that contains no derivative of any portion of the

+Library, but is designed to work with the Library by being compiled or

+linked with it, is called a "work that uses the Library".  Such a

+work, in isolation, is not a derivative work of the Library, and

+therefore falls outside the scope of this License.

+

+  However, linking a "work that uses the Library" with the Library

+creates an executable that is a derivative of the Library (because it

+contains portions of the Library), rather than a "work that uses the

+library".  The executable is therefore covered by this License.

+Section 6 states terms for distribution of such executables.

+

+  When a "work that uses the Library" uses material from a header file

+that is part of the Library, the object code for the work may be a

+derivative work of the Library even though the source code is not.

+Whether this is true is especially significant if the work can be

+linked without the Library, or if the work is itself a library.  The

+threshold for this to be true is not precisely defined by law.

+

+  If such an object file uses only numerical parameters, data

+structure layouts and accessors, and small macros and small inline

+functions (ten lines or less in length), then the use of the object

+file is unrestricted, regardless of whether it is legally a derivative

+work.  (Executables containing this object code plus portions of the

+Library will still fall under Section 6.)

+

+  Otherwise, if the work is a derivative of the Library, you may

+distribute the object code for the work under the terms of Section 6.

+Any executables containing that work also fall under Section 6,

+whether or not they are linked directly with the Library itself.

+

+  6. As an exception to the Sections above, you may also combine or

+link a "work that uses the Library" with the Library to produce a

+work containing portions of the Library, and distribute that work

+under terms of your choice, provided that the terms permit

+modification of the work for the customer's own use and reverse

+engineering for debugging such modifications.

+

+  You must give prominent notice with each copy of the work that the

+Library is used in it and that the Library and its use are covered by

+this License.  You must supply a copy of this License.  If the work

+during execution displays copyright notices, you must include the

+copyright notice for the Library among them, as well as a reference

+directing the user to the copy of this License.  Also, you must do one

+of these things:

+

+    a) Accompany the work with the complete corresponding

+    machine-readable source code for the Library including whatever

+    changes were used in the work (which must be distributed under

+    Sections 1 and 2 above); and, if the work is an executable linked

+    with the Library, with the complete machine-readable "work that

+    uses the Library", as object code and/or source code, so that the

+    user can modify the Library and then relink to produce a modified

+    executable containing the modified Library.  (It is understood

+    that the user who changes the contents of definitions files in the

+    Library will not necessarily be able to recompile the application

+    to use the modified definitions.)

+

+    b) Use a suitable shared library mechanism for linking with the

+    Library.  A suitable mechanism is one that (1) uses at run time a

+    copy of the library already present on the user's computer system,

+    rather than copying library functions into the executable, and (2)

+    will operate properly with a modified version of the library, if

+    the user installs one, as long as the modified version is

+    interface-compatible with the version that the work was made with.

+

+    c) Accompany the work with a written offer, valid for at

+    least three years, to give the same user the materials

+    specified in Subsection 6a, above, for a charge no more

+    than the cost of performing this distribution.

+

+    d) If distribution of the work is made by offering access to copy

+    from a designated place, offer equivalent access to copy the above

+    specified materials from the same place.

+

+    e) Verify that the user has already received a copy of these

+    materials or that you have already sent this user a copy.

+

+  For an executable, the required form of the "work that uses the

+Library" must include any data and utility programs needed for

+reproducing the executable from it.  However, as a special exception,

+the materials to be distributed need not include anything that is

+normally distributed (in either source or binary form) with the major

+components (compiler, kernel, and so on) of the operating system on

+which the executable runs, unless that component itself accompanies

+the executable.

+

+  It may happen that this requirement contradicts the license

+restrictions of other proprietary libraries that do not normally

+accompany the operating system.  Such a contradiction means you cannot

+use both them and the Library together in an executable that you

+distribute.

+

+  7. You may place library facilities that are a work based on the

+Library side-by-side in a single library together with other library

+facilities not covered by this License, and distribute such a combined

+library, provided that the separate distribution of the work based on

+the Library and of the other library facilities is otherwise

+permitted, and provided that you do these two things:

+

+    a) Accompany the combined library with a copy of the same work

+    based on the Library, uncombined with any other library

+    facilities.  This must be distributed under the terms of the

+    Sections above.

+

+    b) Give prominent notice with the combined library of the fact

+    that part of it is a work based on the Library, and explaining

+    where to find the accompanying uncombined form of the same work.

+

+  8. You may not copy, modify, sublicense, link with, or distribute

+the Library except as expressly provided under this License.  Any

+attempt otherwise to copy, modify, sublicense, link with, or

+distribute the Library is void, and will automatically terminate your

+rights under this License.  However, parties who have received copies,

+or rights, from you under this License will not have their licenses

+terminated so long as such parties remain in full compliance.

+

+  9. You are not required to accept this License, since you have not

+signed it.  However, nothing else grants you permission to modify or

+distribute the Library or its derivative works.  These actions are

+prohibited by law if you do not accept this License.  Therefore, by

+modifying or distributing the Library (or any work based on the

+Library), you indicate your acceptance of this License to do so, and

+all its terms and conditions for copying, distributing or modifying

+the Library or works based on it.

+

+  10. Each time you redistribute the Library (or any work based on the

+Library), the recipient automatically receives a license from the

+original licensor to copy, distribute, link with or modify the Library

+subject to these terms and conditions.  You may not impose any further

+restrictions on the recipients' exercise of the rights granted herein.

+You are not responsible for enforcing compliance by third parties with

+this License.

+

+  11. If, as a consequence of a court judgment or allegation of patent

+infringement or for any other reason (not limited to patent issues),

+conditions are imposed on you (whether by court order, agreement or

+otherwise) that contradict the conditions of this License, they do not

+excuse you from the conditions of this License.  If you cannot

+distribute so as to satisfy simultaneously your obligations under this

+License and any other pertinent obligations, then as a consequence you

+may not distribute the Library at all.  For example, if a patent

+license would not permit royalty-free redistribution of the Library by

+all those who receive copies directly or indirectly through you, then

+the only way you could satisfy both it and this License would be to

+refrain entirely from distribution of the Library.

+

+If any portion of this section is held invalid or unenforceable under any

+particular circumstance, the balance of the section is intended to apply,

+and the section as a whole is intended to apply in other circumstances.

+

+It is not the purpose of this section to induce you to infringe any

+patents or other property right claims or to contest validity of any

+such claims; this section has the sole purpose of protecting the

+integrity of the free software distribution system which is

+implemented by public license practices.  Many people have made

+generous contributions to the wide range of software distributed

+through that system in reliance on consistent application of that

+system; it is up to the author/donor to decide if he or she is willing

+to distribute software through any other system and a licensee cannot

+impose that choice.

+

+This section is intended to make thoroughly clear what is believed to

+be a consequence of the rest of this License.

+

+  12. If the distribution and/or use of the Library is restricted in

+certain countries either by patents or by copyrighted interfaces, the

+original copyright holder who places the Library under this License may add

+an explicit geographical distribution limitation excluding those countries,

+so that distribution is permitted only in or among countries not thus

+excluded.  In such case, this License incorporates the limitation as if

+written in the body of this License.

+

+  13. The Free Software Foundation may publish revised and/or new

+versions of the Lesser General Public License from time to time.

+Such new versions will be similar in spirit to the present version,

+but may differ in detail to address new problems or concerns.

+

+Each version is given a distinguishing version number.  If the Library

+specifies a version number of this License which applies to it and

+"any later version", you have the option of following the terms and

+conditions either of that version or of any later version published by

+the Free Software Foundation.  If the Library does not specify a

+license version number, you may choose any version ever published by

+the Free Software Foundation.

+

+  14. If you wish to incorporate parts of the Library into other free

+programs whose distribution conditions are incompatible with these,

+write to the author to ask for permission.  For software which is

+copyrighted by the Free Software Foundation, write to the Free

+Software Foundation; we sometimes make exceptions for this.  Our

+decision will be guided by the two goals of preserving the free status

+of all derivatives of our free software and of promoting the sharing

+and reuse of software generally.

+

+			    NO WARRANTY

+

+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO

+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.

+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR

+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY

+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE

+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR

+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE

+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME

+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

+

+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN

+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY

+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU

+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR

+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE

+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING

+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A

+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF

+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH

+DAMAGES.

+

+		     END OF TERMS AND CONDITIONS

+

+           How to Apply These Terms to Your New Libraries

+

+  If you develop a new library, and you want it to be of the greatest

+possible use to the public, we recommend making it free software that

+everyone can redistribute and change.  You can do so by permitting

+redistribution under these terms (or, alternatively, under the terms of the

+ordinary General Public License).

+

+  To apply these terms, attach the following notices to the library.  It is

+safest to attach them to the start of each source file to most effectively

+convey the exclusion of warranty; and each file should have at least the

+"copyright" line and a pointer to where the full notice is found.

+

+    <one line to give the library's name and a brief idea of what it does.>

+    Copyright (C) <year>  <name of author>

+

+    This library is free software; you can redistribute it and/or

+    modify it under the terms of the GNU Lesser General Public

+    License as published by the Free Software Foundation; either

+    version 2.1 of the License, or (at your option) any later version.

+

+    This library is distributed in the hope that it will be useful,

+    but WITHOUT ANY WARRANTY; without even the implied warranty of

+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU

+    Lesser General Public License for more details.

+

+    You should have received a copy of the GNU Lesser General Public

+    License along with this library; if not, write to the Free Software

+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

+

+Also add information on how to contact you by electronic and paper mail.

+

+You should also get your employer (if you work as a programmer) or your

+school, if any, to sign a "copyright disclaimer" for the library, if

+necessary.  Here is a sample; alter the names:

+

+  Yoyodyne, Inc., hereby disclaims all copyright interest in the

+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.

+

+  <signature of Ty Coon>, 1 April 1990

+  Ty Coon, President of Vice

+

+That's all there is to it!

+

+

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e58c537
--- /dev/null
+++ b/README.md
@@ -0,0 +1,229 @@
+A Beginner's Installation Guide to Taverna Server
+=================================================
+
+When installing Taverna Server 2.5, you *need* to decide whether to
+install in secure or insecure mode. In secure mode, the server
+enforces logins, ensures that they are done over HTTPS, and applies
+strong restrictions to what users can see of other users'
+workflows. In insecure mode, no restrictions are enforced which
+simplifies configuration but greatly reduces the overall system
+security. *Do not mix up installations between the two types.*
+
+You will need:
+
+* **Unix** (e.g., Linux, OSX). Running Linux inside a virtual machine
+  works. Running directly on Windows is not supported.
+
+* **Java 7** (or later) installed. See the Java requirements on the
+  [Taverna website](http://www.taverna.org.uk/download/workbench/system-requirements/).
+
+* **Tomcat 6** (recent version).
+
+* **Taverna Server 2.5**. Either the "full installation" or WAR will do
+  (the "full installation" zip contains a copy of the WAR) - see the
+  [Taverna website](http://www.taverna.org.uk/download/server/2-4/) for details on downloading the file.
+
+If you are installing in secured mode (default) you will also need:
+
+* **SSL** (i.e., HTTPS) **host certificate**. This should not be
+  self-signed and should have the hostname in the Common Name (CN)
+  field. (Self-signed certificates or ones without the hostname in are
+  exceptionally awkward for clients to work with, and proper
+  single-host certificates are in reality very cheap. Save yourself a
+  lot of work here!)
+
+* For the simplest operation, you should create a user `taverna` that is
+  a member of the group called `taverna`. This user will be used for
+  executing workflows, and does not need to allow anyone to log in as
+  it.
+
+Stick to the Factory Defaults
+-----------------------------
+
+Taverna Server 2.5 has a long list of things that may be configured,
+but it comes with “factory” settings that are correct in the majority
+of cases. Leave them alone for your first installation.
+
+Setting up Tomcat
+-----------------
+
+Note that the instructions below do not describe setting up Tomcat
+users. These are not necessary for Taverna Server, as that needs
+finer-grained permission control than is normal for a webapp.
+
+You can always find further information by searching the web for
+“_install tomcat6 YourOperatingSystem_”.
+
+### Installing on Debian Linux, Ubuntu
+
+On Debian Linux (and derivatives), you install Tomcat with:
+
+    sudo apt-get install tomcat6 tomcat6-admin tomcat6-common tomcat6-user
+
+You then start Tomcat with:
+
+    sudo /etc/init.d/tomcat6 start
+
+And stop it with:
+
+    sudo /etc/init.d/tomcat6 stop
+
+It's configuration file (called `conf/server.xml` in the instructions below) will be in:
+
+    /etc/tomcat6/server.xml
+
+It's webapp directory (`webapps` below) will be in:
+
+    /var/lib/tomcat6
+
+### Installing on RedHat Linux, Fedora, CentOS, Scientific Linux
+
+On RedHat Linux (and derivatives), you install Tomcat with:
+
+    yum install tomcat6-webapps
+
+You then start Tomcat with:
+
+    sudo service tomcat6 start
+
+And stop it with:
+
+    sudo service tomcat6 stop
+
+It's configuration file (called `conf/server.xml` in the instructions below) will be in:
+
+    /etc/tomcat6/server.xml
+
+It's webapp directory (`webapps` below) will be in:
+
+    /var/lib/tomcat6
+
+### Installing on MacOS X, and using a baseline Apache distribution
+
+On OSX (or if otherwise installing from a standard Apache
+distribution), you install Tomcat by downloading from the distribution
+page at:
+
+* http://tomcat.apache.org/download-60.cgi
+
+Both ZIP and `.tar.gz` download versions include a file `RUNNING.txt`
+that describes how to perform the installation, start the server, and
+stop it again.
+
+The normal location of the configuration file (`conf/server.xml` in
+the instructions below) is, for Tomcat 6.0.35:
+
+    /usr/local/tomcat6.0/apache-tomcat-6.0.35/conf/server.xml
+
+And its `webapps` directory is at:
+
+    /usr/local/tomcat6.0/apache-tomcat-6.0.35/webapps
+
+Installing an Unsecured Taverna Server
+--------------------------------------
+
+This is not the default configuration of Taverna Server because it is
+_insecure_; there is no attempt to verify the identity of users or to
+keep them from interfering with each other's workflows. _We recommend
+that you use the secured version if possible._
+
+The insecure version is installed by:
+
+### First, place the WAR into Tomcat's webapps directory
+
+Use a filename that relates to what URL you want Taverna Server to
+appear at within Tomcat (e.g., if you want it to be at
+`/tavernaserver`, use the filename `webapps/tavernaserver.war`).
+
+### Next, start Tomcat (if stopped), and shut it down again once it has unpacked the WAR.
+
+At this point, Taverna Server is installed but not usable.
+
+### Then configure for unsecure operation.
+
+Go to the unpacked WAR, find its `WEB-INF/web.xml` (with the above
+installation path, it would be
+`webapps/tavernaserver/WEB-INF/web.xml`), and change the lines:
+
+    <param-value>WEB-INF/secure.xml</param-value>
+    <!-- <param-value>WEB-INF/insecure.xml</param-value> -->
+
+to read:
+
+    <!-- <param-value>WEB-INF/secure.xml</param-value> -->
+    <param-value>WEB-INF/insecure.xml</param-value>
+
+This changes which part of the rest of the server configuration is
+used. It does so by altering what part of that XML file are commented
+out. One of those two `<param-value>` lines **must** be
+uncommented. The overall XML file **must** be valid.
+
+### Finally, start Tomcat.
+
+> **NB:** When accessing an unsecured Taverna Server, for most
+    operations (such as submitting a run) you will need to pass the
+    credentials for the default user. The default user has username
+    `taverna` and password `taverna`.
+
+Installing a Secured Taverna Server
+-----------------------------------
+Taverna Server 2.5 is installed in secure mode by doing this:
+
+### First you need to enable SSL on Tomcat.
+
+With Tomcat not running, make sure that its `conf/server.xml` file
+contains a `<Connector>` definition for SSL HTTP/1.1. The file should
+contain comments on how to do this. Here's an example:
+
+    <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
+        maxThreads="150" scheme="https" secure="true"
+        clientAuth="false" sslProtocol="TLS" keystorePass="tomcat"
+        keystoreFile="conf/tavserv.p12" keystoreType="PKCS12" />
+
+This configuration enables secure access on port 8443 (HTTPS-alt;
+strongly recommended) with the server using the key-pair that has been
+placed in a standard PKCS #12 format file in the file `tavserv.p12` in
+the same directory as the configuration file; the key-pair file will
+be unlocked with the (rather obvious) password “`tomcat`”.
+
+Note that if the configuration file is located below `/etc`, it is
+recommended that you specify the full path to the PKCS #12 file. You
+should also ensure that the file can only be read by the Unix user
+that will be running Tomcat.
+
+### Next, you need to grant permission to the Tomcat server to run code as other users.
+
+In particular, it needs to be able to run the Java executable it is
+using as other people via `sudo`. You _should_ take care to lock this
+down heavily. You do this by using the program visudo to add these
+parts to the sudo configuration. Note that each goes in a separate
+part of the overall file, and that we assume below that Tomcat is
+running as the user `tavserv`; you will probably need to change (e.g.,
+to `tomcat` or `nobody`) as appropriate.
+
+This defines some flags for the main server user:
+
+    Defaults:tavserv   !lecture, timestamp_timeout=0, passwd_tries=1
+
+This defines a rule for who the server can switch to. Let's say that
+they have to be a member of the Unix user group `taverna`; if a user
+isn't in that group, they cannot use Taverna Server. (Note that `root`
+should not be part of the group!)
+
+    Runas_Alias        TAV = %taverna
+
+This creates the actual permission, saying that the `tavserv` user may
+run anything as any user in the alias above (i.e., in the `taverna`
+group). The `NOPASSWD` is important because it allows Taverna Server
+to do the delegation even when running as a user that can't log in.
+
+    tavserv            ALL=(TAV) NOPASSWD: ALL
+
+### Now, place the WAR into Tomcat's `webapps` directory.
+
+Use a filename that relates to what URL you want Taverna Server to
+appear at within Tomcat (e.g., if you want it to be at
+`/tavernaserver`, use the filename `webapps/tavernaserver.war`).
+
+### Finally, start Tomcat.
+
diff --git a/context.sample.xml b/context.sample.xml
new file mode 100644
index 0000000..9531bdb
--- /dev/null
+++ b/context.sample.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Sample context.xml showing how to tweak logging and some parameters. -->
+<!-- See the Install Guide for more information about this file. -->
+<Context path="/taverna-server">
+	<!-- Sample logging configuration. -->
+	<Valve className="org.apache.catalina.valves.AccessLogValve" />
+
+	<!-- Controls whether URs are logged. -->
+	<Parameter name="usage.logFile" value="%{WEBAPPROOT}/usage.log"
+		description="Full path to name of file that usage records for executed workflow runs will be dumped to. To make it relative to the webapp root, prefix with '%{WEBAPPROOT}/'." />
+	<Parameter name="usage.disableDB" value="no"
+		description="Set to yes to disable writing of URs to the database." />
+
+	<!-- For email-dispatched notifications. -->
+	<Parameter name="email.host" value="localhost"
+		description="Where the SMTP server for sending notification emails is located."/>
+	<Parameter name="email.from" value="taverna.server@localhost"
+		description="Who to send notification emails as."/>
+</Context>
diff --git a/install.docx b/install.docx
new file mode 100644
index 0000000..cf9e0a4
--- /dev/null
+++ b/install.docx
Binary files differ
diff --git a/install.pdf b/install.pdf
new file mode 100644
index 0000000..da38ee3
--- /dev/null
+++ b/install.pdf
Binary files differ
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..fc7b0c9
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,560 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>uk.org.taverna.server</groupId>
+	<artifactId>server</artifactId>
+	<packaging>pom</packaging>
+	<version>3.0-SNAPSHOT</version>
+	<name>Taverna Server</name>
+	<description>Taverna Server is a service that provides execution of Taverna Workflows, provided they do not access the user interface while executing.</description>
+	<url>http://www.taverna.org.uk/</url>
+
+	<properties>
+		<!-- Set these to control what is used for signing code. -->
+		<signing.keystore>${project.parent.basedir}/src/main/signing/signing.jks</signing.keystore>
+		<signing.user>taverna</signing.user>
+		<signing.password>taverna</signing.password>
+		<scmBrowseRoot>https://github.com/myGrid/taverna-server/tree/2.5-branch</scmBrowseRoot>
+		<jenkinsJob>taverna-server-2.5-auto</jenkinsJob>
+	</properties>
+
+	<!-- Having to edit anything below here is probably indicative of a bug. -->
+	<parent>
+		<groupId>net.sf.taverna</groupId>
+		<artifactId>taverna-parent</artifactId>
+		<version>3.0.1-SNAPSHOT</version>
+	</parent>
+	<prerequisites>
+		<maven>2.2</maven>
+	</prerequisites>
+	<issueManagement>
+		<system>JIRA</system>
+		<url>http://dev.mygrid.org.uk/issues/browse/TAVSERV</url>
+	</issueManagement>
+	<developers>
+		<developer>
+			<id>donal.k.fellows@man.ac.uk</id>
+			<name>Donal Fellows</name>
+			<email>donal.k.fellows@manchester.ac.uk</email>
+			<organization>The University of Manchester</organization>
+			<organizationUrl>http://www.manchester.ac.uk/</organizationUrl>
+			<timezone>0</timezone>
+			<roles>
+				<role>architect</role>
+				<role>developer</role>
+			</roles>
+		</developer>
+	</developers>
+	<scm>
+		<connection>scm:git://github.com/myGrid/taverna-server.git</connection>
+		<developerConnection>scm:https://github.com/myGrid/taverna-server.git</developerConnection>
+		<url>${scmBrowseRoot}/</url>
+	</scm>
+	<organization>
+		<name>University of Manchester</name>
+		<url>http://www.manchester.ac.uk/</url>
+	</organization>
+	<inceptionYear>2010</inceptionYear>
+	<licenses>
+		<license>
+			<name>LGPL 2.1</name>
+			<url>http://www.opensource.org/licenses/lgpl-2.1.php</url>
+			<comments>This software is made available under the LGPL v2.1. There is no requirement for you to contact us in order to use it, modify it, etc., but please let us know if you are using this software, especially scientifically. We love citations!</comments>
+		</license>
+	</licenses>
+	<mailingLists>
+		<mailingList>
+			<name>Taverna Users</name>
+			<post>taverna-users@lists.sourceforge.net</post>
+			<subscribe>https://lists.sourceforge.net/lists/listinfo/taverna-users</subscribe>
+			<archive>http://sourceforge.net/mailarchive/forum.php?forum_name=taverna-users</archive>
+		</mailingList>
+		<mailingList>
+			<name>Taverna Developers</name>
+			<post>taverna-hackers@lists.sourceforge.net</post>
+			<subscribe>https://lists.sourceforge.net/lists/listinfo/taverna-hackers</subscribe>
+			<archive>http://sourceforge.net/mailarchive/forum.php?forum_name=taverna-hackers</archive>
+		</mailingList>
+	</mailingLists>
+	<contributors>
+		<contributor>
+			<name>Rob Haines</name>
+			<organization>The University of Manchester</organization>
+			<organizationUrl>http://www.manchester.ac.uk/</organizationUrl>
+			<timezone>0</timezone>
+			<roles>
+				<role>code review</role>
+				<role>ruby client</role>
+			</roles>
+		</contributor>
+		<contributor>
+			<name>Alexandra Nenadic</name>
+			<organization>The University of Manchester</organization>
+			<organizationUrl>http://www.manchester.ac.uk/</organizationUrl>
+			<timezone>0</timezone>
+			<roles>
+				<role>website</role>
+			</roles>
+		</contributor>
+		<contributor>
+			<name>Stian Soiland-Reyes</name>
+			<organization>The University of Manchester</organization>
+			<organizationUrl>http://www.manchester.ac.uk/</organizationUrl>
+			<timezone>0</timezone>
+			<roles>
+				<role>code review</role>
+			</roles>
+		</contributor>
+		<contributor>
+			<name>Alan Williams</name>
+			<organization>The University of Manchester</organization>
+			<organizationUrl>http://www.manchester.ac.uk/</organizationUrl>
+			<timezone>0</timezone>
+			<roles>
+				<role>code review</role>
+			</roles>
+		</contributor>
+		<contributor>
+			<name>David Withers</name>
+			<organization>The University of Manchester</organization>
+			<organizationUrl>http://www.manchester.ac.uk/</organizationUrl>
+			<timezone>0</timezone>
+			<roles>
+				<role>platform</role>
+			</roles>
+		</contributor>
+	</contributors>
+	<ciManagement>
+		<system>Jenkins</system>
+		<url>http://build.mygrid.org.uk/ci/job/${jenkinsJob}/</url>
+		<notifiers>
+			<notifier>
+				<type>mail</type>
+				<sendOnSuccess>false</sendOnSuccess>
+				<configuration>
+					<recipients>donal.k.fellows@manchester.ac.uk</recipients>
+				</configuration>
+			</notifier>
+		</notifiers>
+	</ciManagement>
+	<repositories>
+		<repository>
+			<id>mygrid-repository</id>
+			<name>myGrid Respository</name>
+			<url>http://build.mygrid.org.uk/maven/repository</url>
+			<snapshots>
+				<enabled>false</enabled>
+			</snapshots>
+			<releases />
+		</repository>
+		<repository>
+			<id>mygrid-snapshot-repository</id>
+			<name>myGrid Snapshot Repository</name>
+			<url>http://build.mygrid.org.uk/maven/snapshot-repository</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+			<releases />
+		</repository>
+		<repository>
+			<id>mygrid-snapshots</id>
+			<name>myGrid Snapshot Respository</name>
+			<url>http://www.mygrid.org.uk/maven/snapshot-repository</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+			<releases>
+				<enabled>false</enabled>
+			</releases>
+		</repository>
+		<repository>
+			<snapshots>
+				<enabled>false</enabled>
+			</snapshots>
+			<id>central2</id>
+			<name>New Central Maven Repository</name>
+			<url>http://repo2.maven.org/maven2</url>
+		</repository>
+	</repositories>
+
+	<build>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-release-plugin</artifactId>
+					<version>2.4.2</version>
+					<configuration>
+						<autoVersionSubmodules>true</autoVersionSubmodules>
+					</configuration>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-compiler-plugin</artifactId>
+					<version>3.1</version>
+					<configuration>
+						<encoding>US-ASCII</encoding>
+						<source>1.7</source>
+						<target>1.7</target>
+					</configuration>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-eclipse-plugin</artifactId>
+					<version>2.9</version>
+					<configuration>
+						<projectNameTemplate>[artifactId]-[version]</projectNameTemplate>
+						<wtpmanifest>true</wtpmanifest>
+						<wtpapplicationxml>true</wtpapplicationxml>
+						<wtpversion>2.0</wtpversion>
+					</configuration>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-resources-plugin</artifactId>
+					<version>2.6</version>
+					<configuration>
+						<encoding>US-ASCII</encoding>
+					</configuration>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-jar-plugin</artifactId>
+					<version>2.4</version>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-source-plugin</artifactId>
+					<version>2.2.1</version>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-surefire-plugin</artifactId>
+					<version>2.16</version>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-jarsigner-plugin</artifactId>
+					<version>1.2</version>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-dependency-plugin</artifactId>
+					<version>2.8</version>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-assembly-plugin</artifactId>
+					<version>2.4</version>
+					<dependencies>
+						<dependency>
+							<groupId>org.codehaus.plexus</groupId>
+							<artifactId>plexus-utils</artifactId>
+							<version>3.0.15</version>
+						</dependency>
+					</dependencies>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-release-plugin</artifactId>
+				<configuration>
+					<autoVersionSubmodules>true</autoVersionSubmodules>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+	<dependencies>
+		<dependency>
+			<groupId>com.google.code.findbugs</groupId>
+			<artifactId>jsr305</artifactId>
+			<version>1.3.7</version>
+		</dependency>
+	</dependencies>
+	<dependencyManagement>
+		<dependencies>
+			<dependency>
+				<groupId>commons-codec</groupId>
+				<artifactId>commons-codec</artifactId>
+				<version>1.8</version>
+			</dependency>
+			<dependency>
+				<groupId>commons-collections</groupId>
+				<artifactId>commons-collections</artifactId>
+				<version>3.2.1</version>
+			</dependency>
+			<dependency>
+				<groupId>commons-dbcp</groupId>
+				<artifactId>commons-dbcp</artifactId>
+				<version>1.4</version>
+			</dependency>
+			<dependency>
+				<groupId>commons-io</groupId>
+				<artifactId>commons-io</artifactId>
+				<version>2.3</version>
+			</dependency>
+			<dependency>
+				<groupId>commons-lang</groupId>
+				<artifactId>commons-lang</artifactId>
+				<version>2.6</version>
+			</dependency>
+			<dependency>
+				<groupId>commons-logging</groupId>
+				<artifactId>commons-logging</artifactId>
+				<version>1.1.3</version>
+			</dependency>
+			<dependency>
+				<groupId>jaxen</groupId>
+				<artifactId>jaxen</artifactId>
+				<version>1.1.4</version>
+			</dependency>
+			<dependency>
+				<groupId>junit</groupId>
+				<artifactId>junit</artifactId>
+				<version>4.11</version>
+				<scope>test</scope>
+			</dependency>
+			<dependency>
+				<groupId>net.sf.mime-util</groupId>
+				<artifactId>mime-util</artifactId>
+				<version>1.2</version>
+			</dependency>
+			<dependency>
+				<groupId>org.apache.httpcomponents</groupId>
+				<artifactId>httpclient</artifactId>
+				<version>4.3.1</version>
+			</dependency>
+			<dependency>
+				<groupId>joda-time</groupId>
+				<artifactId>joda-time</artifactId>
+				<version>2.3</version>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
+	<reporting>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-javadoc-plugin</artifactId>
+				<version>2.8</version>
+				<configuration>
+					<show>protected</show>
+					<nohelp>true</nohelp>
+					<detectLinks>true</detectLinks>
+					<stylesheet>maven</stylesheet>
+					<!-- The version package is purely implementation convenience. -->
+					<excludePackageNames>*.version:*.mocks</excludePackageNames>
+					<groups>
+						<group>
+							<title>Server Core WebService</title>
+							<packages>org.taverna.server.master:org.taverna.server.master.common:org.taverna.server.master.facade:org.taverna.server.master.rest:org.taverna.server.master.rest.handler:org.taverna.server.master.soap</packages>
+						</group>
+						<group>
+							<title>Default Values and Administration</title>
+							<packages>org.taverna.server.master.admin:org.taverna.server.master.defaults</packages>
+						</group>
+						<group>
+							<title>Server SPI</title>
+							<packages>org.taverna.server.master.exceptions:org.taverna.server.master.factories:org.taverna.server.master.interfaces</packages>
+						</group>
+						<group>
+							<title>Notification and Accounting</title>
+							<packages>org.taverna.server.master.interaction:org.taverna.server.master.notification:org.taverna.server.master.notification.atom:org.taverna.server.master.usage</packages>
+						</group>
+						<group>
+							<title>Server Coupling to RMI Back End</title>
+							<packages>org.taverna.server.master.identity:org.taverna.server.master.localworker:org.taverna.server.master.worker</packages>
+						</group>
+						<group>
+							<title>Server RMI Interface to Back End</title>
+							<packages>org.taverna.server.localworker.remote:org.taverna.server.localworker.server</packages>
+						</group>
+						<group>
+							<title>Externally-Defined Document Formats</title>
+							<packages>org.taverna.server.port_description:org.ogf.usage:org.ogf.usage.v1_0:org.w3._2000._09.xmldsig_</packages>
+						</group>
+						<group>
+							<title>Server Back End Factory Coupling to Command Line Executor</title>
+							<packages>org.taverna.server.localworker.impl:org.taverna.server.unixforker:org.taverna.server.winforker</packages>
+						</group>
+						<group>
+							<title>Utilities</title>
+							<packages>org.taverna.server.master.utils:org.taverna.server.localworker.impl.utils</packages>
+						</group>
+					</groups>
+					<detectJavaApiLink>true</detectJavaApiLink>
+					<aggregate>true</aggregate>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-changelog-plugin</artifactId>
+				<version>2.2</version>
+				<configuration>
+					<issueLinkUrl>http://dev.mygrid.org.uk/issues/browse/%ISSUE%</issueLinkUrl>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-project-info-reports-plugin</artifactId>
+				<version>2.4</version>
+			</plugin>
+		</plugins>
+	</reporting>
+
+	<profiles>
+		<profile>
+			<id>site</id>
+			<activation>
+				<file>
+					<exists>${basedir}</exists>
+				</file>
+			</activation>
+			<build>
+				<pluginManagement>
+					<plugins>
+						<plugin>
+							<groupId>org.apache.maven.plugins</groupId>
+							<artifactId>maven-site-plugin</artifactId>
+							<version>3.0</version>
+						</plugin>
+					</plugins>
+				</pluginManagement>
+			</build>
+		</profile>
+		<profile>
+			<id>full-site</id>
+			<reporting>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-javadoc-plugin</artifactId>
+						<version>2.8</version>
+						<configuration>
+							<show>private</show>
+							<nohelp>true</nohelp>
+							<detectLinks>true</detectLinks>
+							<linksource>true</linksource>
+							<stylesheet>maven</stylesheet>
+							<!-- The version package is purely implementation convenience. -->
+							<groups>
+								<group>
+									<title>Server Core WebService</title>
+									<packages>org.taverna.server.master:org.taverna.server.master.common:org.taverna.server.master.facade:org.taverna.server.master.rest:org.taverna.server.master.rest.handler:org.taverna.server.master.soap</packages>
+								</group>
+								<group>
+									<title>Default Values and Administration</title>
+									<packages>org.taverna.server.master.admin:org.taverna.server.master.defaults</packages>
+								</group>
+								<group>
+									<title>Server SPI</title>
+									<packages>org.taverna.server.master.exceptions:org.taverna.server.master.factories:org.taverna.server.master.interfaces</packages>
+								</group>
+								<group>
+									<title>Notification and Accounting</title>
+									<packages>org.taverna.server.master.interaction:org.taverna.server.master.notification:org.taverna.server.master.notification.atom:org.taverna.server.master.usage</packages>
+								</group>
+								<group>
+									<title>Server Coupling to RMI Back End</title>
+									<packages>org.taverna.server.master.identity:org.taverna.server.master.localworker:org.taverna.server.master.worker</packages>
+								</group>
+								<group>
+									<title>Server RMI Interface to Back End</title>
+									<packages>org.taverna.server.localworker.remote:org.taverna.server.localworker.server</packages>
+								</group>
+								<group>
+									<title>Externally-Defined Document Formats</title>
+									<packages>org.taverna.server.port_description:org.ogf.usage:org.ogf.usage.v1_0:org.w3._2000._09.xmldsig_</packages>
+								</group>
+								<group>
+									<title>Server Back End Factory Coupling to Command Line Executor</title>
+									<packages>org.taverna.server.localworker.impl:org.taverna.server.unixforker:org.taverna.server.winforker</packages>
+								</group>
+								<group>
+									<title>Utilities</title>
+									<packages>org.taverna.server.master.utils:org.taverna.server.localworker.impl.utils</packages>
+								</group>
+							</groups>
+							<detectJavaApiLink>true</detectJavaApiLink>
+							<aggregate>true</aggregate>
+						</configuration>
+					</plugin>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-changes-plugin</artifactId>
+						<version>2.6</version>
+						<reportSets>
+							<reportSet>
+								<id>jira-report</id>
+								<reports>
+									<report>jira-report</report>
+								</reports>
+							</reportSet>
+						</reportSets>
+					</plugin>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-checkstyle-plugin</artifactId>
+						<version>2.7</version>
+					</plugin>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-pmd-plugin</artifactId>
+						<version>2.5</version>
+					</plugin>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-surefire-report-plugin</artifactId>
+						<version>2.9</version>
+					</plugin>
+				</plugins>
+			</reporting>
+		</profile>
+		<profile>
+			<id>signed</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-jarsigner-plugin</artifactId>
+						<executions>
+							<execution>
+								<goals>
+									<goal>sign</goal>
+								</goals>
+								<id>sign</id>
+								<phase>package</phase>
+								<configuration>
+									<keystore>${signing.keystore}</keystore>
+									<alias>${signing.user}</alias>
+									<storepass>${signing.password}</storepass>
+									<keypass>${signing.password}</keypass>
+									<excludeClassifiers>
+										<param>tests</param>
+										<param>test-sources</param>
+									</excludeClassifiers>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+	</profiles>
+
+	<modules>
+		<module>server-webapp</module>
+		<module>server-runinterface</module>
+		<module>server-worker</module>
+		<module>server-unix-forker</module>
+		<module>server-usagerecord</module>
+		<module>server-port-description</module>
+		<module>server-execution-delegate</module>
+		<module>server-rmidaemon</module>
+		<module>server-client</module>
+		<module>server-distribution</module>
+	</modules>
+</project>
diff --git a/release-notes.txt b/release-notes.txt
new file mode 100644
index 0000000..0f3396d
--- /dev/null
+++ b/release-notes.txt
@@ -0,0 +1,97 @@
+Taverna 2.5.4 Server Release Notes
+==================================
+
+This is the fourth public release of Taverna 2.5 Server. We welcome feedback
+on both the things that are there and the things that are not.
+
+Key Features
+------------
+ * Runs arbitrary Taverna 2 workflows
+   * Based on Taverna 2.5 Workflow Engine
+   * Includes support for Components and Interaction processors
+ * REST and SOAP interfaces
+   * All functionality available through both interfaces
+ * Manages files for workflows
+   * Make files, read files, delete files
+   * Create subdirectories, list directory contents
+   * Can download a whole directory structure as a ZIP
+ * Tidies up when workflow runs expire
+   * Expiry time fully configurable
+   * Can force immediate deletion of a workflow run
+ * Generates Run Bundles
+   * Contains formal description of the workflow run, designed for
+     sharing, including the inputs, outputs and provenance trace
+ * Security
+   * Encrypted communication supported
+   * Multiple users
+     * Users isolated from each other (via sudo)
+     * Workflows isolated from server
+     * Users may grant access to other users
+   * Specify credentials for workflows to access back-end services
+ * Notification framework
+   * Inform users when workflows terminate
+     * Many protocols: Atom feed, email, SMS, Twitter, Jabber
+ * Management interface
+   * Administrative access to all server's tunable parameters
+     * Authenticated web interface, JMX
+   * Usage monitoring/accounting
+ * General quality improvements
+   * Improved speed
+   * Improved robustness
+     * State can persist over (limited) server restarts
+   * Improved installation
+     * Self-contained server package
+   * Support for transfer of large data files
+
+Significant Changes
+-------------------
+ * Supports full Taverna 2.5.0 Enterprise execution platform
+ * Now requires Java 7
+
+Planned Future Features
+-----------------------
+ * Support for Cluster Deployment
+ * Full WebDAV access to run working directory
+
+Specific Issues Addressed in This Release
+-----------------------------------------
+See http://www.mygrid.org.uk/dev/issues/browse/TAVSERV
+
+TAVSERV-5 Need to detect interactive localworkers
+TAVSERV-76 Support user-supplied functionality
+TAVSERV-283	Invocation is spelt as invokation in the admin interface
+TAVSERV-293	A HEAD or OPTIONS to the wsdl address returns a 500 error
+TAVSERV-301 Interaction feed address is not a full URI
+TAVSERV-309 Failure in getRunOutputDescription SOAP operation
+TAVSERV-310	Support OPTIONS in main interface
+---- 2.5.0 release ----
+TAVSERV-219 Some server errors are reported as 403 when 500 is more
+            appropriate
+TAVSERV-290 Support restricting what URLs to use workflows from
+TAVSERV-307 Users created through the admin web interface are not properly
+            set up in "insecure" mode
+TAVSERV-321 Interaction service internal bug
+TAVSERV-322 Atom feed updated timestamp being rounded down
+TAVSERV-325 Can't create run with long default name
+TAVSERV-326 Log concatenation
+TAVSERV-328 An OPTIONS request to any resource produces incorrect headers
+---- 2.5.1 release ----
+TAVSERV-42 Provenance: Access and Lifetime
+TAVSERV-44 Managing Workflow Instances
+TAVSERV-69 Provenance: core
+TAVSERV-297 Running instance of Taverna Server 2.4.1 hangs after a while;
+            submitting a new workflow using REST API results in "HTTP 403
+            Permission denied"
+---- 2.5.2 release ----
+TAVSERV-329 Master feed broken
+TAVSERV-332 Massively degraded performance of 2.5.2
+---- 2.5.3 release ----
+    Updated to use Taverna 2.5.0 Enterprise Execution Core
+TAVSERV-103 Generated WSDL interface incorrectly claims that parameters are
+            optional
+TAVSERV-331 Add an option for the provenance export using the new -provbundle
+            command line tool switch
+TAVSERV-336 Support splitting inputs 
+TAVSERV-337 RMI registry subprocess race condition
+SERVINF-395 Fix race condition in XML parsing
+---- 2.5.4 release ----
\ No newline at end of file
diff --git a/server-client/pom.xml b/server-client/pom.xml
new file mode 100644
index 0000000..e62978a
--- /dev/null
+++ b/server-client/pom.xml
@@ -0,0 +1,123 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>uk.org.taverna.server</groupId>
+		<artifactId>server</artifactId>
+		<version>3.0-SNAPSHOT</version>
+	</parent>
+	<artifactId>server-client</artifactId>
+	<packaging>bundle</packaging>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.jvnet.ws.wadl</groupId>
+			<artifactId>wadl-core</artifactId>
+			<version>1.1.6</version>
+		</dependency>
+		<dependency>
+			<groupId>com.sun.jersey</groupId>
+			<artifactId>jersey-client</artifactId>
+			<version>1.8</version>
+		</dependency>
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+			<version>2.4</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.tika</groupId>
+			<artifactId>tika-core</artifactId>
+			<version>1.5</version>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-usagerecord</artifactId>
+			<version>${project.version}</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.felix</groupId>
+				<artifactId>maven-bundle-plugin</artifactId>
+				<extensions>true</extensions>
+				<configuration>
+					<instructions>
+						<Export-Package>uk.org.taverna.server.client</Export-Package>
+						<Private-Package>uk.org.taverna.server.client.*</Private-Package>
+					</instructions>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.jvnet.ws.wadl</groupId>
+				<artifactId>wadl-client-plugin</artifactId>
+				<version>1.1.6</version>
+				<executions>
+					<execution>
+						<goals>
+							<goal>generate</goal>
+						</goals>
+					</execution>
+				</executions>
+				<configuration>
+					<packageName>org.taverna.server.client.wadl</packageName>
+					<includes>*.wadl</includes>
+					<customClassNames>
+						<property>
+							<name>http://example.com/taverna/rest</name>
+							<value>TavernaServer</value>
+						</property>
+					</customClassNames>
+				</configuration>
+			</plugin>
+		</plugins>
+		<pluginManagement>
+			<plugins>
+				<!--This plugin's configuration is used to store Eclipse m2e settings 
+					only. It has no influence on the Maven build itself. -->
+				<plugin>
+					<groupId>org.eclipse.m2e</groupId>
+					<artifactId>lifecycle-mapping</artifactId>
+					<version>1.0.0</version>
+					<configuration>
+						<lifecycleMappingMetadata>
+							<pluginExecutions>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>
+											org.jvnet.ws.wadl
+										</groupId>
+										<artifactId>
+											wadl-client-plugin
+										</artifactId>
+										<versionRange>
+											[1.1.6,)
+										</versionRange>
+										<goals>
+											<goal>generate</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<execute />
+									</action>
+								</pluginExecution>
+							</pluginExecutions>
+						</lifecycleMappingMetadata>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
+	<inceptionYear>2014</inceptionYear>
+	<name>Server Client OSGi Module</name>
+</project>
\ No newline at end of file
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/Connected.java b/server-client/src/main/java/uk/org/taverna/server/client/Connected.java
new file mode 100644
index 0000000..263034c
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/Connected.java
@@ -0,0 +1,20 @@
+package uk.org.taverna.server.client;
+
+import uk.org.taverna.server.client.TavernaServer.ClientException;
+import uk.org.taverna.server.client.TavernaServer.ServerException;
+
+import com.sun.jersey.api.client.ClientResponse;
+
+abstract class Connected {
+	void checkError(ClientResponse response) throws ClientException,
+			ServerException {
+		ClientResponse.Status s = response.getClientResponseStatus();
+		if (s.getStatusCode() == 401)
+			throw new TavernaServer.AuthorizationException("not authorized",
+					null);
+		if (s.getStatusCode() >= 500)
+			throw new TavernaServer.ServerException(s.getReasonPhrase(), null);
+		if (s.getStatusCode() >= 400)
+			throw new TavernaServer.ClientException(s.getReasonPhrase(), null);
+	}
+}
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/DirEntry.java b/server-client/src/main/java/uk/org/taverna/server/client/DirEntry.java
new file mode 100644
index 0000000..267707d
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/DirEntry.java
@@ -0,0 +1,39 @@
+package uk.org.taverna.server.client;
+
+import org.taverna.server.client.wadl.TavernaServer.Root.RunsRunName.Wd.Path2;
+
+import uk.org.taverna.server.client.TavernaServer.ClientException;
+import uk.org.taverna.server.client.TavernaServer.ServerException;
+
+import com.sun.jersey.api.client.ClientResponse;
+
+public abstract class DirEntry extends Connected {
+	final Path2 handle;
+	final String path;
+	final Run run;
+
+	protected DirEntry(Run run, String path) {
+		this.run = run;
+		this.path = path.replaceFirst("/+$", "");
+		this.handle = run.run.wd().path2(this.path);
+	}
+
+	public void delete() throws ClientException, ServerException {
+		checkError(handle.deleteAsXml(ClientResponse.class));
+	}
+
+	String path(ClientResponse response) throws ClientException, ServerException {
+		checkError(response);
+		String[] bits = response.getLocation().getPath().split("/");
+		return concat(bits[bits.length - 1]);
+	}
+
+	String localName() {
+		String[] bits = path.split("/");
+		return bits[bits.length - 1];
+	}
+
+	String concat(String name) {
+		return path + "/" + name.split("/", 2)[0];
+	}
+}
\ No newline at end of file
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/Directory.java b/server-client/src/main/java/uk/org/taverna/server/client/Directory.java
new file mode 100644
index 0000000..38dc394
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/Directory.java
@@ -0,0 +1,94 @@
+package uk.org.taverna.server.client;
+
+import static java.io.File.createTempFile;
+import static javax.ws.rs.client.Entity.entity;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipFile;
+
+import org.taverna.server.client.wadl.TavernaServer.Root.RunsRunName.Wd;
+
+import uk.org.taverna.server.client.TavernaServer.ClientException;
+import uk.org.taverna.server.client.TavernaServer.ServerException;
+import uk.org.taverna.server.client.generic.DirectoryEntry;
+import uk.org.taverna.server.client.generic.DirectoryReference;
+import uk.org.taverna.server.client.generic.FileReference;
+import uk.org.taverna.server.client.rest.DirectoryContents;
+import uk.org.taverna.server.client.rest.MakeDirectory;
+import uk.org.taverna.server.client.rest.UploadFile;
+
+import com.sun.jersey.api.client.ClientResponse;
+
+public class Directory extends DirEntry {
+	private final Wd wd;
+
+	Directory(Run run) {
+		super(run, "");
+		this.wd = run.run.wd();
+	}
+
+	Directory(Run run, String path) {
+		super(run, path);
+		this.wd = run.run.wd();
+	}
+
+	public List<DirEntry> list() {
+		List<DirEntry> result = new ArrayList<>();
+		for (DirectoryEntry de : wd.path3(path)
+				.getAsXml(DirectoryContents.class).getDirOrFile())
+			if (de instanceof DirectoryReference)
+				result.add(new Directory(run, de.getValue()));
+			else if (de instanceof FileReference)
+				result.add(new File(run, de.getValue()));
+		return result;
+	}
+
+	public File createFile(String name, byte[] content) throws ClientException,
+			ServerException {
+		UploadFile uf = new UploadFile();
+		uf.setName(name);
+		uf.setValue(content);
+		return new File(run, path(wd.path(path).putAsXml(uf,
+				ClientResponse.class)));
+	}
+
+	public File createFile(String name, java.io.File content)
+			throws ClientException, ServerException {
+		return new File(run, path(wd.path(concat(name)).putOctetStreamAsXml(
+				entity(content, APPLICATION_OCTET_STREAM_TYPE),
+				ClientResponse.class)));
+	}
+
+	public File createFile(String name, URI source) throws ClientException,
+			ServerException {
+		return new File(run, path(wd.path(concat(name)).postTextUriListAsXml(
+				source.toString(), ClientResponse.class)));
+	}
+
+	public Directory createDirectory(String name) throws ClientException,
+			ServerException {
+		MakeDirectory mkdir = new MakeDirectory();
+		mkdir.setName(name);
+		return new Directory(run, path(wd.path(path).putAsXml(mkdir,
+				ClientResponse.class)));
+	}
+
+	public byte[] getZippedContents() {
+		return wd.path3(path).getAsZip(byte[].class);
+	}
+
+	public ZipFile getZip() throws IOException {
+		byte[] contents = getZippedContents();
+		java.io.File tmp = createTempFile(localName(), ".zip");
+		try (OutputStream os = new FileOutputStream(tmp)) {
+			os.write(contents);
+		}
+		return new ZipFile(tmp);
+	}
+}
\ No newline at end of file
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/File.java b/server-client/src/main/java/uk/org/taverna/server/client/File.java
new file mode 100644
index 0000000..0287afb
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/File.java
@@ -0,0 +1,95 @@
+package uk.org.taverna.server.client;
+
+import static java.io.File.createTempFile;
+import static javax.ws.rs.client.Entity.entity;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
+import static org.apache.commons.io.IOUtils.copy;
+import static org.apache.tika.mime.MimeTypes.getDefaultMimeTypes;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+import org.apache.tika.mime.MimeTypeException;
+import org.taverna.server.client.wadl.TavernaServer.Root.RunsRunName.Wd;
+
+import uk.org.taverna.server.client.TavernaServer.ClientException;
+import uk.org.taverna.server.client.TavernaServer.ServerException;
+
+import com.sun.jersey.api.client.ClientHandlerException;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.UniformInterfaceException;
+
+public class File extends DirEntry {
+	private final Wd wd;
+
+	File(Run run, String path) {
+		super(run, path);
+		wd = run.run.wd();
+	}
+
+	public InputStream getAsStream() {
+		return wd.path3(path).getAsOctetStream(InputStream.class);
+	}
+
+	public byte[] get() {
+		return wd.path3(path).getAsOctetStream(byte[].class);
+	}
+
+	public String get(Charset encoding) {
+		return new String(wd.path3(path).getAsOctetStream(byte[].class),
+				encoding);
+	}
+
+	public java.io.File getAsFile() throws ClientHandlerException,
+			UniformInterfaceException, IOException, MimeTypeException,
+			ClientException, ServerException {
+		ClientResponse cr = wd.path3(path).getAsOctetStream(
+				ClientResponse.class);
+		checkError(cr);
+		String[] bits = localName().split("[.]");
+		String ext = getDefaultMimeTypes().forName(
+				cr.getHeaders().getFirst("Content-Type")).getExtension();
+		if (ext == null)
+			ext = bits[bits.length - 1];
+		java.io.File tmp = createTempFile(bits[0], ext);
+		try (OutputStream os = new FileOutputStream(tmp);
+				InputStream is = cr.getEntity(InputStream.class)) {
+			copy(is, os);
+		}
+		return tmp;
+	}
+
+	public void setContents(byte[] newContents) throws ClientException,
+			ServerException {
+		checkError(wd.path(path).putOctetStreamAsXml(newContents,
+				ClientResponse.class));
+	}
+
+	public void setContents(String newContents) throws ClientException,
+			ServerException {
+		checkError(wd.path(path).putOctetStreamAsXml(newContents,
+				ClientResponse.class));
+	}
+
+	public void setContents(String newContents, Charset encoding)
+			throws ClientException, ServerException {
+		checkError(wd.path(path).putOctetStreamAsXml(
+				newContents.getBytes(encoding), ClientResponse.class));
+	}
+
+	public void setContents(InputStream newContents) throws ClientException,
+			ServerException {
+		checkError(wd.path(path).putOctetStreamAsXml(newContents,
+				ClientResponse.class));
+	}
+
+	public void setContents(java.io.File newContents) throws IOException,
+			ClientException, ServerException {
+		checkError(wd.path(path).putOctetStreamAsXml(
+				entity(newContents, APPLICATION_OCTET_STREAM_TYPE),
+				ClientResponse.class));
+	}
+}
\ No newline at end of file
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/Property.java b/server-client/src/main/java/uk/org/taverna/server/client/Property.java
new file mode 100644
index 0000000..0e6542f
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/Property.java
@@ -0,0 +1,18 @@
+package uk.org.taverna.server.client;
+
+public enum Property {
+	STDOUT("stdout"), STDERR("stderr"), EXIT_CODE("exitcode"), READY_TO_NOTIFY(
+			"readyToNotify"), EMAIL("notificationAddress"), USAGE(
+			"usageRecord");
+
+	private String s;
+
+	private Property(String s) {
+		this.s = s;
+	}
+
+	@Override
+	public String toString() {
+		return s;
+	}
+}
\ No newline at end of file
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/Run.java b/server-client/src/main/java/uk/org/taverna/server/client/Run.java
new file mode 100644
index 0000000..5c6875e
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/Run.java
@@ -0,0 +1,215 @@
+package uk.org.taverna.server.client;
+
+import static org.joda.time.format.ISODateTimeFormat.dateTime;
+import static org.joda.time.format.ISODateTimeFormat.dateTimeParser;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Date;
+import java.util.List;
+
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.io.IOUtils;
+import org.joda.time.DateTime;
+import org.ogf.usage.JobUsageRecord;
+import org.taverna.server.client.wadl.TavernaServer.Root.RunsRunName;
+import org.w3c.dom.Element;
+
+import uk.org.taverna.server.client.TavernaServer.ClientException;
+import uk.org.taverna.server.client.TavernaServer.ServerException;
+import uk.org.taverna.server.client.generic.KeyPairCredential;
+import uk.org.taverna.server.client.generic.PasswordCredential;
+import uk.org.taverna.server.client.generic.port.InputPort;
+import uk.org.taverna.server.client.generic.port.OutputPort;
+import uk.org.taverna.server.client.rest.InputDescription;
+import uk.org.taverna.server.client.rest.InputDescription.Value;
+
+import com.sun.jersey.api.client.ClientResponse;
+
+public class Run extends Connected {
+	RunsRunName run;
+
+	Run(TavernaServer server, String value) {
+		run = server.root.runsRunName(value);
+	}
+
+	public String getName() {
+		return run.name().getAsTextPlain(ClientResponse.class)
+				.getEntity(String.class);
+	}
+
+	public void setName(String name) {
+		run.name().putTextPlain(name, String.class);
+	}
+
+	public Date getExpiry() {
+		return dateTimeParser().parseDateTime(
+				run.expiry().getAsTextPlain(String.class)).toDate();
+	}
+
+	public void setExpiry(Date expiryTimestamp) {
+		run.expiry().putTextPlain(
+				dateTime().print(new DateTime(expiryTimestamp)), String.class);
+	}
+
+	public Date getCreate() {
+		String timestamp = run.createTime().getAsTextPlain(String.class);
+		if (timestamp == null || timestamp.trim().isEmpty())
+			return null;
+		return dateTimeParser().parseDateTime(timestamp).toDate();
+	}
+
+	public Date getStart() {
+		String timestamp = run.startTime().getAsTextPlain(String.class);
+		if (timestamp == null || timestamp.trim().isEmpty())
+			return null;
+		return dateTimeParser().parseDateTime(timestamp).toDate();
+	}
+
+	public Date getFinish() {
+		String timestamp = run.finishTime().getAsTextPlain(String.class);
+		if (timestamp == null || timestamp.trim().isEmpty())
+			return null;
+		return dateTimeParser().parseDateTime(timestamp).toDate();
+	}
+
+	public Status getStatus() {
+		return Status.valueOf(run.status().getAsTextPlain(String.class));
+	}
+
+	public void setStatus(Status status) {
+		run.status().putTextPlain(status, String.class);
+	}
+
+	public void start() {
+		setStatus(Status.Operating);
+	}
+
+	public void kill() {
+		setStatus(Status.Finished);
+	}
+
+	public boolean isRunning() {
+		return getStatus() == Status.Operating;
+	}
+
+	public String getStandardOutput() {
+		return run.stdout().getAsTextPlain(String.class);
+	}
+
+	public String getStandardError() {
+		return run.stderr().getAsTextPlain(String.class);
+	}
+
+	public String getLog() {
+		return run.log().getAsTextPlain(String.class);
+	}
+
+	public Integer getExitCode() {
+		String code = run.listeners().name("io")
+				.propertiesPropertyName("exitCode")
+				.getAsTextPlain(String.class);
+		if (code == null || code.trim().isEmpty())
+			return null;
+		return Integer.parseInt(code);
+	}
+
+	public String getProperty(Property prop) {
+		return run.listeners().name("io")
+				.propertiesPropertyName(prop.toString())
+				.getAsTextPlain(String.class);
+	}
+
+	public void setGenerateRunBundle(boolean generateRunBundle) {
+		run.generateProvenance().putTextPlain(generateRunBundle, String.class);
+	}
+
+	public byte[] getRunBundle() {
+		return run.runBundle().getAsVndWf4everRobundleZip(byte[].class);
+	}
+
+	public List<InputPort> getInputs() {
+		return run.input().expected().getAsInputDescriptionXml().getInput();
+	}
+
+	public List<OutputPort> getOutputs() {
+		return run.output().getAsOutputDescriptionXml().getOutput();
+	}
+
+	public void setInput(String name, String value) {
+		Value v = new Value();
+		v.setValue(value);
+		InputDescription idesc = new InputDescription();
+		idesc.setValue(v);
+		run.input().inputName(name).putXmlAsInputDescription(idesc);
+	}
+
+	public void setInput(String name, String value, char listSeparator) {
+		Value v = new Value();
+		v.setValue(value);
+		InputDescription idesc = new InputDescription();
+		idesc.setValue(v);
+		idesc.setListDelimiter(new String(new char[] { listSeparator }));
+		run.input().inputName(name).putXmlAsInputDescription(idesc);
+	}
+
+	public byte[] getWorkflow() {
+		return run.workflow().getAsVndTavernaT2flowXml(byte[].class);
+	}
+
+	// TODO Consider better ways to do this
+	public Element getInteractionFeed() {
+		return run.interaction().getAsAtomXml(Element.class);
+	}
+
+	public Element getInteractionEntry(String id) {
+		return run.interaction().id(id).getAsAtomXml(Element.class);
+	}
+
+	public JobUsageRecord getUsageRecord() throws JAXBException {
+		return JobUsageRecord.unmarshal(run.usage().getAsXml(Element.class));
+	}
+
+	public Directory getWorkingDirectory() {
+		return new Directory(this);
+	}
+
+	public String getOwner() {
+		return run.security().owner().getAsTextPlain(String.class);
+	}
+
+	// TODO permissions
+
+	public void grantPasswordCredential(URI contextService, String username,
+			String password) throws ClientException, ServerException {
+		PasswordCredential pc = new PasswordCredential();
+		pc.setServiceURI(contextService.toString());
+		pc.setUsername(username);
+		pc.setPassword(password);
+		checkError(run.security().credentials()
+				.postXmlAsOctetStream(pc, ClientResponse.class));
+	}
+
+	public void grantKeyCredential(URI contextService, java.io.File source,
+			String unlockPassword, String aliasEntry) throws IOException,
+			ClientException, ServerException {
+		KeyPairCredential kpc = new KeyPairCredential();
+		kpc.setServiceURI(contextService.toString());
+		try (InputStream in = new FileInputStream(source)) {
+			byte[] buffer = new byte[(int) source.length()];
+			IOUtils.read(in, buffer);
+			kpc.setCredentialBytes(buffer);
+		}
+		if (source.getName().endsWith(".p12"))
+			kpc.setFileType("PKCS12");
+		else
+			kpc.setFileType("JKS");
+		kpc.setCredentialName(aliasEntry);
+		kpc.setUnlockPassword(unlockPassword);
+		checkError(run.security().credentials()
+				.postXmlAsOctetStream(kpc, ClientResponse.class));
+	}
+}
\ No newline at end of file
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/Status.java b/server-client/src/main/java/uk/org/taverna/server/client/Status.java
new file mode 100644
index 0000000..9c375ad
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/Status.java
@@ -0,0 +1,36 @@
+package uk.org.taverna.server.client;
+
+/**
+ * States of a workflow run. They are {@link #Initialized Initialized},
+ * {@link #Operating Operating}, {@link #Stopped Stopped}, and
+ * {@link #Finished Finished}. Conceptually, there is also a
+ * <tt>Destroyed</tt> state, but the workflow run does not exist (and hence
+ * can't have its state queried or set) in that case.
+ * 
+ * @author Donal Fellows
+ */
+public enum Status {
+	/**
+	 * The workflow run has been created, but is not yet running. The run
+	 * will need to be manually moved to {@link #Operating Operating} when
+	 * ready.
+	 */
+	Initialized,
+	/**
+	 * The workflow run is going, reading input, generating output, etc.
+	 * Will eventually either move automatically to {@link #Finished
+	 * Finished} or can be moved manually to {@link #Stopped Stopped} (where
+	 * supported).
+	 */
+	Operating,
+	/**
+	 * The workflow run is paused, and will need to be moved back to
+	 * {@link #Operating Operating} manually.
+	 */
+	Stopped,
+	/**
+	 * The workflow run has ceased; data files will continue to exist until
+	 * the run is destroyed (which may be manual or automatic).
+	 */
+	Finished
+}
\ No newline at end of file
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/TavernaServer.java b/server-client/src/main/java/uk/org/taverna/server/client/TavernaServer.java
new file mode 100644
index 0000000..7c0dcdd
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/TavernaServer.java
@@ -0,0 +1,128 @@
+package uk.org.taverna.server.client;
+
+import static java.nio.file.Files.readAllBytes;
+import static org.taverna.server.client.wadl.TavernaServer.createClient;
+import static org.taverna.server.client.wadl.TavernaServer.root;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.taverna.server.client.wadl.TavernaServer.Root;
+
+import uk.org.taverna.server.client.generic.Capability;
+import uk.org.taverna.server.client.generic.TavernaRun;
+import uk.org.taverna.server.client.generic.VersionedElement;
+
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.filter.HTTPBasicAuthFilter;
+
+public class TavernaServer extends Connected {
+	final Root root;
+	private final URI location;
+	private final boolean authenticated;
+
+	TavernaServer(URI serviceRoot) {
+		root = root(createClient(), location = serviceRoot);
+		authenticated = false;
+	}
+
+	TavernaServer(URI serviceRoot, String username, String password) {
+		Client client = createClient();
+		client.addFilter(new HTTPBasicAuthFilter(username, password));
+		authenticated = true;
+		root = root(client, location = serviceRoot);
+	}
+
+	TavernaServer(TavernaServer service, String username, String password) {
+		Client client = createClient();
+		client.addFilter(new HTTPBasicAuthFilter(username, password));
+		authenticated = true;
+		root = root(client, location = service.location);
+		getServerVersionInfo();
+	}
+
+	public TavernaServer upgradeToAuth(String username, String password) {
+		if (authenticated)
+			throw new IllegalStateException("may only upgrade an unauthenticated connection");
+		return new TavernaServer(this, username, password);
+	}
+
+	public List<Capability> getCapabilities() {
+		return root.policy().capabilities().getAsCapabilitiesXml()
+				.getCapability();
+	}
+
+	public int getRunLimit() {
+		return root.policy().runLimit().getAsTextPlain(Integer.class);
+	}
+
+	public int getOperatingLimit() {
+		return root.policy().operatingLimit().getAsTextPlain(Integer.class);
+	}
+
+	public List<String> getPermittedWorkflows() {
+		return root.policy().permittedWorkflows().getAsPermittedWorkflowsXml()
+				.getWorkflow();
+	}
+
+	public List<Run> getExistingRuns() {
+		List<Run> runs = new ArrayList<>();
+		for (TavernaRun run : root.runs().getAsRunListXml().getRun())
+			runs.add(new Run(this, run.getValue()));
+		return runs;
+	}
+
+	public VersionedElement getServerVersionInfo() {
+		return root.getAsServerDescriptionXml();
+	}
+
+	private Run response2run(ClientResponse response) throws ClientException, ServerException {
+		checkError(response);
+		if (response.getClientResponseStatus().getStatusCode() == 201) {
+			String[] path = response.getLocation().getPath().split("/");
+			return new Run(this, path[path.length - 1]);
+		}
+		return null;
+	}
+
+	public Run createWorkflowRun(byte[] t2flowBytes) throws ClientException, ServerException {
+		return response2run(root.runs().postVndTavernaT2flowXmlAsOctetStream(
+				t2flowBytes, ClientResponse.class));
+	}
+
+	public Run createWorkflowRun(File t2flowFile) throws IOException, ClientException, ServerException {
+		return createWorkflowRun(readAllBytes(t2flowFile.toPath()));
+	}
+
+	public Run createWorkflowRun(URI t2flowUri) throws ClientException, ServerException {
+		return response2run(root.runs().postTextUriListAsOctetStream(
+				t2flowUri.toString(), ClientResponse.class));
+	}
+
+
+	public static class ClientException extends Exception {
+		private static final long serialVersionUID = 1L;
+
+		ClientException(String msg, Throwable cause) {
+			super(msg, cause);
+		}
+	}
+	public static class AuthorizationException extends ClientException {
+		private static final long serialVersionUID = 1L;
+
+		AuthorizationException(String msg, Throwable cause) {
+			super(msg, cause);
+		}
+	}
+	static class ServerException extends Exception {
+		private static final long serialVersionUID = 1L;
+
+		ServerException(String msg, Throwable cause) {
+			super(msg, cause);
+		}
+	}
+}
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/TavernaServerConnectionFactory.java b/server-client/src/main/java/uk/org/taverna/server/client/TavernaServerConnectionFactory.java
new file mode 100644
index 0000000..b00b075
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/TavernaServerConnectionFactory.java
@@ -0,0 +1,23 @@
+package uk.org.taverna.server.client;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TavernaServerConnectionFactory {
+	private Map<URI, TavernaServer> cache = new HashMap<>();
+
+	public synchronized TavernaServer connectNoAuth(URI uri) {
+		TavernaServer conn = cache.get(uri);
+		if (conn == null)
+			cache.put(uri, conn = new TavernaServer(uri));
+		return conn;
+	}
+
+	public TavernaServer connectAuth(URI uri, String username, String password) {
+		TavernaServer conn = new TavernaServer(uri, username, password);
+		// Force a check of the credentials by getting the server version
+		conn.getServerVersionInfo();
+		return conn;
+	}
+}
diff --git a/server-client/src/main/java/uk/org/taverna/server/client/package-info.java b/server-client/src/main/java/uk/org/taverna/server/client/package-info.java
new file mode 100644
index 0000000..59e809d
--- /dev/null
+++ b/server-client/src/main/java/uk/org/taverna/server/client/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Implementation of a basic client for Taverna Server.
+ * @author Donal Fellows
+ */
+package uk.org.taverna.server.client;
\ No newline at end of file
diff --git a/server-client/src/main/wadl/tavserv.wadl b/server-client/src/main/wadl/tavserv.wadl
new file mode 100644
index 0000000..2ba8fbe
--- /dev/null
+++ b/server-client/src/main/wadl/tavserv.wadl
@@ -0,0 +1,591 @@
+<application xmlns="http://wadl.dev.java.net/2009/02"
+	xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:prefix1="http://ns.taverna.org.uk/2010/xml/server/rest/"
+	xmlns:prefix3="http://ns.taverna.org.uk/2010/xml/server/"
+	xmlns:prefix10="http://ns.taverna.org.uk/2010/port/" xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
+	xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" jxb:version="2.1">
+<grammars>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:admin="http://ns.taverna.org.uk/2010/xml/server/admin/" xmlns:feed="http://ns.taverna.org.uk/2010/xml/server/feed/" xmlns:port="http://ns.taverna.org.uk/2010/port/" xmlns:tns="http://ns.taverna.org.uk/2010/xml/server/rest/" xmlns:ts="http://ns.taverna.org.uk/2010/xml/server/" xmlns:ts-rest="http://ns.taverna.org.uk/2010/xml/server/rest/" xmlns:ts-soap="http://ns.taverna.org.uk/2010/xml/server/soap/" xmlns:xlink="http://www.w3.org/1999/xlink" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://ns.taverna.org.uk/2010/xml/server/rest/" jxb:version="2.1">
+    <xs:annotation>
+    	<xs:appinfo>
+			<jxb:schemaBindings>
+				<jxb:package name="uk.org.taverna.server.client.rest" />
+			</jxb:schemaBindings>
+    	</xs:appinfo>
+    </xs:annotation>
+    <xs:import namespace="http://ns.taverna.org.uk/2010/xml/server/"/>
+    <xs:import namespace="http://www.w3.org/1999/xlink"/>
+    <xs:element name="capabilities">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element maxOccurs="unbounded" minOccurs="0" name="capability" type="ts:Capability"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="credential" type="ts-rest:Credential"/>
+    <xs:element name="credentials" type="ts-rest:credentialList"/>
+    <xs:element name="directoryContents" type="ts-rest:DirectoryContents"/>
+    <xs:element name="enabledNotificationFabrics">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element maxOccurs="unbounded" minOccurs="0" name="notifier" type="xs:string"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="filesystemOperation" type="ts-rest:FilesystemCreationOperation"/>
+    <xs:element name="listenerDefinition" type="ts-rest:ListenerDefinition"/>
+    <xs:element name="listenerDescription" type="ts-rest:ListenerDescription"/>
+    <xs:element name="listeners">
+        <xs:complexType>
+            <xs:complexContent>
+                <xs:extension base="ts:VersionedElement">
+                    <xs:sequence>
+                        <xs:element maxOccurs="unbounded" minOccurs="0" name="listener" type="ts-rest:ListenerDescription"/>
+                    </xs:sequence>
+                </xs:extension>
+            </xs:complexContent>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="mkdir" type="ts-rest:MakeDirectory"/>
+    <xs:element name="permissionUpdate" type="ts-rest:permissionDescription"/>
+    <xs:element name="permissionsDescriptor" type="ts-rest:permissionsDescription"/>
+    <xs:element name="permittedListeners">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element maxOccurs="unbounded" minOccurs="0" name="type" type="xs:string"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="permittedWorkflows">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element maxOccurs="unbounded" minOccurs="0" name="workflow" type="xs:string"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="policyDescription">
+        <xs:complexType>
+            <xs:complexContent>
+                <xs:extension base="ts:VersionedElement">
+                    <xs:sequence>
+                        <xs:element minOccurs="0" name="runLimit" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="operatingLimit" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="permittedWorkflows" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="permittedListenerTypes" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="enabledNotificationFabrics" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="capabilities" type="ts:Location"/>
+                    </xs:sequence>
+                </xs:extension>
+            </xs:complexContent>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="properties">
+        <xs:complexType>
+            <xs:complexContent>
+                <xs:extension base="ts:VersionedElement">
+                    <xs:sequence>
+                        <xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="ts-rest:PropertyDescription"/>
+                    </xs:sequence>
+                </xs:extension>
+            </xs:complexContent>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="runDescription">
+        <xs:complexType>
+            <xs:complexContent>
+                <xs:extension base="ts:VersionedElement">
+                    <xs:sequence>
+                        <xs:element minOccurs="0" name="expiry">
+                            <xs:complexType>
+                                <xs:simpleContent>
+                                    <xs:extension base="xs:string">
+                                        <xs:attribute ref="xlink:href"/>
+                                    </xs:extension>
+                                </xs:simpleContent>
+                            </xs:complexType>
+                        </xs:element>
+                        <xs:element minOccurs="0" name="creationWorkflow" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="createTime" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="startTime" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="finishTime" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="status" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="workingDirectory" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="inputs" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="output" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="securityContext" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="listeners">
+                            <xs:complexType>
+                                <xs:complexContent>
+                                    <xs:extension base="ts:Location">
+                                        <xs:sequence>
+                                            <xs:element maxOccurs="unbounded" minOccurs="0" name="listener" nillable="true" type="ts:Location"/>
+                                        </xs:sequence>
+                                    </xs:extension>
+                                </xs:complexContent>
+                            </xs:complexType>
+                        </xs:element>
+                        <xs:element minOccurs="0" name="interaction" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="name" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="stdout" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="stderr" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="usage" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="log" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="run-bundle" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="generate-provenance" type="ts:Location"/>
+                    </xs:sequence>
+                    <xs:attribute ref="ts-rest:owner"/>
+                </xs:extension>
+            </xs:complexContent>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="runInput" type="ts-rest:InputDescription"/>
+    <xs:element name="runInputs" type="ts-rest:TavernaRunInputs"/>
+    <xs:element name="runList">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element maxOccurs="unbounded" minOccurs="0" name="run" type="ts:TavernaRun"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="securityDescriptor" type="ts-rest:SecurityDescriptor"/>
+    <xs:element name="serverDescription">
+        <xs:complexType>
+            <xs:complexContent>
+                <xs:extension base="ts:VersionedElement">
+                    <xs:sequence>
+                        <xs:element minOccurs="0" name="runs" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="policy" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="feed" type="ts:Location"/>
+                        <xs:element minOccurs="0" name="interactionFeed" type="ts:Location"/>
+                    </xs:sequence>
+                </xs:extension>
+            </xs:complexContent>
+        </xs:complexType>
+    </xs:element>
+    <xs:element name="trustedIdentities" type="ts-rest:trustList"/>
+    <xs:element name="upload" type="ts-rest:UploadFile"/>
+    <xs:element name="userPermission" type="ts-rest:linkedPermissionDescription"/>
+    <xs:complexType name="InputDescription">
+        <xs:complexContent>
+            <xs:extension base="ts:VersionedElement">
+                <xs:sequence>
+                    <xs:choice minOccurs="0">
+                        <xs:element name="file">
+                            <xs:complexType>
+                                <xs:simpleContent>
+                                    <xs:extension base="ts-rest:InputContents"/>
+                                </xs:simpleContent>
+                            </xs:complexType>
+                        </xs:element>
+                        <xs:element name="reference">
+                            <xs:complexType>
+                                <xs:simpleContent>
+                                    <xs:extension base="ts-rest:InputContents"/>
+                                </xs:simpleContent>
+                            </xs:complexType>
+                        </xs:element>
+                        <xs:element name="value">
+                            <xs:complexType>
+                                <xs:simpleContent>
+                                    <xs:extension base="ts-rest:InputContents"/>
+                                </xs:simpleContent>
+                            </xs:complexType>
+                        </xs:element>
+                    </xs:choice>
+                </xs:sequence>
+                <xs:attribute ref="ts-rest:name"/>
+                <xs:attribute ref="ts-rest:descriptorRef"/>
+                <xs:attribute ref="ts-rest:listDelimiter"/>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:simpleType name="InputContents">
+        <xs:restriction base="xs:string"/>
+    </xs:simpleType>
+    <xs:complexType name="ListenerDescription">
+        <xs:complexContent>
+            <xs:extension base="ts:VersionedElement">
+                <xs:sequence>
+                    <xs:element minOccurs="0" name="configuration" type="ts:Location"/>
+                    <xs:element minOccurs="0" name="properties">
+                        <xs:complexType>
+                            <xs:sequence>
+                                <xs:element maxOccurs="unbounded" minOccurs="0" name="property" type="ts-rest:PropertyDescription"/>
+                            </xs:sequence>
+                        </xs:complexType>
+                    </xs:element>
+                </xs:sequence>
+                <xs:attribute ref="xlink:href"/>
+                <xs:attribute ref="ts-rest:name"/>
+                <xs:attribute ref="ts-rest:type"/>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="PropertyDescription">
+        <xs:complexContent>
+            <xs:extension base="ts:Location">
+                <xs:sequence/>
+                <xs:attribute ref="ts-rest:name"/>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="TavernaRunInputs">
+        <xs:complexContent>
+            <xs:extension base="ts:VersionedElement">
+                <xs:sequence>
+                    <xs:element minOccurs="0" name="expected" type="ts:Location"/>
+                    <xs:element minOccurs="0" name="baclava" type="ts:Location"/>
+                    <xs:element maxOccurs="unbounded" minOccurs="0" name="input" nillable="true" type="ts:Location"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType final="extension restriction" name="Credential">
+        <xs:choice>
+            <xs:element ref="ts:keypair"/>
+            <xs:element ref="ts:userpass"/>
+        </xs:choice>
+    </xs:complexType>
+    <xs:complexType name="DirectoryContents">
+        <xs:sequence>
+            <xs:choice maxOccurs="unbounded" minOccurs="0">
+                <xs:element ref="ts:dir"/>
+                <xs:element ref="ts:file"/>
+            </xs:choice>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType name="FilesystemCreationOperation">
+        <xs:simpleContent>
+            <xs:extension base="xs:base64Binary">
+                <xs:attribute ref="ts-rest:name"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="MakeDirectory">
+        <xs:simpleContent>
+            <xs:extension base="ts-rest:FilesystemCreationOperation"/>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="UploadFile">
+        <xs:simpleContent>
+            <xs:extension base="ts-rest:FilesystemCreationOperation"/>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="permissionsDescription">
+        <xs:complexContent>
+            <xs:extension base="ts:VersionedElement">
+                <xs:sequence>
+                    <xs:element maxOccurs="unbounded" minOccurs="0" name="permission" type="ts-rest:linkedPermissionDescription"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="linkedPermissionDescription">
+        <xs:complexContent>
+            <xs:extension base="ts:Location">
+                <xs:sequence>
+                    <xs:element minOccurs="0" name="userName" type="xs:string"/>
+                    <xs:element minOccurs="0" name="permission" type="ts:Permission"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType final="extension restriction" name="trustList">
+        <xs:complexContent>
+            <xs:extension base="ts:VersionedElement">
+                <xs:sequence>
+                    <xs:element maxOccurs="unbounded" minOccurs="0" name="trust" type="ts:TrustDescriptor"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="permissionDescription">
+        <xs:sequence>
+            <xs:element minOccurs="0" name="userName" type="xs:string"/>
+            <xs:element minOccurs="0" name="permission" type="ts:Permission"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType final="extension restriction" name="SecurityDescriptor">
+        <xs:complexContent>
+            <xs:extension base="ts:VersionedElement">
+                <xs:sequence>
+                    <xs:element minOccurs="0" name="owner" type="xs:string"/>
+                    <xs:element minOccurs="0" name="permissions" type="ts:Location"/>
+                    <xs:element minOccurs="0" name="credentials" type="ts-rest:CredentialCollection"/>
+                    <xs:element minOccurs="0" name="trusts" type="ts-rest:TrustCollection"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType final="extension restriction" name="CredentialCollection">
+        <xs:sequence>
+            <xs:element maxOccurs="unbounded" minOccurs="0" ref="ts-rest:credential"/>
+        </xs:sequence>
+        <xs:attribute ref="xlink:href"/>
+    </xs:complexType>
+    <xs:complexType final="extension restriction" name="TrustCollection">
+        <xs:sequence>
+            <xs:element maxOccurs="unbounded" minOccurs="0" name="trust" type="ts:TrustDescriptor"/>
+        </xs:sequence>
+        <xs:attribute ref="xlink:href"/>
+    </xs:complexType>
+    <xs:complexType final="extension restriction" name="credentialList">
+        <xs:complexContent>
+            <xs:extension base="ts:VersionedElement">
+                <xs:sequence>
+                    <xs:element maxOccurs="unbounded" minOccurs="0" ref="ts-rest:credential"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="ListenerDefinition">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute ref="ts-rest:type"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:attribute name="descriptorRef" type="xs:anyURI"/>
+    <xs:attribute name="listDelimiter" type="xs:string"/>
+    <xs:attribute name="name" type="xs:string"/>
+    <xs:attribute name="owner" type="xs:string"/>
+    <xs:attribute name="type" type="xs:string"/>
+</xs:schema>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://www.w3.org/1999/xlink" attributeFormDefault="unqualified" elementFormDefault="unqualified" targetNamespace="http://www.w3.org/1999/xlink" jxb:version="2.1">
+    <xs:attribute name="href" type="xs:string"/>
+</xs:schema>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:port="http://ns.taverna.org.uk/2010/port/" xmlns:run="http://ns.taverna.org.uk/2010/run/" xmlns:tns="http://ns.taverna.org.uk/2010/port/" xmlns:xlink="http://www.w3.org/1999/xlink" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://ns.taverna.org.uk/2010/port/" jxb:version="2.1">
+    <xs:annotation>
+    	<xs:appinfo>
+			<jxb:schemaBindings>
+				<jxb:package name="uk.org.taverna.server.client.generic.port" />
+			</jxb:schemaBindings>
+    	</xs:appinfo>
+    </xs:annotation>
+    <xs:import namespace="http://www.w3.org/1999/xlink"/>
+    <xs:element name="inputDescription" type="port:inputDescription"/>
+    <xs:element name="workflowOutputs" type="port:outputDescription"/>
+    <xs:complexType name="outputDescription">
+        <xs:complexContent>
+            <xs:extension base="port:PortDescription">
+                <xs:sequence>
+                    <xs:element maxOccurs="unbounded" minOccurs="0" name="output" type="port:OutputPort"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType abstract="true" name="PortDescription">
+        <xs:sequence/>
+        <xs:attribute ref="port:workflowId"/>
+        <xs:attribute ref="port:workflowRun"/>
+        <xs:attribute ref="port:workflowRunId"/>
+    </xs:complexType>
+    <xs:complexType name="OutputPort">
+        <xs:complexContent>
+            <xs:extension base="port:Port">
+                <xs:choice>
+                    <xs:element name="value" type="port:LeafValue"/>
+                    <xs:element name="list" type="port:ListValue"/>
+                    <xs:element name="error" type="port:ErrorValue"/>
+                    <xs:element name="absent" type="port:AbsentValue"/>
+                </xs:choice>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="Port">
+        <xs:sequence/>
+        <xs:attribute ref="port:name" use="required"/>
+        <xs:attribute ref="port:depth"/>
+    </xs:complexType>
+    <xs:complexType name="LeafValue">
+        <xs:complexContent>
+            <xs:extension base="port:Value">
+                <xs:sequence/>
+                <xs:attribute ref="port:contentFile"/>
+                <xs:attribute ref="port:contentType"/>
+                <xs:attribute ref="port:contentByteLength"/>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType abstract="true" name="Value">
+        <xs:sequence/>
+        <xs:attribute ref="xlink:href"/>
+    </xs:complexType>
+    <xs:complexType name="ErrorValue">
+        <xs:complexContent>
+            <xs:extension base="port:Value">
+                <xs:sequence/>
+                <xs:attribute ref="port:depth"/>
+                <xs:attribute ref="port:errorFile"/>
+                <xs:attribute ref="port:errorByteLength"/>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="ListValue">
+        <xs:complexContent>
+            <xs:extension base="port:Value">
+                <xs:sequence>
+                    <xs:choice maxOccurs="unbounded" minOccurs="0">
+                        <xs:element name="value" type="port:LeafValue"/>
+                        <xs:element name="list" type="port:ListValue"/>
+                        <xs:element name="error" type="port:ErrorValue"/>
+                        <xs:element name="absent" type="port:AbsentValue"/>
+                    </xs:choice>
+                </xs:sequence>
+                <xs:attribute ref="port:length"/>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="AbsentValue">
+        <xs:complexContent>
+            <xs:extension base="port:Value">
+                <xs:sequence/>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="inputDescription">
+        <xs:complexContent>
+            <xs:extension base="port:PortDescription">
+                <xs:sequence>
+                    <xs:element maxOccurs="unbounded" minOccurs="0" name="input" type="port:InputPort"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="InputPort">
+        <xs:complexContent>
+            <xs:extension base="port:Port">
+                <xs:sequence/>
+                <xs:attribute ref="xlink:href"/>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:attribute name="contentByteLength" type="xs:long"/>
+    <xs:attribute name="contentFile" type="xs:string"/>
+    <xs:attribute name="contentType" type="xs:string"/>
+    <xs:attribute name="depth" type="xs:int"/>
+    <xs:attribute name="errorByteLength" type="xs:long"/>
+    <xs:attribute name="errorFile" type="xs:string"/>
+    <xs:attribute name="length" type="xs:int"/>
+    <xs:attribute name="name" type="xs:ID"/>
+    <xs:attribute name="workflowId" type="xs:string"/>
+    <xs:attribute name="workflowRun" type="xs:anyURI"/>
+    <xs:attribute name="workflowRunId" type="xs:string"/>
+</xs:schema>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:admin="http://ns.taverna.org.uk/2010/xml/server/admin/" xmlns:feed="http://ns.taverna.org.uk/2010/xml/server/feed/" xmlns:tns="http://ns.taverna.org.uk/2010/xml/server/" xmlns:ts="http://ns.taverna.org.uk/2010/xml/server/" xmlns:ts-rest="http://ns.taverna.org.uk/2010/xml/server/rest/" xmlns:ts-soap="http://ns.taverna.org.uk/2010/xml/server/soap/" xmlns:xlink="http://www.w3.org/1999/xlink" attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://ns.taverna.org.uk/2010/xml/server/" jxb:version="2.1">
+    <xs:annotation>
+    	<xs:appinfo>
+			<jxb:schemaBindings>
+				<jxb:package name="uk.org.taverna.server.client.generic" />
+			</jxb:schemaBindings>
+    	</xs:appinfo>
+    </xs:annotation>
+    <xs:import namespace="http://www.w3.org/1999/xlink"/>
+    <xs:element name="capability" type="ts:Capability"/>
+    <xs:element name="dir" type="ts:DirectoryReference"/>
+    <xs:element name="file" type="ts:FileReference"/>
+    <xs:element name="keypair" type="ts:KeyPairCredential"/>
+    <xs:element name="runReference" type="ts:TavernaRun"/>
+    <xs:element name="trustedIdentity" type="ts:TrustDescriptor"/>
+    <xs:element name="userpass" type="ts:PasswordCredential"/>
+    <xs:element name="workflow" type="ts:Workflow"/>
+    <xs:complexType abstract="true" name="VersionedElement">
+        <xs:sequence/>
+        <xs:attribute ref="ts:serverVersion"/>
+        <xs:attribute ref="ts:serverRevision"/>
+        <xs:attribute ref="ts:serverBuildTimestamp"/>
+    </xs:complexType>
+    <xs:complexType name="Location">
+        <xs:sequence/>
+        <xs:attribute ref="xlink:href"/>
+    </xs:complexType>
+    <xs:complexType name="Capability">
+        <xs:sequence/>
+        <xs:attribute ref="ts:capability"/>
+        <xs:attribute ref="ts:version"/>
+    </xs:complexType>
+    <xs:complexType name="KeyPairCredential">
+        <xs:complexContent>
+            <xs:extension base="ts:CredentialDescriptor">
+                <xs:sequence>
+                    <xs:element name="credentialName" type="xs:string"/>
+                    <xs:element minOccurs="0" name="credentialFile" type="xs:string"/>
+                    <xs:element minOccurs="0" name="fileType" type="xs:string"/>
+                    <xs:element minOccurs="0" name="unlockPassword" type="xs:string"/>
+                    <xs:element minOccurs="0" name="credentialBytes" type="xs:base64Binary"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType abstract="true" name="CredentialDescriptor">
+        <xs:sequence>
+            <xs:element minOccurs="0" name="serviceURI" type="xs:anyURI"/>
+        </xs:sequence>
+        <xs:attribute ref="xlink:href"/>
+    </xs:complexType>
+    <xs:complexType name="PasswordCredential">
+        <xs:complexContent>
+            <xs:extension base="ts:CredentialDescriptor">
+                <xs:sequence>
+                    <xs:element name="username" type="xs:string"/>
+                    <xs:element name="password" type="xs:string"/>
+                </xs:sequence>
+            </xs:extension>
+        </xs:complexContent>
+    </xs:complexType>
+    <xs:complexType name="DirectoryEntry">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute ref="xlink:href"/>
+                <xs:attribute ref="ts:name"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="DirectoryReference">
+        <xs:simpleContent>
+            <xs:extension base="ts:DirectoryEntry"/>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="FileReference">
+        <xs:simpleContent>
+            <xs:extension base="ts:DirectoryEntry"/>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="TavernaRun">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute ref="xlink:href"/>
+                <xs:attribute ref="ts:serverVersion"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+    <xs:complexType name="Workflow">
+        <xs:sequence>
+            <xs:any maxOccurs="unbounded" minOccurs="0" namespace="##other" processContents="lax"/>
+        </xs:sequence>
+    </xs:complexType>
+    <xs:complexType final="extension restriction" name="TrustDescriptor">
+        <xs:sequence>
+            <xs:element minOccurs="0" name="certificateFile" type="xs:string"/>
+            <xs:element minOccurs="0" name="fileType" type="xs:string"/>
+            <xs:element minOccurs="0" name="certificateBytes" type="xs:base64Binary"/>
+            <xs:element maxOccurs="unbounded" minOccurs="0" name="serverName" type="xs:string"/>
+        </xs:sequence>
+        <xs:attribute ref="xlink:href"/>
+    </xs:complexType>
+    <xs:simpleType name="Permission">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="none"/>
+            <xs:enumeration value="read"/>
+            <xs:enumeration value="update"/>
+            <xs:enumeration value="destroy"/>
+        </xs:restriction>
+    </xs:simpleType>
+    <xs:attribute name="capability" type="xs:anyURI"/>
+    <xs:attribute name="name" type="xs:string"/>
+    <xs:attribute name="serverBuildTimestamp" type="xs:string"/>
+    <xs:attribute name="serverRevision" type="xs:string"/>
+    <xs:attribute name="serverVersion" type="xs:string"/>
+    <xs:attribute name="version" type="xs:string"/>
+</xs:schema>
+</grammars><resources base="http://example.com/taverna/rest"><resource path="/"><method name="GET"><doc>Produces the description of the service.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:serverDescription"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the service.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><resource path="runs"><method name="GET"><doc>Produces a list of all runs visible to the user.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:runList"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the operations on the collection of runs.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="POST"><doc>Accepts (or not) a request to create a new run executing the given workflow.</doc><request><representation mediaType="application/vnd.taverna.t2flow+xml" element="prefix3:workflow"><doc>Accepts (or not) a request to create a new run executing the given workflow.</doc></representation><representation mediaType="application/xml" element="prefix3:workflow"><doc>Accepts (or not) a request to create a new run executing the given workflow.</doc></representation></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="POST"><doc>Accepts a URL to a workflow to download and run. The URL must be hosted on a publicly-accessible service.</doc><request><representation mediaType="text/uri-list"><doc>Accepts a URL to a workflow to download and run. The URL must be hosted on a publicly-accessible service.</doc></representation></request><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/policy"><method name="GET"><doc>Describe the parts of this policy.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:policyDescription"></representation><representation mediaType="application/json"></representation></response></method><resource path="/capabilities"><method name="GET"><doc>Gets a description of the capabilities supported by this installation of Taverna Server.</doc><response><representation mediaType="application/xml" element="prefix1:capabilities"></representation><representation mediaType="application/json"></representation></response></method></resource><resource path="/enabledNotificationFabrics"><method name="GET"><doc>Gets the list of supported, enabled notification fabrics. Each corresponds (approximately) to a protocol, e.g., email.</doc><response><representation mediaType="application/xml" element="prefix1:enabledNotificationFabrics"></representation><representation mediaType="application/json"></representation></response></method></resource><resource path="/operatingLimit"><method name="GET"><doc>Gets the maximum number of simultaneously operating runs that the user may have. Note that this is often a global limit; it does not represent a promise that a particular user may be able to have that many operating runs at once.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:int"><doc>Gets the maximum number of simultaneously operating runs that the user may have. Note that this is often a global limit; it does not represent a promise that a particular user may be able to have that many operating runs at once.</doc></param></representation></response></method></resource><resource path="/permittedListenerTypes"><method name="GET"><doc>Gets the list of permitted event listener types.</doc><response><representation mediaType="application/xml" element="prefix1:permittedListeners"></representation><representation mediaType="application/json"></representation></response></method></resource><resource path="/permittedWorkflows"><method name="GET"><doc>Gets the list of permitted workflows.</doc><response><representation mediaType="application/xml" element="prefix1:permittedWorkflows"></representation><representation mediaType="application/json"></representation></response></method></resource><resource path="/runLimit"><method name="GET"><doc>Gets the maximum number of simultaneous runs in any state that the user may create.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:int"><doc>Gets the maximum number of simultaneous runs in any state that the user may create.</doc></param></representation></response></method></resource></resource><resource path="/runs/{runName}"><doc>This represents how a Taverna Server workflow run looks to a RESTful API.</doc><param name="runName" style="template" type="xs:string"/><method name="DELETE"><doc>Deletes a workflow run.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="GET"><doc>Describes a workflow run.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:runDescription"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><resource path="/createTime"><method name="GET"><doc>Gives the time when the workflow run was first submitted to the server.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the time when the workflow run was first submitted to the server.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run create time.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/expiry"><method name="GET"><doc>Gives the time when the workflow run becomes eligible for automatic deletion.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the time when the workflow run becomes eligible for automatic deletion.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run expiry.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Sets the time when the workflow run becomes eligible for automatic deletion.</doc><request><representation mediaType="text/plain"><param name="request" style="plain" type="xs:string"><doc>Sets the time when the workflow run becomes eligible for automatic deletion.</doc></param></representation></request><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Sets the time when the workflow run becomes eligible for automatic deletion.</doc></param></representation></response></method></resource><resource path="/finishTime"><method name="GET"><doc>Gives the time when the workflow run was first detected as finished, or an empty string if it has not yet finished (including if it has never started).</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the time when the workflow run was first detected as finished, or an empty string if it has not yet finished (including if it has never started).</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run finish time.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/generate-provenance"><method name="GET"><doc>Whether to create the run bundle for the workflow run.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:boolean"><doc>Whether to create the run bundle for the workflow run.</doc></param></representation></response></method><method name="OPTIONS"><doc>Whether to create the run bundle for the workflow run.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Whether to create the run bundle for the workflow run.</doc><request><representation mediaType="text/plain"><param name="request" style="plain" type="xs:boolean"><doc>Whether to create the run bundle for the workflow run.</doc></param></representation></request><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:boolean"><doc>Whether to create the run bundle for the workflow run.</doc></param></representation></response></method></resource><resource path="/log"><method name="GET"><doc>Return the log for the workflow run.</doc><response><representation mediaType="text/plain"></representation></response></method><method name="OPTIONS"><doc>Return the log for the workflow run.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/name"><method name="GET"><doc>Gives the descriptive name of the workflow run.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the descriptive name of the workflow run.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of the operations on the run&apos;s descriptive name.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Set the descriptive name of the workflow run. Note that this value may be arbitrarily truncated by the implementation.</doc><request><representation mediaType="text/plain"><param name="request" style="plain" type="xs:string"><doc>Set the descriptive name of the workflow run. Note that this value may be arbitrarily truncated by the implementation.</doc></param></representation></request><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Set the descriptive name of the workflow run. Note that this value may be arbitrarily truncated by the implementation.</doc></param></representation></response></method></resource><resource path="/output"><method name="GET"><doc>Gives the Baclava file where output will be written; empty means use multiple simple files in the out directory.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the Baclava file where output will be written; empty means use multiple simple files in the out directory.</doc></param></representation></response></method><method name="GET"><doc>Gives a description of the outputs, as currently understood</doc><request></request><response><representation mediaType="application/xml" element="prefix10:workflowOutputs"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run output.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Sets the Baclava file where output will be written; empty means use multiple simple files in the out directory.</doc><request><representation mediaType="text/plain"><param name="request" style="plain" type="xs:string"><doc>Sets the Baclava file where output will be written; empty means use multiple simple files in the out directory.</doc></param></representation></request><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Sets the Baclava file where output will be written; empty means use multiple simple files in the out directory.</doc></param></representation></response></method></resource><resource path="/run-bundle"><method name="GET"><doc>Return the run bundle for the workflow run.</doc><response><representation mediaType="application/vnd.wf4ever.robundle+zip"></representation></response></method><method name="OPTIONS"><doc>Return the run bundle for the workflow run.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/startTime"><method name="GET"><doc>Gives the time when the workflow run was started, or an empty string if the run has not yet started.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the time when the workflow run was started, or an empty string if the run has not yet started.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run start time.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/status"><method name="GET"><doc>Gives the current status of the workflow run.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the current status of the workflow run.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run status.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Attempts to update the status of the workflow run.</doc><request><representation mediaType="text/plain"><param name="request" style="plain" type="xs:string"><doc>Attempts to update the status of the workflow run.</doc></param></representation></request><response><representation mediaType="text/plain"></representation></response></method></resource><resource path="/stderr"><method name="GET"><doc>Return the stderr for the workflow run.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Return the stderr for the workflow run.</doc></param></representation></response></method><method name="OPTIONS"><doc>Return the stderr for the workflow run.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/stdout"><method name="GET"><doc>Return the stdout for the workflow run.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Return the stdout for the workflow run.</doc></param></representation></response></method><method name="OPTIONS"><doc>Return the stdout for the workflow run.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/usage"><method name="GET"><doc>Return the usage record for the workflow run.</doc><response><representation mediaType="application/xml"></representation></response></method><method name="OPTIONS"><doc>Return the usage record for the workflow run.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/workflow"><method name="GET"><doc>Gives the workflow document used to create the workflow run.</doc><response><representation mediaType="application/vnd.taverna.t2flow+xml" element="prefix3:workflow"></representation><representation mediaType="application/xml" element="prefix3:workflow"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run workflow.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/input"><doc>This represents how a Taverna Server workflow run&apos;s inputs looks to a RESTful API.</doc><method name="GET"><doc>Describe the sub-URIs of this resource.</doc><response><representation mediaType="application/xml" element="prefix1:runInputs"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of one run&apos;s inputs&apos; operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><resource path="/baclava"><method name="GET"><doc>Gives the Baclava file describing the inputs, or empty if individual files are used.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the Baclava file describing the inputs, or empty if individual files are used.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of the inputs&apos; baclava operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Sets the Baclava file describing the inputs.</doc><request><representation mediaType="text/plain"><param name="request" style="plain" type="xs:string"><doc>Sets the Baclava file describing the inputs.</doc></param></representation></request><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Sets the Baclava file describing the inputs.</doc></param></representation></response></method></resource><resource path="/expected"><method name="GET"><doc>Describe the expected inputs of this workflow run.</doc><response><representation mediaType="application/xml" element="prefix10:inputDescription"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the expected inputs&apos; operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/input/{name}"><param name="name" style="template" type="xs:string"/><method name="GET"><doc>Gives a description of what is used to supply a particular input.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:runInput"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the one input&apos;s operations.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Sets the source for a particular input port.</doc><request><representation mediaType="application/xml" element="prefix1:runInput"><doc>Sets the source for a particular input port.</doc></representation><representation mediaType="application/json"><doc>Sets the source for a particular input port.</doc></representation></request><response><representation mediaType="application/xml" element="prefix1:runInput"></representation><representation mediaType="application/json"></representation></response></method></resource></resource><resource path="/interaction"><method name="GET"><doc>Get the feed document for this ATOM feed.</doc><response><representation mediaType="application/atom+xml"></representation></response></method><method name="OPTIONS"><doc>Describes what HTTP operations are supported on the feed.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="POST"><doc>Adds an entry to this ATOM feed.</doc><request><representation mediaType="application/atom+xml"><doc>Adds an entry to this ATOM feed.</doc></representation></request><response><representation mediaType="application/atom+xml"></representation></response></method><resource path="/{id}"><param name="id" style="template" type="xs:string"/><method name="DELETE"><doc>Deletes an entry from this ATOM feed.</doc><request></request><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Deletes an entry from this ATOM feed.</doc></param></representation></response></method><method name="GET"><doc>Get the entry with a particular ID within this ATOM feed.</doc><request></request><response><representation mediaType="application/atom+xml"></representation></response></method><method name="OPTIONS"><doc>Describes what HTTP operations are supported on an entry.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method></resource></resource><resource path="/listeners"><doc>This represents all the event listeners attached to a workflow run.</doc><method name="GET"><doc>Get the listeners installed in the workflow run.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:listeners"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run listeners&apos; operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="POST"><doc>Add a new event listener to the named workflow run.</doc><request><representation mediaType="application/xml" element="prefix1:listenerDefinition"><doc>Add a new event listener to the named workflow run.</doc></representation><representation mediaType="application/json"><doc>Add a new event listener to the named workflow run.</doc></representation></request><response><representation mediaType="application/octet-stream"></representation></response></method><resource path="/{name}"><doc>This represents a single event listener attached to a workflow run.</doc><param name="name" style="template" type="xs:string"/><method name="GET"><doc>Get the description of this listener.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:listenerDescription"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of one run listener&apos;s operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><resource path="/configuration"><method name="GET"><doc>Get the configuration for the given event listener that is attached to a workflow run.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Get the configuration for the given event listener that is attached to a workflow run.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of one run listener&apos;s configuration&apos;s operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/properties"><method name="GET"><doc>Get the list of properties supported by a given event listener attached to a workflow run.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:properties"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of one run listener&apos;s properties&apos; operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/properties/{propertyName}"><doc>This represents a single property attached of an event listener.</doc><param name="propertyName" style="template" type="xs:string"/><method name="GET"><doc>Get the value of the particular property of an event listener attached to a workflow run.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Get the value of the particular property of an event listener attached to a workflow run.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of one run listener&apos;s property&apos;s operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Set the value of the particular property of an event listener attached to a workflow run.</doc><request><representation mediaType="text/plain"><param name="request" style="plain" type="xs:string"><doc>Set the value of the particular property of an event listener attached to a workflow run.</doc></param></representation></request><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Set the value of the particular property of an event listener attached to a workflow run.</doc></param></representation></response></method></resource></resource></resource><resource path="/security"><doc>Manages the security of the workflow run. In general, only the owner of a run may access this resource.</doc><method name="GET"><doc>Gives a description of the security information supported by the workflow run.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:securityDescriptor"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run security.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><resource path="/credentials"><method name="DELETE"><doc>Deletes all credentials.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="GET"><doc>Gives a list of credentials supplied to this workflow run.</doc><response><representation mediaType="application/xml" element="prefix1:credentials"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run credentials&apos; operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="POST"><doc>Creates a new credential.</doc><request><representation mediaType="application/xml" element="prefix1:credential"><doc>Creates a new credential.</doc></representation><representation mediaType="application/json"><doc>Creates a new credential.</doc></representation></request><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/credentials/{id}"><param name="id" style="template" type="xs:string"/><method name="DELETE"><doc>Deletes a particular credential.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="GET"><doc>Describes a particular credential.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:credential"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of one run credential&apos;s operations.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Updates a particular credential.</doc><request><representation mediaType="application/xml" element="prefix1:credential"><doc>Updates a particular credential.</doc></representation><representation mediaType="application/json"><doc>Updates a particular credential.</doc></representation></request><response><representation mediaType="application/xml" element="prefix1:credential"></representation><representation mediaType="application/json"></representation></response></method></resource><resource path="/owner"><method name="GET"><doc>Gives the identity of who owns the workflow run.</doc><response><representation mediaType="text/plain"><param name="result" style="plain" type="xs:string"><doc>Gives the identity of who owns the workflow run.</doc></param></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run owner.</doc><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/permissions"><method name="GET"><doc>Gives a list of all non-default permissions associated with the enclosing workflow run. By default, nobody has any access at all except for the owner of the run.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:permissionsDescriptor"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run permissions&apos; operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="POST"><doc>Creates a new assignment of permissions to a particular user.</doc><request><representation mediaType="application/xml" element="prefix1:permissionUpdate"><doc>Creates a new assignment of permissions to a particular user.</doc></representation><representation mediaType="application/json"><doc>Creates a new assignment of permissions to a particular user.</doc></representation></request><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/permissions/{id}"><param name="id" style="template" type="xs:string"/><method name="DELETE"><doc>Deletes (by resetting to default) the permissions associated with a particular user.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="GET"><doc>Describes the permission granted to a particular user.</doc><request></request><response><representation mediaType="text/plain"></representation></response></method><method name="OPTIONS"><doc>Produces the description of one run permission&apos;s operations.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Updates the permissions granted to a particular user.</doc><request><representation mediaType="text/plain"><doc>Updates the permissions granted to a particular user.</doc></representation></request><response><representation mediaType="text/plain"></representation></response></method></resource><resource path="/trusts"><method name="DELETE"><doc>Deletes all trusted identities.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="GET"><doc>Gives a list of trusted identities supplied to this workflow run.</doc><response><representation mediaType="application/xml" element="prefix1:trustedIdentities"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the run trusted certificates&apos; operations.</doc><response><representation mediaType="application/octet-stream"></representation></response></method><method name="POST"><doc>Adds a new trusted identity.</doc><request><representation mediaType="application/xml" element="prefix3:trustedIdentity"><doc>Adds a new trusted identity.</doc></representation><representation mediaType="application/json"><doc>Adds a new trusted identity.</doc></representation></request><response><representation mediaType="application/octet-stream"></representation></response></method></resource><resource path="/trusts/{id}"><param name="id" style="template" type="xs:string"/><method name="DELETE"><doc>Deletes a particular trusted identity.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="GET"><doc>Describes a particular trusted identity.</doc><request></request><response><representation mediaType="application/xml" element="prefix3:trustedIdentity"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of one run trusted certificate&apos;s operations.</doc><request></request><response><representation mediaType="application/octet-stream"></representation></response></method><method name="PUT"><doc>Updates a particular trusted identity.</doc><request><representation mediaType="application/xml" element="prefix3:trustedIdentity"><doc>Updates a particular trusted identity.</doc></representation><representation mediaType="application/json"><doc>Updates a particular trusted identity.</doc></representation></request><response><representation mediaType="application/xml" element="prefix3:trustedIdentity"></representation><representation mediaType="application/json"></representation></response></method></resource></resource><resource path="/wd"><doc>Representation of how a workflow run&apos;s working directory tree looks.</doc><method name="GET"><doc>Describes the working directory of the workflow run.</doc><request></request><response><representation mediaType="application/xml" element="prefix1:directoryContents"></representation><representation mediaType="application/json"></representation></response></method><resource path="/{path:(.*)}"><param name="path" style="template" repeating="true"/><method name="POST"><doc>Creates or updates a file in a particular location beneath the working directory of the workflow run with the contents of a publicly readable URL.</doc><request><representation mediaType="text/uri-list"><doc>Creates or updates a file in a particular location beneath the working directory of the workflow run with the contents of a publicly readable URL.</doc></representation></request><response><representation mediaType="application/xml"></representation><representation mediaType="application/json"></representation></response></method><method name="PUT"><doc>Creates or updates a file in a particular location beneath the working directory of the workflow run.</doc><request><representation mediaType="application/octet-stream"><doc>Creates or updates a file in a particular location beneath the working directory of the workflow run.</doc></representation><representation mediaType="*/*"><doc>Creates or updates a file in a particular location beneath the working directory of the workflow run.</doc></representation></request><response><representation mediaType="application/xml"></representation><representation mediaType="application/json"></representation></response></method></resource><resource path="/{path:.*}"><param name="path" style="template" repeating="true"/><method name="DELETE"><doc>Deletes a file or directory that is in or below the working directory of a workflow run.</doc><request></request><response><representation mediaType="application/xml"></representation><representation mediaType="application/json"></representation></response></method><method name="OPTIONS"><doc>Produces the description of the files/directories&apos; baclava operations.</doc><request></request><response><representation mediaType="application/xml"></representation><representation mediaType="application/json"></representation></response></method><method name="POST"><doc>Creates a directory in the filesystem beneath the working directory of the workflow run, or creates or updates a file&apos;s contents, where that file is in or below the working directory of a workflow run.</doc><request><representation mediaType="application/xml" element="prefix1:filesystemOperation"><doc>Creates a directory in the filesystem beneath the working directory of the workflow run, or creates or updates a file&apos;s contents, where that file is in or below the working directory of a workflow run.</doc></representation><representation mediaType="application/json"><doc>Creates a directory in the filesystem beneath the working directory of the workflow run, or creates or updates a file&apos;s contents, where that file is in or below the working directory of a workflow run.</doc></representation></request><response><representation mediaType="application/xml"></representation><representation mediaType="application/json"></representation></response></method></resource><resource path="/{path:.+}"><param name="path" style="template" repeating="true"/><method name="GET"><doc>Gives a description of the named entity in or beneath the working directory of the workflow run (either a Directory or File).</doc><request></request><response><representation mediaType="application/xml"></representation><representation mediaType="application/json"></representation><representation mediaType="application/octet-stream"></representation><representation mediaType="application/zip"></representation><representation mediaType="*/*"></representation></response></method></resource></resource></resource></resource></resources></application>
\ No newline at end of file
diff --git a/server-distribution/pom.xml b/server-distribution/pom.xml
new file mode 100644
index 0000000..9eab5d3
--- /dev/null
+++ b/server-distribution/pom.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>server</artifactId>
+		<groupId>uk.org.taverna.server</groupId>
+		<version>3.0-SNAPSHOT</version>
+		<relativePath>..</relativePath>
+	</parent>
+	<artifactId>server-distribution</artifactId>
+	<name>Distribution Packaging</name>
+	<packaging>pom</packaging>
+	<scm>
+		<url>${scmBrowseRoot}/server-distribution</url>
+	</scm>
+
+	<dependencies>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-webapp</artifactId>
+			<version>${project.parent.version}</version>
+			<type>war</type>
+		</dependency>
+	</dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<configuration>
+					<finalName>TavernaServer-${project.parent.version}</finalName>
+					<descriptors>
+						<descriptor>src/assemble/dist.xml</descriptor>
+					</descriptors>
+				</configuration>
+				<executions>
+					<execution>
+						<id>make-t2server-distribution</id>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-source-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>default</id>
+						<phase>none</phase>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>default</id>
+						<phase>none</phase>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+	<reporting>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-project-info-reports-plugin</artifactId>
+				<version>2.4</version>
+				<reportSets>
+					<reportSet>
+						<reports>
+							<!-- Only generate the index, nothing else. -->
+							<report>index</report>
+						</reports>
+					</reportSet>
+				</reportSets>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-javadoc-plugin</artifactId>
+				<version>2.8</version>
+				<reportSets>
+					<reportSet />
+				</reportSets>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-changelog-plugin</artifactId>
+				<version>2.2</version>
+				<reportSets>
+					<reportSet />
+				</reportSets>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-changes-plugin</artifactId>
+				<version>2.6</version>
+				<reportSets>
+					<reportSet />
+				</reportSets>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-checkstyle-plugin</artifactId>
+				<version>2.7</version>
+				<reportSets>
+					<reportSet />
+				</reportSets>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-pmd-plugin</artifactId>
+				<version>2.5</version>
+				<reportSets>
+					<reportSet />
+				</reportSets>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-report-plugin</artifactId>
+				<version>2.9</version>
+				<reportSets>
+					<reportSet />
+				</reportSets>
+			</plugin>
+		</plugins>
+	</reporting>
+</project>
diff --git a/server-distribution/src/assemble/dist.xml b/server-distribution/src/assemble/dist.xml
new file mode 100644
index 0000000..a77883c
--- /dev/null
+++ b/server-distribution/src/assemble/dist.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<assembly
+	xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+	<id>distribution</id>
+	<formats>
+		<format>zip</format>
+	</formats>
+<!-- 
+	<dependencySets>
+		<dependencySet>
+			<useTransitiveDependencies>false</useTransitiveDependencies>
+			<useProjectArtifact>false</useProjectArtifact>
+			<unpack>false</unpack>
+			<scope>runtime</scope>
+			<fileMode>0644</fileMode>
+		</dependencySet>
+	</dependencySets>
+	<fileSets>
+		<fileSet>
+			<includes>
+				<include>*.html</include>
+				<include>*.xml</include>
+				<include>*.txt</include>
+				<include>*.pdf</include>
+			</includes>
+			<excludes>
+				<exclude>pom.xml</exclude>
+			</excludes>
+		</fileSet>
+	</fileSets>
+-->
+	<fileSets>
+		<fileSet>
+			<directory>${project.parent.basedir}</directory>
+			<includes>
+				<include>*.xml</include>
+				<include>*.txt</include>
+				<include>*.pdf</include>
+			</includes>
+			<excludes>
+				<exclude>pom.xml</exclude>
+			</excludes>
+			<fileMode>0644</fileMode>
+			<outputDirectory>.</outputDirectory>
+		</fileSet>
+		<fileSet>
+			<directory>${project.parent.basedir}/server-webapp/target</directory>
+			<includes>
+				<include>*.war</include>
+			</includes>
+			<outputDirectory>.</outputDirectory>
+			<fileMode>0644</fileMode>
+		</fileSet>
+	</fileSets>
+<!-- 
+	<moduleSets>
+		<moduleSet>
+			<includes>
+				<include>uk.org.taverna.server:server</include>
+			</includes>
+			<sources>
+				<fileSets>
+					<fileSet>
+						<includes>
+							<include>*.html</include>
+							<include>*.txt</include>
+						</includes>
+					</fileSet>
+				</fileSets>
+			</sources>
+		</moduleSet>
+		<moduleSet>
+			<includes>
+				<include>uk.org.taverna.server:server-webapp</include>
+			</includes>
+			<binaries />
+		</moduleSet>
+	</moduleSets>
+-->
+</assembly>
\ No newline at end of file
diff --git a/server-execution-delegate/pom.xml b/server-execution-delegate/pom.xml
new file mode 100644
index 0000000..6acd601
--- /dev/null
+++ b/server-execution-delegate/pom.xml
@@ -0,0 +1,49 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>uk.org.taverna</groupId>
+		<artifactId>platform</artifactId>
+		<version>0.1.3-SNAPSHOT</version>
+	</parent>
+	<groupId>uk.org.taverna.server</groupId>
+	<artifactId>server-execution-delegate</artifactId>
+	<version>3.0-SNAPSHOT</version>
+	<packaging>bundle</packaging>
+	<repositories>
+		<repository>
+			<releases />
+			<snapshots>
+				<enabled>false</enabled>
+			</snapshots>
+			<id>mygrid-repository</id>
+			<name>myGrid Repository</name>
+			<url>http://www.mygrid.org.uk/maven/repository
+			</url>
+		</repository>
+		<repository>
+			<releases>
+				<enabled>false</enabled>
+			</releases>
+			<snapshots />
+			<id>mygrid-snapshot-repository</id>
+			<name>myGrid Snapshot Repository</name>
+			<url>http://www.mygrid.org.uk/maven/snapshot-repository</url>
+		</repository>
+	</repositories>
+	<dependencies>
+		<dependency>
+			<groupId>uk.org.taverna.platform</groupId>
+			<artifactId>taverna-execution-api</artifactId>
+			<version>0.1.3-SNAPSHOT</version>
+			<type>bundle</type>
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<name>Reporting Probe</name>
+	<inceptionYear>2012</inceptionYear>
+</project>
\ No newline at end of file
diff --git a/server-execution-delegate/src/main/java/org/taverna/server/execution_delegate/ExecutionDelegate.java b/server-execution-delegate/src/main/java/org/taverna/server/execution_delegate/ExecutionDelegate.java
new file mode 100644
index 0000000..7ea9998
--- /dev/null
+++ b/server-execution-delegate/src/main/java/org/taverna/server/execution_delegate/ExecutionDelegate.java
@@ -0,0 +1,172 @@
+package org.taverna.server.execution_delegate;
+
+import java.net.URI;
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Map;
+
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import org.taverna.server.execution_delegate.RemoteExecution.ProcessorReportDocument.Property;
+
+import uk.org.taverna.platform.data.api.Data;
+import uk.org.taverna.platform.data.api.DataLocation;
+import uk.org.taverna.platform.execution.api.Execution;
+import uk.org.taverna.platform.report.ActivityReport;
+import uk.org.taverna.platform.report.ProcessorReport;
+import uk.org.taverna.platform.report.StatusReport;
+import uk.org.taverna.platform.report.WorkflowReport;
+import uk.org.taverna.scufl2.api.common.AbstractNamed;
+
+public class ExecutionDelegate extends UnicastRemoteObject implements
+		RemoteExecution {
+	private Execution delegated;
+	private DatatypeFactory dtf;
+
+	public ExecutionDelegate(Execution execution) throws RemoteException,
+			DatatypeConfigurationException {
+		super();
+		delegated = execution;
+		dtf = DatatypeFactory.newInstance();
+	}
+
+	@Override
+	public String getID() {
+		return delegated.getID();
+	}
+
+	@Override
+	public void delete() {
+		delegated.delete();
+	}
+
+	@Override
+	public void start() {
+		delegated.start();
+	}
+
+	@Override
+	public void pause() {
+		delegated.pause();
+	}
+
+	@Override
+	public void resume() {
+		delegated.resume();
+	}
+
+	@Override
+	public void cancel() {
+		delegated.cancel();
+	}
+
+	private XMLGregorianCalendar date(Date d) {
+		if (d == null)
+			return null;
+		GregorianCalendar c = new GregorianCalendar();
+		c.setTime(d);
+		return dtf.newXMLGregorianCalendar(c);
+	}
+
+	private <R extends ReportDocument, T extends AbstractNamed> R init(
+			R snapshot, StatusReport<T, ?, ?> report) {
+		snapshot.created = date(report.getCreatedDate());
+		snapshot.completed = date(report.getCompletedDate());
+		snapshot.cancelled = date(report.getCancelledDate());
+		snapshot.failed = date(report.getFailedDate());
+		snapshot.started = date(report.getStartedDate());
+		for (Date d : report.getPausedDates())
+			snapshot.paused.add(date(d));
+		for (Date d : report.getResumedDates())
+			snapshot.resumed.add(date(d));
+		snapshot.subject = report.getSubject().getName();
+		snapshot.state = report.getState().toString();
+		return snapshot;
+	}
+
+	private WorkflowReportDocument getReport(WorkflowReport report) {
+		WorkflowReportDocument snapshot = init(new WorkflowReportDocument(),
+				report);
+		for (ProcessorReport pr : report.getChildReports())
+			snapshot.processor.add(getReport(pr));
+		initMap(snapshot.inputs, report.getInputs());
+		initMap(snapshot.outputs, report.getOutputs());
+		return snapshot;
+	}
+
+	private void initMap(ArrayList<PortMapping> snapshot, Map<String, ?> report) {
+		for (String port : report.keySet())
+			snapshot.add(data(port, report.get(port)));
+	}
+
+	private static final String DEFAULT_PROPERTY_STRING = "";
+
+	private PortMapping data(String name, Object o) {
+		String loc;
+		if (o instanceof DataLocation) {
+			DataLocation d = (DataLocation) o;
+			loc = d.getDataServiceURI() + "#" + d.getDataID();
+		} else {
+			Data d = (Data) o;
+			loc = null;
+			if (d.hasReferences()) {
+				try {
+					loc = d.getReferences().iterator().next().getURI()
+							.toString();
+				} catch (Exception e) {
+				}
+			}
+			if (loc == null) {
+				DataLocation dl = d.getDataService().getDataLocation(d);
+				loc = dl.getDataServiceURI() + "#" + dl.getDataID();
+			}
+		}
+		return new PortMapping(name, URI.create(loc));
+	}
+
+	private ProcessorReportDocument getReport(ProcessorReport report) {
+		ProcessorReportDocument snapshot = init(new ProcessorReportDocument(),
+				report);
+		for (ActivityReport pr : report.getChildReports())
+			snapshot.activity.add(getReport(pr));
+		snapshot.jobsQueued = report.getJobsStarted();
+		snapshot.jobsStarted = report.getJobsStarted();
+		snapshot.jobsCompleted = report.getJobsCompleted();
+		snapshot.jobsErrored = report.getJobsCompletedWithErrors();
+		for (String key : report.getPropertyKeys()) {
+			Object value = report.getProperty(key);
+			if (value instanceof String || value instanceof Number)
+				snapshot.properties.add(new Property(key, value.toString()));
+			else
+				snapshot.properties.add(new Property(key,
+						DEFAULT_PROPERTY_STRING));
+		}
+		return snapshot;
+	}
+
+	private ActivityReportDocument getReport(ActivityReport report) {
+		ActivityReportDocument snapshot = init(new ActivityReportDocument(),
+				report);
+		for (WorkflowReport pr : report.getChildReports())
+			snapshot.workflow.add(getReport(pr));
+		initMap(snapshot.inputs, report.getInputs());
+		initMap(snapshot.outputs, report.getOutputs());
+		return snapshot;
+	}
+
+	@Override
+	public WorkflowReportDocument getWorkflowReport() {
+		return getReport(delegated.getWorkflowReport());
+	}
+}
+
+// ExecutionDelegate.java:[96,2]
+// initMap(java.util.ArrayList<org.taverna.server.execution_delegate.RemoteExecution.PortMapping>,java.util.Map<java.lang.String,uk.org.taverna.platform.data.api.Data>)
+// in org.taverna.server.execution_delegate.ExecutionDelegate cannot be applied
+// to
+// (java.util.ArrayList<org.taverna.server.execution_delegate.RemoteExecution.PortMapping>,java.util.Map<java.lang.String,uk.org.taverna.platform.data.api.DataLocation>)
diff --git a/server-execution-delegate/src/main/java/org/taverna/server/execution_delegate/RemoteExecution.java b/server-execution-delegate/src/main/java/org/taverna/server/execution_delegate/RemoteExecution.java
new file mode 100644
index 0000000..b8510df
--- /dev/null
+++ b/server-execution-delegate/src/main/java/org/taverna/server/execution_delegate/RemoteExecution.java
@@ -0,0 +1,188 @@
+package org.taverna.server.execution_delegate;
+
+import static javax.xml.bind.annotation.XmlAccessType.NONE;
+
+import java.net.URI;
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+/**
+ * Interface for a single execution of a Taverna workflow in a remote process.
+ * 
+ * @author David Withers
+ */
+public interface RemoteExecution extends Remote {
+
+	/**
+	 * Returns the identifier for this execution.
+	 * 
+	 * @return the identifier for this execution
+	 */
+	String getID() throws RemoteException;
+
+	/**
+	 * Returns the <code>WorkflowReport</code> for the execution.
+	 * 
+	 * @return the <code>WorkflowReport</code> for the execution
+	 */
+	WorkflowReportDocument getWorkflowReport() throws RemoteException;
+
+	/**
+	 * Deletes the execution.
+	 */
+	void delete() throws RemoteException;
+
+	/**
+	 * Starts the execution.
+	 */
+	void start() throws RemoteException;
+
+	/**
+	 * Pauses the execution.
+	 */
+	void pause() throws RemoteException;
+
+	/**
+	 * Resumes a paused execution.
+	 */
+	void resume() throws RemoteException;
+
+	/**
+	 * Cancels the execution.
+	 */
+	void cancel() throws RemoteException;
+
+	@XmlType(name = "Report", propOrder = { "state", "created", "started",
+			"completed", "failed", "cancelled", "paused", "resumed" })
+	@XmlSeeAlso({ WorkflowReportDocument.class, ProcessorReportDocument.class,
+			ActivityReportDocument.class })
+	@XmlAccessorType(NONE)
+	public static abstract class ReportDocument {
+		@XmlAttribute
+		public String subject;
+		@XmlElement
+		public String state;
+		@XmlElement(required = true)
+		@XmlSchemaType(name = "dateTime")
+		public XMLGregorianCalendar created;
+		@XmlElement
+		@XmlSchemaType(name = "dateTime")
+		public XMLGregorianCalendar started;
+		@XmlElement
+		@XmlSchemaType(name = "dateTime")
+		public XMLGregorianCalendar completed;
+		@XmlElement
+		@XmlSchemaType(name = "dateTime")
+		public XMLGregorianCalendar failed;
+		@XmlElement
+		@XmlSchemaType(name = "dateTime")
+		public XMLGregorianCalendar cancelled;
+		@XmlElement
+		@XmlSchemaType(name = "dateTime")
+		public List<XMLGregorianCalendar> paused;
+		@XmlElement
+		@XmlSchemaType(name = "dateTime")
+		public List<XMLGregorianCalendar> resumed;
+	}
+
+	@XmlType(name = "WorkflowReport", propOrder = { "processor", "inputs",
+			"outputs" })
+	@XmlRootElement(name = "workflowReport")
+	@XmlAccessorType(NONE)
+	public static class WorkflowReportDocument extends ReportDocument {
+		@XmlElement(name = "processor")
+		public ArrayList<ProcessorReportDocument> processor = new ArrayList<ProcessorReportDocument>();
+		@XmlElement(name = "input")
+		public ArrayList<PortMapping> inputs = new ArrayList<PortMapping>();
+		@XmlElement(name = "output")
+		public ArrayList<PortMapping> outputs = new ArrayList<PortMapping>();
+	}
+
+	@XmlType(name = "ProcessorReport", propOrder = { "jobsQueued",
+			"jobsStarted", "jobsCompleted", "jobsErrored", "properties",
+			"activity" })
+	@XmlAccessorType(NONE)
+	public static class ProcessorReportDocument extends ReportDocument {
+		@XmlElement(name = "jobsQueued", required = true)
+		public int jobsQueued;
+		@XmlElement(name = "jobsStarted", required = true)
+		public int jobsStarted;
+		@XmlElement(name = "jobsCompleted", required = true)
+		public int jobsCompleted;
+		@XmlElement(name = "jobsErrored", required = true)
+		public int jobsErrored;
+		@XmlElement(name = "property")
+		@XmlElementWrapper(name = "properties")
+		public ArrayList<Property> properties = new ArrayList<Property>();
+		@XmlElement(name = "activity")
+		public ArrayList<ActivityReportDocument> activity = new ArrayList<ActivityReportDocument>();
+
+		@XmlType(name = "ProcessorProperty")
+		@XmlAccessorType(NONE)
+		public static class Property {
+			@XmlAttribute(name = "key", required = true)
+			String key;
+			@XmlValue
+			String value;
+
+			public Property() {
+			}
+
+			public String getKey() {
+				return key;
+			}
+
+			public String getValue() {
+				return value;
+			}
+
+			Property(String key, String value) {
+				this.key = key;
+				this.value = value;
+			}
+		}
+	}
+
+	@XmlType(name = "ActivityReport", propOrder = { "workflow", "inputs",
+			"outputs" })
+	@XmlAccessorType(NONE)
+	public static class ActivityReportDocument extends ReportDocument {
+		@XmlElement(name = "workflow")
+		public ArrayList<WorkflowReportDocument> workflow = new ArrayList<WorkflowReportDocument>();
+		@XmlElement(name = "input")
+		public ArrayList<PortMapping> inputs = new ArrayList<PortMapping>();
+		@XmlElement(name = "output")
+		public ArrayList<PortMapping> outputs = new ArrayList<PortMapping>();
+	}
+
+	@XmlType(name = "PortMapping")
+	@XmlAccessorType(NONE)
+	public static class PortMapping {
+		public PortMapping() {
+		}
+
+		PortMapping(String port, URI data) {
+			this.name = port;
+			this.reference = data;
+		}
+
+		@XmlAttribute(name = "name")
+		public String name;
+		@XmlAttribute(name = "ref")
+		@XmlSchemaType(name = "anyURI")
+		public URI reference;
+	}
+}
diff --git a/server-execution-delegate/src/test/java/SerializationTest.java b/server-execution-delegate/src/test/java/SerializationTest.java
new file mode 100644
index 0000000..c4bf0b9
--- /dev/null
+++ b/server-execution-delegate/src/test/java/SerializationTest.java
@@ -0,0 +1,54 @@
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.SchemaOutputResolver;
+import javax.xml.transform.Result;
+import javax.xml.transform.stream.StreamResult;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.taverna.server.execution_delegate.RemoteExecution.WorkflowReportDocument;
+
+public class SerializationTest {
+	private static final boolean PRINT = true;
+	SchemaOutputResolver sink;
+	StringWriter schema;
+
+	String schema() {
+		return schema.toString();
+	}
+
+	@Before
+	public void init() {
+		schema = new StringWriter();
+		sink = new SchemaOutputResolver() {
+			@Override
+			public Result createOutput(String namespaceUri,
+					String suggestedFileName) throws IOException {
+				StreamResult sr = new StreamResult(schema);
+				sr.setSystemId("/dev/null");
+				return sr;
+			}
+		};
+		assertEquals("", schema());
+	}
+
+	@Test
+	public void testSchemaGeneration() throws JAXBException, IOException {
+		JAXBContext.newInstance(WorkflowReportDocument.class).generateSchema(
+				sink);
+		assertFalse("generated schema must be non-empty", schema().isEmpty());
+		assertTrue(
+				"generated schema must define workflowReport element",
+				schema().contains(
+						"<xs:element name=\"workflowReport\" type=\"WorkflowReport\"/>\n"));
+		if (PRINT)
+			System.out.print(schema());
+	}
+}
diff --git a/server-port-description/.gitignore b/server-port-description/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/server-port-description/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/server-port-description/pom.xml b/server-port-description/pom.xml
new file mode 100644
index 0000000..44a9d50
--- /dev/null
+++ b/server-port-description/pom.xml
@@ -0,0 +1,50 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>server</artifactId>
+		<groupId>uk.org.taverna.server</groupId>
+		<version>3.0-SNAPSHOT</version>
+	</parent>
+	<artifactId>server-port-description</artifactId>
+	<name>Workflow Port Descriptor Types</name>
+	<description>The structural types used to describe ports on workflows, as exported by Taverna Server.</description>
+	<scm>
+		<url>${scmBrowseRoot}/server-port-description</url>
+	</scm>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+	</properties>
+	<dependencies>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<encoding>US-ASCII</encoding>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+		</plugins>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-eclipse-plugin</artifactId>
+					<configuration>
+						<projectNameTemplate>[artifactId]-[version]</projectNameTemplate>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
+</project>
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/AbsentValue.java b/server-port-description/src/main/java/org/taverna/server/port_description/AbsentValue.java
new file mode 100644
index 0000000..2ea392f
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/AbsentValue.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "AbsentValue")
+public class AbsentValue extends AbstractValue {
+
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/AbstractPort.java b/server-port-description/src/main/java/org/taverna/server/port_description/AbstractPort.java
new file mode 100644
index 0000000..e57c4f9
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/AbstractPort.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlID;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import org.taverna.server.port_description.utils.IntAdapter;
+
+@XmlType(name = "Port")
+public class AbstractPort {
+	@XmlID
+	@XmlAttribute(required = true)
+	public String name;
+
+	@XmlAttribute
+	@XmlSchemaType(name = "int")
+	@XmlJavaTypeAdapter(IntAdapter.class)
+	public Integer depth;
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/AbstractPortDescription.java b/server-port-description/src/main/java/org/taverna/server/port_description/AbstractPortDescription.java
new file mode 100644
index 0000000..7c3fe9a
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/AbstractPortDescription.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import java.net.URI;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "PortDescription")
+public abstract class AbstractPortDescription {
+	@XmlAttribute
+	public String workflowId;
+	@XmlAttribute
+	@XmlSchemaType(name = "anyURI")
+	public URI workflowRun;
+	@XmlAttribute
+	public String workflowRunId;
+
+	public void fillInBaseData(String docId, String runId, URI runUrl) {
+		this.workflowId = docId;
+		this.workflowRun = runUrl;
+		this.workflowRunId = runId;
+	}
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/AbstractValue.java b/server-port-description/src/main/java/org/taverna/server/port_description/AbstractValue.java
new file mode 100644
index 0000000..47232c2
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/AbstractValue.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import static org.taverna.server.port_description.Namespaces.XLINK;
+
+import java.net.URI;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "Value")
+@XmlSeeAlso( { ErrorValue.class, LeafValue.class, ListValue.class, AbsentValue.class })
+public abstract class AbstractValue {
+	@XmlAttribute(namespace = XLINK)
+	@XmlSchemaType(name = "anyURI")
+	public URI href;
+
+	public void setAddress(URI uri, String localAddress) {
+		if (uri.getPath().endsWith("/")) {
+			href = URI.create(uri + "wd/out/" + localAddress);
+		} else {
+			href = URI.create(uri + "/wd/out/" + localAddress);
+		}
+		//about = "out/" + localAddress;
+	}
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/ErrorValue.java b/server-port-description/src/main/java/org/taverna/server/port_description/ErrorValue.java
new file mode 100644
index 0000000..0bb07b4
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/ErrorValue.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import org.taverna.server.port_description.utils.IntAdapter;
+
+@XmlType(name = "ErrorValue")
+public class ErrorValue extends AbstractValue {
+	@XmlAttribute
+	@XmlSchemaType(name = "int")
+	@XmlJavaTypeAdapter(IntAdapter.class)
+	public Integer depth;
+	@XmlAttribute(name = "errorFile")
+	public String fileName;
+	@XmlAttribute(name = "errorByteLength")
+	public Long byteLength;
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/InputDescription.java b/server-port-description/src/main/java/org/taverna/server/port_description/InputDescription.java
new file mode 100644
index 0000000..60b9353
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/InputDescription.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import static org.taverna.server.port_description.Namespaces.XLINK;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * A description of the inputs of a workflow run, as they are currently known
+ * about.
+ * 
+ * @author Donal Fellows.
+ */
+@XmlRootElement
+public class InputDescription extends AbstractPortDescription {
+	@XmlElement
+	public List<InputPort> input = new ArrayList<>();
+
+	@XmlType(name = "InputPort")
+	public static class InputPort extends AbstractPort {
+		@XmlAttribute(namespace = XLINK)
+		@XmlSchemaType(name = "anyURI")
+		public URI href;
+	}
+
+	/**
+	 * Add an input port to the list of ports.
+	 * 
+	 * @param name
+	 *            The name of the port to add.
+	 * @return The port (so that its details may be set);
+	 */
+	public InputPort addPort(String name) {
+		InputPort p = new InputPort();
+		p.name = name;
+		input.add(p);
+		return p;
+	}
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/LeafValue.java b/server-port-description/src/main/java/org/taverna/server/port_description/LeafValue.java
new file mode 100644
index 0000000..052c0ef
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/LeafValue.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "LeafValue")
+public class LeafValue extends AbstractValue {
+	@XmlAttribute(name = "contentFile")
+	public String fileName;
+	@XmlAttribute(name = "contentType")
+	public String contentType;
+	@XmlAttribute(name = "contentByteLength")
+	public Long byteLength;
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/ListValue.java b/server-port-description/src/main/java/org/taverna/server/port_description/ListValue.java
new file mode 100644
index 0000000..b14cdf1
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/ListValue.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElements;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+import org.taverna.server.port_description.utils.IntAdapter;
+
+@XmlType(name = "ListValue")
+public class ListValue extends AbstractValue {
+	@XmlAttribute
+	@XmlSchemaType(name = "int")
+	@XmlJavaTypeAdapter(IntAdapter.class)
+	public Integer length;
+	@XmlElements({
+			@XmlElement(name = "value", type = LeafValue.class, nillable = false),
+			@XmlElement(name = "list", type = ListValue.class, nillable = false),
+			@XmlElement(name = "error", type = ErrorValue.class, nillable = false),
+			@XmlElement(name = "absent", type = AbsentValue.class, nillable = false) })
+	public List<AbstractValue> contents = new ArrayList<>();
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/Namespaces.java b/server-port-description/src/main/java/org/taverna/server/port_description/Namespaces.java
new file mode 100644
index 0000000..4003cdb
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/Namespaces.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+public interface Namespaces {
+	static final String DATA = "http://ns.taverna.org.uk/2010/port/";
+	static final String RUN = "http://ns.taverna.org.uk/2010/run/";
+	static final String XLINK = "http://www.w3.org/1999/xlink";
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/OutputDescription.java b/server-port-description/src/main/java/org/taverna/server/port_description/OutputDescription.java
new file mode 100644
index 0000000..0b94973
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/OutputDescription.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElements;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * A description of the outputs of a workflow run, as they are currently known
+ * about.
+ * 
+ * @author Donal Fellows
+ */
+@XmlRootElement(name = "workflowOutputs")
+public class OutputDescription extends AbstractPortDescription {
+	private static final AbsentValue ABSENT_VALUE = new AbsentValue();
+	@XmlElement(name = "output")
+	public List<OutputPort> ports = new ArrayList<>();
+
+	@XmlType(name = "OutputPort")
+	public static class OutputPort extends AbstractPort {
+		@XmlElements({
+				@XmlElement(name = "value", type = LeafValue.class, nillable = false, required = true),
+				@XmlElement(name = "list", type = ListValue.class, nillable = false, required = true),
+				@XmlElement(name = "error", type = ErrorValue.class, nillable = false, required = true),
+				@XmlElement(name = "absent", type = AbsentValue.class, nillable = false, required = true) })
+		public AbstractValue output;
+	}
+
+	/**
+	 * Add an output port to the list of ports.
+	 * 
+	 * @param name
+	 *            The name of the port to add.
+	 * @return The port (so that its value may be set);
+	 */
+	public OutputPort addPort(String name) {
+		OutputPort p = new OutputPort();
+		p.name = name;
+		p.output = ABSENT_VALUE;
+		ports.add(p);
+		return p;
+	}
+}
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/package-info.java b/server-port-description/src/main/java/org/taverna/server/port_description/package-info.java
new file mode 100644
index 0000000..b1ac2c6
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+@XmlSchema(namespace = DATA, elementFormDefault = QUALIFIED, attributeFormDefault = QUALIFIED, xmlns = {
+		@XmlNs(prefix = "port", namespaceURI = DATA),
+		@XmlNs(prefix = "xlink", namespaceURI = XLINK),
+		@XmlNs(prefix = "run", namespaceURI = RUN) })
+package org.taverna.server.port_description;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.port_description.Namespaces.DATA;
+import static org.taverna.server.port_description.Namespaces.RUN;
+import static org.taverna.server.port_description.Namespaces.XLINK;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+
diff --git a/server-port-description/src/main/java/org/taverna/server/port_description/utils/IntAdapter.java b/server-port-description/src/main/java/org/taverna/server/port_description/utils/IntAdapter.java
new file mode 100644
index 0000000..8764176
--- /dev/null
+++ b/server-port-description/src/main/java/org/taverna/server/port_description/utils/IntAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.port_description.utils;
+
+import static javax.xml.bind.DatatypeConverter.parseInt;
+import static javax.xml.bind.DatatypeConverter.printInt;
+
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+
+/**
+ * A type conversion utility for use with JAXB.
+ * 
+ * @author Donal Fellows
+ */
+public class IntAdapter extends XmlAdapter<String, Integer> {
+	@Override
+	public String marshal(Integer value) throws Exception {
+		if (value == null)
+			return null;
+		return printInt(value);
+	}
+
+	@Override
+	public Integer unmarshal(String value) throws Exception {
+		if (value == null)
+			return null;
+		return parseInt(value);
+	}
+}
diff --git a/server-port-description/src/test/java/org/taverna/server/port_description/JaxbSanityTest.java b/server-port-description/src/test/java/org/taverna/server/port_description/JaxbSanityTest.java
new file mode 100644
index 0000000..c952ec2
--- /dev/null
+++ b/server-port-description/src/test/java/org/taverna/server/port_description/JaxbSanityTest.java
@@ -0,0 +1,98 @@
+package org.taverna.server.port_description;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.SchemaOutputResolver;
+import javax.xml.transform.Result;
+import javax.xml.transform.stream.StreamResult;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * This test file ensures that the JAXB bindings will work once deployed instead
+ * of mysteriously failing in service.
+ * 
+ * @author Donal Fellows
+ */
+public class JaxbSanityTest {
+	SchemaOutputResolver sink;
+	StringWriter schema;
+
+	String schema() {
+		return schema.toString();
+	}
+
+	private String schemaTest(Class<?>... classes) throws IOException, JAXBException {
+		Assert.assertTrue(schema().isEmpty());
+		JAXBContext.newInstance(classes).generateSchema(sink);
+		Assert.assertFalse(schema().isEmpty());
+		return schema();
+	}
+
+	@Before
+	public void init() {
+		schema = new StringWriter();
+		sink = new SchemaOutputResolver() {
+			@Override
+			public Result createOutput(String namespaceUri,
+					String suggestedFileName) throws IOException {
+				StreamResult sr = new StreamResult(schema);
+				sr.setSystemId("/dev/null");
+				return sr;
+			}
+		};
+	}
+
+	@Test
+	public void testJAXBForInput() throws Exception {
+		schemaTest(InputDescription.InputPort.class);
+	}
+
+	@Test
+	public void testJAXBForInputDescription() throws Exception {
+		schemaTest(InputDescription.class);
+	}
+
+	@Test
+	public void testJAXBForAbsentValue() throws Exception {
+		schemaTest(AbstractValue.class);
+	}
+
+	@Test
+	public void testJAXBForAbstractValue() throws Exception {
+		schemaTest(AbstractValue.class);
+	}
+
+	@Test
+	public void testJAXBForErrorValue() throws Exception {
+		schemaTest(ErrorValue.class);
+	}
+
+	@Test
+	public void testJAXBForLeafValue() throws Exception {
+		schemaTest(LeafValue.class);
+	}
+
+	@Test
+	public void testJAXBForListValue() throws Exception {
+		schemaTest(ListValue.class);
+	}
+
+	@Test
+	public void testJAXBForOutputDescription() throws Exception {
+		schemaTest(OutputDescription.class);
+	}
+
+	@Test
+	public void testJAXBForEverythingAtOnce() throws Exception {
+		schemaTest(AbsentValue.class, AbstractValue.class, ListValue.class,
+				LeafValue.class, ErrorValue.class, OutputDescription.class,
+				InputDescription.InputPort.class, InputDescription.class);
+		// System.out.println(schema());
+	}
+}
diff --git a/server-rmidaemon/pom.xml b/server-rmidaemon/pom.xml
new file mode 100644
index 0000000..c2282b0
--- /dev/null
+++ b/server-rmidaemon/pom.xml
@@ -0,0 +1,64 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>uk.org.taverna.server</groupId>
+		<artifactId>server</artifactId>
+		<version>3.0-SNAPSHOT</version>
+	</parent>
+	<artifactId>server-rmidaemon</artifactId>
+	<name>RMI registry daemon</name>
+	<description>Customised RMI registry that supports restricting to localhost.</description>
+	<scm>
+		<url>${scmBrowseRoot}/server-rmidaemon</url>
+	</scm>
+
+	<properties>
+		<project.build.sourceEncoding>US-ASCII</project.build.sourceEncoding>
+		<mainClass>org.taverna.server.rmidaemon.Registry</mainClass>
+	</properties>
+	<dependencies>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<encoding>US-ASCII</encoding>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<configuration>
+					<descriptorRefs>
+						<descriptorRef>jar-with-dependencies</descriptorRef>
+					</descriptorRefs>
+					<archive>
+						<manifest>
+							<mainClass>${mainClass}</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+				<executions>
+					<execution>
+						<id>make-assembly</id>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/server-rmidaemon/src/main/java/org/taverna/server/rmidaemon/Registry.java b/server-rmidaemon/src/main/java/org/taverna/server/rmidaemon/Registry.java
new file mode 100644
index 0000000..4be7579
--- /dev/null
+++ b/server-rmidaemon/src/main/java/org/taverna/server/rmidaemon/Registry.java
@@ -0,0 +1,72 @@
+package org.taverna.server.rmidaemon;
+
+import static java.lang.System.setProperty;
+import static java.net.InetAddress.getLocalHost;
+import static java.rmi.registry.LocateRegistry.createRegistry;
+import static java.rmi.registry.Registry.REGISTRY_PORT;
+import static java.rmi.server.RMISocketFactory.getDefaultSocketFactory;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.net.ServerSocket;
+import java.rmi.MarshalledObject;
+import java.rmi.server.RMIServerSocketFactory;
+
+/**
+ * Special version of <tt>rmiregistry</tt>.
+ * 
+ * @author Donal Fellows
+ */
+public class Registry {
+	/**
+	 * Run a registry. The first optional argument is the port for the registry
+	 * to listen on, and the second optional argument is whether the registry
+	 * should restrict itself to connections from localhost only.
+	 * 
+	 * @param args
+	 *            Arguments to the program.
+	 */
+	public static void main(String... args) {
+		try {
+			if (args.length > 0)
+				port = Integer.parseInt(args[0]);
+		} catch (Exception e) {
+			System.err.println("failed to parse port: " + e.getMessage());
+			System.exit(2);
+		}
+		try {
+			if (args.length > 1)
+				localhostOnly = Boolean.parseBoolean(args[1]);
+		} catch (Exception e) {
+			System.err.println("failed to parse boolean localhost flag: "
+					+ e.getMessage());
+			System.exit(2);
+		}
+		try {
+			Object registryHandle = makeRegistry();
+			try (ObjectOutputStream oos = new ObjectOutputStream(System.out)) {
+				oos.writeObject(registryHandle);
+			}
+		} catch (Exception e) {
+			System.err.println("problem creating registry: " + e.getMessage());
+			System.exit(1);
+		}
+	}
+
+	private static int port = REGISTRY_PORT;
+	private static boolean localhostOnly = false;
+
+	private static MarshalledObject<java.rmi.registry.Registry> makeRegistry() throws IOException {
+		if (!localhostOnly)
+			return new MarshalledObject<>(createRegistry(port));
+		setProperty("java.rmi.server.hostname", "127.0.0.1");
+		return new MarshalledObject<>(createRegistry(port,
+				getDefaultSocketFactory(), new RMIServerSocketFactory() {
+					@Override
+					public ServerSocket createServerSocket(int port)
+							throws IOException {
+						return new ServerSocket(port, 0, getLocalHost());
+					}
+				}));
+	}
+}
diff --git a/server-rmidaemon/src/main/java/org/taverna/server/rmidaemon/package-info.java b/server-rmidaemon/src/main/java/org/taverna/server/rmidaemon/package-info.java
new file mode 100644
index 0000000..32144b5
--- /dev/null
+++ b/server-rmidaemon/src/main/java/org/taverna/server/rmidaemon/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * RMI daemon implementation. A variation of an RMI registry.
+ * @author Donal Fellows
+ */
+package org.taverna.server.rmidaemon;
\ No newline at end of file
diff --git a/server-runinterface/.gitignore b/server-runinterface/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/server-runinterface/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/server-runinterface/pom.xml b/server-runinterface/pom.xml
new file mode 100644
index 0000000..696487a
--- /dev/null
+++ b/server-runinterface/pom.xml
@@ -0,0 +1,28 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<artifactId>server-runinterface</artifactId>
+	<name>RMI Interface: Webapp &lt;-&gt; Worker</name>
+	<description>This is the implementation of the RMI interface between the workflow run factory and the web-app service front-end.</description>
+	<parent>
+		<groupId>uk.org.taverna.server</groupId>
+		<artifactId>server</artifactId>
+		<version>3.0-SNAPSHOT</version>
+		<relativePath>..</relativePath>
+	</parent>
+	<scm>
+		<url>${scmBrowseRoot}/server-runinterface</url>
+	</scm>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<encoding>US-ASCII</encoding>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/server-runinterface/src/main/java/META-INF/MANIFEST.MF b/server-runinterface/src/main/java/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..5e94951
--- /dev/null
+++ b/server-runinterface/src/main/java/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0

+Class-Path: 

+

diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/IllegalStateTransitionException.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/IllegalStateTransitionException.java
new file mode 100644
index 0000000..9f4bf50
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/IllegalStateTransitionException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Exception that indicates where a change of a workflow run's status is
+ * illegal.
+ * 
+ * @author Donal Fellows
+ * @see RemoteSingleRun#setStatus(RemoteStatus)
+ */
+@WebFault(name = "IllegalStateTransitionFault", targetNamespace = "http://ns.taverna.org.uk/2010/xml/server/worker/")
+public class IllegalStateTransitionException extends Exception {
+	private static final long serialVersionUID = 159673249162345L;
+
+	public IllegalStateTransitionException() {
+		this("illegal state transition");
+	}
+
+	public IllegalStateTransitionException(String message) {
+		super(message);
+	}
+
+	public IllegalStateTransitionException(Throwable cause) {
+		this("illegal state transition", cause);
+	}
+
+	public IllegalStateTransitionException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/ImplementationException.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/ImplementationException.java
new file mode 100644
index 0000000..a47d112
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/ImplementationException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Exception that indicates that the implementation has gone wrong in some
+ * unexpected way.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "ImplementationFault", targetNamespace = "http://ns.taverna.org.uk/2010/xml/server/worker/")
+@SuppressWarnings("serial")
+public class ImplementationException extends Exception {
+	public ImplementationException(String message) {
+		super(message);
+	}
+
+	public ImplementationException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteDirectory.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteDirectory.java
new file mode 100644
index 0000000..395842a
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteDirectory.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import java.io.IOException;
+import java.rmi.RemoteException;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Represents a directory that is the working directory of a workflow run, or a
+ * sub-directory of it.
+ * 
+ * @author Donal Fellows
+ * @see RemoteFile
+ */
+public interface RemoteDirectory extends RemoteDirectoryEntry {
+	/**
+	 * @return A list of the contents of the directory.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws IOException
+	 *             If anything goes wrong with listing the directory.
+	 */
+	@Nonnull
+	public Collection<RemoteDirectoryEntry> getContents()
+			throws RemoteException, IOException;
+
+	/**
+	 * Creates a sub-directory of this directory.
+	 * 
+	 * @param name
+	 *            The name of the sub-directory.
+	 * @return A handle to the newly-created directory.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws IOException
+	 *             If things go wrong with creating the subdirectory.
+	 */
+	@Nonnull
+	public RemoteDirectory makeSubdirectory(@Nonnull String name)
+			throws RemoteException, IOException;
+
+	/**
+	 * Creates an empty file in this directory.
+	 * 
+	 * @param name
+	 *            The name of the file to create.
+	 * @return A handle to the newly-created file.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws IOException
+	 *             If anything goes wrong with creating the file.
+	 */
+	@Nonnull
+	public RemoteFile makeEmptyFile(@Nonnull String name)
+			throws RemoteException, IOException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteDirectoryEntry.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteDirectoryEntry.java
new file mode 100644
index 0000000..1e04b44
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteDirectoryEntry.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import java.io.IOException;
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.util.Date;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * An entry in a {@link RemoteDirectory} representing a file or sub-directory.
+ * 
+ * @author Donal Fellows
+ * @see RemoteDirectory
+ * @see RemoteFile
+ */
+public interface RemoteDirectoryEntry extends Remote {
+	/**
+	 * @return The "local" name of the entry. This will never be "<tt>..</tt>"
+	 *         or contain the character "<tt>/</tt>".
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public String getName() throws RemoteException;
+
+	/**
+	 * @return The time when the entry was last modified.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public Date getModificationDate() throws RemoteException;
+
+	/**
+	 * Gets the directory containing this directory entry.
+	 * 
+	 * @return A directory handle, or <tt>null</tt> if called on the workflow
+	 *         run's working directory.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nullable
+	public RemoteDirectory getContainingDirectory() throws RemoteException;
+
+	/**
+	 * Destroy this directory entry, deleting the file or sub-directory. The
+	 * workflow run's working directory can never be manually destroyed.
+	 * 
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws IOException
+	 *             If things go wrong when deleting the directory entry.
+	 */
+	public void destroy() throws RemoteException, IOException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteFile.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteFile.java
new file mode 100644
index 0000000..e09466d
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteFile.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import java.io.IOException;
+import java.rmi.RemoteException;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Represents a file in the working directory of a workflow instance run, or in
+ * some sub-directory of it.
+ * 
+ * @author Donal Fellows
+ * @see RemoteDirectory
+ */
+public interface RemoteFile extends RemoteDirectoryEntry {
+	/**
+	 * Read from the file.
+	 * 
+	 * @param offset
+	 *            Where in the file to read the bytes from.
+	 * @param length
+	 *            How much of the file to read; -1 for "to the end".
+	 * @return The literal byte contents of the given section of the file.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws IOException
+	 *             If things go wrong reading the file.
+	 */
+	@Nonnull
+	byte[] getContents(int offset, int length) throws RemoteException,
+			IOException;
+
+	/**
+	 * Write the data to the file, totally replacing what was there before.
+	 * 
+	 * @param data
+	 *            The literal bytes that will form the new contents of the file.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws IOException
+	 *             If things go wrong writing the contents.
+	 */
+	void setContents(@Nonnull byte[] data) throws RemoteException, IOException;
+
+	/**
+	 * Append the data to the file.
+	 * 
+	 * @param data
+	 *            The literal bytes that will be appended.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws IOException
+	 *             If things go wrong writing the contents.
+	 */
+	void appendContents(@Nonnull byte[] data) throws RemoteException,
+			IOException;
+
+	/**
+	 * @return The length of the file, in bytes.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	long getSize() throws RemoteException;
+
+	/**
+	 * Copy from another file to this one.
+	 * 
+	 * @param sourceFile
+	 *            The other file to copy from.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws IOException
+	 *             If things go wrong during the copy.
+	 */
+	void copy(@Nonnull RemoteFile sourceFile) throws RemoteException,
+			IOException;
+
+	/**
+	 * @return The full native OS name for the file.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	String getNativeName() throws RemoteException;
+
+	/**
+	 * @return The host holding the file.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	String getNativeHost() throws RemoteException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteInput.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteInput.java
new file mode 100644
index 0000000..7a7510c
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteInput.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * This represents the assignment of inputs to input ports of the workflow. Note
+ * that the <tt>file</tt> and <tt>value</tt> properties are never set at the
+ * same time.
+ * 
+ * @author Donal Fellows
+ */
+public interface RemoteInput extends Remote {
+	/**
+	 * @return The file currently assigned to this input port, or <tt>null</tt>
+	 *         if no file is assigned.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nullable
+	String getFile() throws RemoteException;
+
+	/**
+	 * @return The name of this input port. This may not be changed.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	String getName() throws RemoteException;
+
+	/**
+	 * @return The value currently assigned to this input port, or <tt>null</tt>
+	 *         if no value is assigned.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nullable
+	String getValue() throws RemoteException;
+
+	/**
+	 * @return The delimiter currently used to split this input port's value
+	 *         into a list, or <tt>null</tt> if no delimiter is to be used
+	 *         (i.e., the value is a singleton).
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nullable
+	String getDelimiter() throws RemoteException;
+
+	/**
+	 * Sets the file to use for this input. This overrides the use of the
+	 * previous file and any set value.
+	 * 
+	 * @param file
+	 *            The filename to use. Must not start with a <tt>/</tt> or
+	 *            contain any <tt>..</tt> segments. Will be interpreted relative
+	 *            to the run's working directory.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	void setFile(@Nonnull String file) throws RemoteException;
+
+	/**
+	 * Sets the value to use for this input. This overrides the use of the
+	 * previous value and any set file.
+	 * 
+	 * @param value
+	 *            The value to use.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	void setValue(@Nonnull String value) throws RemoteException;
+
+	/**
+	 * Sets the delimiter used to split this input port's value into a list.
+	 * 
+	 * @param delimiter
+	 *            The delimiter character, or <tt>null</tt> if no delimiter is
+	 *            to be used (i.e., the value is a singleton).
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	void setDelimiter(@Nullable String delimiter) throws RemoteException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteListener.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteListener.java
new file mode 100644
index 0000000..4001721
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteListener.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+
+import javax.annotation.Nonnull;
+
+/**
+ * An event listener that is attached to a {@link RemoteSingleRun}.
+ * 
+ * @author Donal Fellows
+ */
+public interface RemoteListener extends Remote {
+	/**
+	 * @return The name of the listener.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public String getName() throws RemoteException;
+
+	/**
+	 * @return The type of the listener.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public String getType() throws RemoteException;
+
+	/**
+	 * @return The configuration document for the listener.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public String getConfiguration() throws RemoteException;
+
+	/**
+	 * @return The supported properties of the listener.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public String[] listProperties() throws RemoteException;
+
+	/**
+	 * Get the value of a particular property, which should be listed in the
+	 * {@link #listProperties()} method.
+	 * 
+	 * @param propName
+	 *            The name of the property to read.
+	 * @return The value of the property.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public String getProperty(@Nonnull String propName) throws RemoteException;
+
+	/**
+	 * Set the value of a particular property, which should be listed in the
+	 * {@link #listProperties()} method.
+	 * 
+	 * @param propName
+	 *            The name of the property to write.
+	 * @param value
+	 *            The value to set the property to.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	public void setProperty(@Nonnull String propName, @Nonnull String value)
+			throws RemoteException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteRunFactory.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteRunFactory.java
new file mode 100644
index 0000000..eec4ab5
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteRunFactory.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.util.UUID;
+
+import org.taverna.server.localworker.server.UsageRecordReceiver;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * The main RMI-enabled interface for creating runs.
+ * 
+ * @author Donal Fellows
+ */
+public interface RemoteRunFactory extends Remote {
+	/**
+	 * Makes a workflow run that will process a particular workflow document.
+	 * 
+	 * @param workflow
+	 *            The (serialised) workflow to instantiate as a run.
+	 * @param creator
+	 *            Who is this run created for?
+	 * @param usageRecordReceiver
+	 *            Where to write any usage records. May be <tt>null</tt> to
+	 *            cause them to not be written.
+	 * @param masterID
+	 *            The UUID of the run to use, or <tt>null</tt> if the execution
+	 *            engine is to manufacture a new one for itself.
+	 * @return A remote handle for the run.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	RemoteSingleRun make(@Nonnull byte[] workflow, @Nonnull String creator,
+			@Nullable UsageRecordReceiver usageRecordReceiver,
+			@Nullable UUID masterID) throws RemoteException;
+
+	/**
+	 * Asks this factory to unregister itself from the registry and cease
+	 * operation.
+	 * 
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	void shutdown() throws RemoteException;
+
+	/**
+	 * Configures the details to use when setting up the workflow run's
+	 * connnection to the interaction feed.
+	 * 
+	 * @param host
+	 *            The host where the feed is located.
+	 * @param port
+	 *            The port where the feed is located.
+	 * @param webdavPath
+	 *            The path used for pushing web pages into the feed.
+	 * @param feedPath
+	 *            The path used for reading and writing notifications on the
+	 *            feed.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	void setInteractionServiceDetails(@Nonnull String host,
+			@Nonnull String port, @Nonnull String webdavPath,
+			@Nonnull String feedPath) throws RemoteException;
+
+	/**
+	 * Gets a count of the number of {@linkplain RemoteSingleRun workflow runs}
+	 * that this factor knows about that are in the
+	 * {@link RemoteStatus#Operating Operating} state.
+	 * 
+	 * @return A count of "running" workflow runs.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	int countOperatingRuns() throws RemoteException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteSecurityContext.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteSecurityContext.java
new file mode 100644
index 0000000..35e6c09
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteSecurityContext.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import java.net.URI;
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Outline of the security context for a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+public interface RemoteSecurityContext extends Remote {
+	void setKeystore(@Nonnull byte[] keystore) throws RemoteException,
+			ImplementationException;
+
+	void setPassword(@Nonnull char[] password) throws RemoteException,
+			ImplementationException;
+
+	void setTruststore(@Nonnull byte[] truststore) throws RemoteException,
+			ImplementationException;
+
+	void setUriToAliasMap(@Nonnull Map<URI, String> uriToAliasMap)
+			throws RemoteException;
+
+	void setHelioToken(@Nonnull String helioToken) throws RemoteException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteSingleRun.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteSingleRun.java
new file mode 100644
index 0000000..fa68b81
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteSingleRun.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+import java.net.URL;
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+import java.util.Date;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public interface RemoteSingleRun extends Remote {
+	/**
+	 * @return The name of the Baclava file to use for all inputs, or
+	 *         <tt>null</tt> if no Baclava file is set.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nullable
+	public String getInputBaclavaFile() throws RemoteException;
+
+	/**
+	 * Sets the Baclava file to use for all inputs. This overrides the use of
+	 * individual inputs.
+	 * 
+	 * @param filename
+	 *            The filename to use. Must not start with a <tt>/</tt> or
+	 *            contain any <tt>..</tt> segments. Will be interpreted relative
+	 *            to the run's working directory.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	public void setInputBaclavaFile(@Nonnull String filename)
+			throws RemoteException;
+
+	/**
+	 * @return The list of input assignments.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public List<RemoteInput> getInputs() throws RemoteException;
+
+	/**
+	 * Create an input assignment.
+	 * 
+	 * @param name
+	 *            The name of the port that this will be an input for.
+	 * @return The assignment reference.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public RemoteInput makeInput(@Nonnull String name) throws RemoteException;
+
+	/**
+	 * @return The file (relative to the working directory) to write the outputs
+	 *         of the run to as a Baclava document, or <tt>null</tt> if they are
+	 *         to be written to non-Baclava files in a directory called
+	 *         <tt>out</tt>.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nullable
+	public String getOutputBaclavaFile() throws RemoteException;
+
+	/**
+	 * Sets where the output of the run is to be written to. This will cause the
+	 * output to be generated as a Baclava document, rather than a collection of
+	 * individual non-Baclava files in the subdirectory of the working directory
+	 * called <tt>out</tt>.
+	 * 
+	 * @param filename
+	 *            Where to write the Baclava file (or <tt>null</tt> to cause the
+	 *            output to be written to individual files); overwrites any
+	 *            previous setting of this value.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	public void setOutputBaclavaFile(@Nullable String filename)
+			throws RemoteException;
+
+	/**
+	 * @return The current status of the run.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public RemoteStatus getStatus() throws RemoteException;
+
+	/**
+	 * Set the status of the run, which should cause it to move into the given
+	 * state. This may cause some significant changes.
+	 * 
+	 * @param s
+	 *            The state to try to change to.
+	 * @throws IllegalStateTransitionException
+	 *             If the requested state change is impossible. (Note that it is
+	 *             always legal to set the status to the current status.)
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws ImplementationException
+	 *             If something goes horribly wrong on the back end.
+	 * @throws StillWorkingOnItException
+	 *             If the startup time of the workflow implementation exceeds a
+	 *             built-in threshold.
+	 */
+	public void setStatus(@Nonnull RemoteStatus s)
+			throws IllegalStateTransitionException, RemoteException,
+			ImplementationException, StillWorkingOnItException;
+
+	/**
+	 * @return When this workflow run was found to have finished, or
+	 *         <tt>null</tt> if it has never finished (either still running or
+	 *         never started).
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nullable
+	public Date getFinishTimestamp() throws RemoteException;
+
+	/**
+	 * @return When this workflow run was started, or <tt>null</tt> if it has
+	 *         never been started.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nullable
+	public Date getStartTimestamp() throws RemoteException;
+
+	/**
+	 * @return Handle to the main working directory of the run.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public RemoteDirectory getWorkingDirectory() throws RemoteException;
+
+	/**
+	 * @return The list of listener instances attached to the run.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public List<RemoteListener> getListeners() throws RemoteException;
+
+	/**
+	 * Add a listener to the run.
+	 * 
+	 * @param listener
+	 *            The listener to add.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws ImplementationException
+	 *             If something goes wrong when adding the listener.
+	 */
+	public void addListener(@Nonnull RemoteListener listener)
+			throws RemoteException, ImplementationException;
+
+	/**
+	 * @return The security context structure for this run.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws ImplementationException
+	 *             If something goes wrong when getting the context.
+	 */
+	@Nonnull
+	public RemoteSecurityContext getSecurityContext() throws RemoteException,
+			ImplementationException;
+
+	/**
+	 * Kill off this run, removing all resources which it consumes.
+	 * 
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 * @throws ImplementationException
+	 *             If something goes horribly wrong when destroying the run.
+	 */
+	public void destroy() throws RemoteException, ImplementationException;
+
+	/**
+	 * Get the types of listener supported by this run.
+	 * 
+	 * @return A list of listener type names.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public List<String> getListenerTypes() throws RemoteException;
+
+	/**
+	 * Create a listener that can be attached to this run.
+	 * 
+	 * @param type
+	 *            The type name of the listener to create; it must be one of the
+	 *            names returned by the {@link #getListenerTypes()} operation.
+	 * @param configuration
+	 *            The configuration document for this listener. The nature of
+	 *            the contents of this are determined by the type.
+	 * @return A handle for the listener.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	@Nonnull
+	public RemoteListener makeListener(@Nonnull String type,
+			@Nonnull String configuration) throws RemoteException;
+
+	/**
+	 * Configures the details to use when setting up the workflow run's
+	 * connnection to the interaction feed.
+	 * 
+	 * @param interactionFeed
+	 *            The location of the interaction feed. If <tt>null</tt>,
+	 *            defaults from the factory will be used instead.
+	 * @param webdavPath
+	 *            The location used for pushing web pages to support the feed.
+	 *            If <tt>null</tt>, a default from the factory will be used
+	 *            instead.
+	 * @param publishUrlBase
+	 *            Where to <i>actually</i> publish to, if this needs to be
+	 *            different from the location presented in the published HTML
+	 *            and Feed entries. Necessary in complex network scenarios.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	void setInteractionServiceDetails(@Nonnull URL interactionFeed,
+			@Nonnull URL webdavPath, @Nullable URL publishUrlBase) throws RemoteException;
+
+	/**
+	 * A do-nothing method, used to check the general reachability of the
+	 * workflow run.
+	 * 
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	void ping() throws RemoteException;
+
+	/**
+	 * Sets whether we should generate provenance information from a run.
+	 * 
+	 * @param generateProvenance
+	 *            Boolean flag, true for do the generation. Must be set before
+	 *            starting the run for this to have an effect.
+	 * @throws RemoteException
+	 *             If anything goes wrong with the communication.
+	 */
+	void setGenerateProvenance(boolean generateProvenance)
+			throws RemoteException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteStatus.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteStatus.java
new file mode 100644
index 0000000..db039f0
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/RemoteStatus.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+/**
+ * States of a workflow run. They are {@link RemoteStatus#Initialized
+ * Initialized}, {@link RemoteStatus#Operating Operating},
+ * {@link RemoteStatus#Stopped Stopped}, and {@link RemoteStatus#Finished
+ * Finished}. Conceptually, there is also a <tt>Destroyed</tt> state, but the
+ * workflow run does not exist (and hence can't have its state queried or set)
+ * in that case.
+ * 
+ * @author Donal Fellows
+ */
+public enum RemoteStatus {
+	/**
+	 * The workflow run has been created, but is not yet running. The run will
+	 * need to be manually moved to {@link #Operating} when ready.
+	 */
+	Initialized,
+	/**
+	 * The workflow run is going, reading input, generating output, etc. Will
+	 * eventually either move automatically to {@link #Finished} or can be moved
+	 * manually to {@link #Stopped} (where supported).
+	 */
+	Operating,
+	/**
+	 * The workflow run is paused, and will need to be moved back to
+	 * {@link #Operating} manually.
+	 */
+	Stopped,
+	/**
+	 * The workflow run has ceased; data files will continue to exist until the
+	 * run is destroyed (which may be manual or automatic).
+	 */
+	Finished
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/StillWorkingOnItException.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/StillWorkingOnItException.java
new file mode 100644
index 0000000..4643d5c
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/StillWorkingOnItException.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.remote;
+
+/**
+ * Exception that indicates that the implementation is still working on
+ * processing the operation. Note that though this is an exception, it is <i>not
+ * a failure</i>.
+ * 
+ * @author Donal Fellows
+ */
+@SuppressWarnings("serial")
+public class StillWorkingOnItException extends Exception {
+	public StillWorkingOnItException(String string) {
+		super(string);
+	}
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/remote/package-info.java b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/package-info.java
new file mode 100644
index 0000000..9880a3b
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/remote/package-info.java
@@ -0,0 +1,9 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Interfaces exported by worker classes to the server.
+ */
+package org.taverna.server.localworker.remote;
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/server/UsageRecordReceiver.java b/server-runinterface/src/main/java/org/taverna/server/localworker/server/UsageRecordReceiver.java
new file mode 100644
index 0000000..89c29ae
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/server/UsageRecordReceiver.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.server;
+
+import java.rmi.Remote;
+import java.rmi.RemoteException;
+
+/**
+ * Interface exported by (part of) the webapp to allow processes it creates to
+ * push in usage records.
+ * 
+ * @author Donal Fellows
+ */
+public interface UsageRecordReceiver extends Remote {
+	/**
+	 * Called to push in a usage record. Note that it is assumed that the usage
+	 * record already contains all the information required to locate and
+	 * process the job; there is no separate handle.
+	 * 
+	 * @param usageRecord
+	 *            The serialised XML of the usage record.
+	 * @throws RemoteException
+	 *             if anything goes wrong.
+	 */
+	void acceptUsageRecord(String usageRecord) throws RemoteException;
+}
diff --git a/server-runinterface/src/main/java/org/taverna/server/localworker/server/package-info.java b/server-runinterface/src/main/java/org/taverna/server/localworker/server/package-info.java
new file mode 100644
index 0000000..cdd592b
--- /dev/null
+++ b/server-runinterface/src/main/java/org/taverna/server/localworker/server/package-info.java
@@ -0,0 +1,9 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Interfaces exported by the server to worker classes.
+ */
+package org.taverna.server.localworker.server;
diff --git a/server-unix-forker/pom.xml b/server-unix-forker/pom.xml
new file mode 100644
index 0000000..cdbce7b
--- /dev/null
+++ b/server-unix-forker/pom.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>server</artifactId>
+		<groupId>uk.org.taverna.server</groupId>
+		<version>3.0-SNAPSHOT</version>
+	</parent>
+	<artifactId>server-unix-forker</artifactId>
+	<scm>
+		<url>${scmBrowseRoot}/server-unix-forker</url>
+	</scm>
+
+	<properties>
+		<project.build.sourceEncoding>US-ASCII</project.build.sourceEncoding>
+		<forkerMainClass>org.taverna.server.unixforker.Forker</forkerMainClass>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<encoding>US-ASCII</encoding>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<configuration>
+					<descriptorRefs>
+						<descriptorRef>jar-with-dependencies</descriptorRef>
+					</descriptorRefs>
+					<archive>
+						<manifest>
+							<mainClass>${forkerMainClass}</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+				<executions>
+					<execution>
+						<id>make-assembly</id>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+	<description>Manages the starting of the worker processes as different users. Unix-specific.</description>
+	<name>Impersonation Module (Unix)</name>
+</project>
diff --git a/server-unix-forker/src/main/java/org/taverna/server/unixforker/Forker.java b/server-unix-forker/src/main/java/org/taverna/server/unixforker/Forker.java
new file mode 100644
index 0000000..f9dc632
--- /dev/null
+++ b/server-unix-forker/src/main/java/org/taverna/server/unixforker/Forker.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.unixforker;
+
+import static java.lang.System.err;
+import static java.lang.System.getProperty;
+import static java.lang.System.in;
+import static java.lang.System.out;
+import static java.util.Arrays.asList;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A simple class that forks off processes when asked to over its standard
+ * input. The one complication is that it forks them off as other users, through
+ * the use of the <tt>sudo</tt> utility. It is Unix-specific.
+ * 
+ * @author Donal Fellows
+ */
+public class Forker extends Thread {
+	private static String password;
+	private static BufferedReader br;
+
+	/**
+	 * Helper to make reading a password from a file clearer. The password must
+	 * be the first line of the file.
+	 * 
+	 * @param passwordFile
+	 *            The file to load from.
+	 * @throws IOException
+	 *             If anything goes wrong.
+	 */
+	private static void loadPassword(@Nonnull File passwordFile)
+			throws IOException {
+		try {
+			err.println("attempting to load password from " + passwordFile);
+			try (FileReader fr = new FileReader(passwordFile)) {
+				password = new BufferedReader(fr).readLine();
+			}
+		} catch (IOException e) {
+			err.println("failed to read password from file " + passwordFile
+					+ "described in password.file property");
+			throw e;
+		}
+	}
+
+	/**
+	 * Initialization code, which runs before the main loop starts processing.
+	 * 
+	 * @param args
+	 *            The arguments to the program.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	public static void init(String[] args) throws Exception {
+		if (args.length < 1)
+			throw new IllegalArgumentException(
+					"wrong # args: must be \"program ?argument ...?\"");
+		if (getProperty("password.file") != null)
+			loadPassword(new File(getProperty("password.file")));
+		if (password == null)
+			err.println("no password.file property or empty file; "
+					+ "assuming password-less sudo is configured");
+		else
+			err.println("password is of length " + password.length());
+		br = new BufferedReader(new InputStreamReader(in));
+	}
+
+	/**
+	 * The body of the main loop of this program.
+	 * 
+	 * @param args
+	 *            The arguments to use when running the other program.
+	 * @return Whether to repeat the loop.
+	 * @throws Exception
+	 *             If anything goes wrong. Note that the loop is repeated if an
+	 *             exception occurs in it.
+	 */
+	public static boolean mainLoopBody(String[] args) throws Exception {
+		String line = br.readLine();
+		if (line == null)
+			return false;
+		List<String> vals = asList(line.split("[ \t]+"));
+		if (vals.size() != 2) {
+			out.println("wrong # values: must be \"username UUID\"");
+			return true;
+		}
+		ProcessBuilder pb = new ProcessBuilder();
+		pb.command()
+				.addAll(asList("sudo", "-u", vals.get(0), "-S", "-H", "--"));
+		pb.command().addAll(asList(args));
+		pb.command().add(vals.get(1));
+		Forker f = new Forker(pb);
+		f.setDaemon(true);
+		f.start();
+		return true;
+	}
+
+	/**
+	 * The main code for this class, which turns this into an executable
+	 * program. Runs the initialisation and then the main loop, in both cases
+	 * with appropriate error handling.
+	 * 
+	 * @param args
+	 *            Arguments to this program.
+	 */
+	public static void main(String... args) {
+		try {
+			init(args);
+			while (true) {
+				try {
+					if (!mainLoopBody(args))
+						break;
+				} catch (Exception e) {
+					e.printStackTrace(err);
+					out.println(e.getClass().getName() + ": " + e.getMessage());
+				}
+			}
+			System.exit(0);
+		} catch (Exception e) {
+			e.printStackTrace(err);
+			System.exit(1);
+		}
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+
+	public Forker(ProcessBuilder pb) throws IOException {
+		out.println("Starting subprocess: " + pb.command());
+		final Process p = pb.start();
+		abstract class ProcessAttachedDaemon extends Thread {
+			public ProcessAttachedDaemon() {
+				setDaemon(true);
+				start();
+			}
+
+			abstract void act() throws Exception;
+
+			@Override
+			public final void run() {
+				try {
+					act();
+					p.waitFor();
+				} catch (InterruptedException e) {
+					// Just drop
+				} catch (Exception e) {
+					p.destroy();
+					e.printStackTrace(err);
+				}
+			}
+		}
+		new ProcessAttachedDaemon() {
+			@Override
+			void act() throws Exception {
+				copyFromSudo("Subprocess(out):", p.getInputStream());
+			}
+		};
+		new ProcessAttachedDaemon() {
+			@Override
+			void act() throws Exception {
+				copyFromSudo("Subprocess(err):", p.getErrorStream());
+			}
+		};
+		new ProcessAttachedDaemon() {
+			@Override
+			void act() throws Exception {
+				interactWithSudo(p.getOutputStream());
+			}
+		};
+	}
+
+	protected void interactWithSudo(OutputStream os) throws Exception {
+		if (password != null) {
+			OutputStreamWriter osw = new OutputStreamWriter(os);
+			osw.write(password + "\n");
+			osw.flush();
+		}
+		os.close();
+	}
+
+	protected void copyFromSudo(String header, InputStream sudoStream)
+			throws Exception {
+		int b = '\n';
+		while (true) {
+			if (b == '\n')
+				out.print(header);
+			b = sudoStream.read();
+			if (b == -1)
+				break;
+			out.write(b);
+			out.flush();
+		}
+		sudoStream.close();
+	}
+}
diff --git a/server-usagerecord/.gitignore b/server-usagerecord/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/server-usagerecord/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/server-usagerecord/pom.xml b/server-usagerecord/pom.xml
new file mode 100644
index 0000000..5352779
--- /dev/null
+++ b/server-usagerecord/pom.xml
@@ -0,0 +1,81 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>server</artifactId>
+		<groupId>uk.org.taverna.server</groupId>
+		<version>3.0-SNAPSHOT</version>
+		<relativePath>..</relativePath>
+	</parent>
+	<artifactId>server-usagerecord</artifactId>
+	<name>Usage Record Support</name>
+	<scm>
+		<url>${scmBrowseRoot}/server-usagerecord</url>
+	</scm>
+	<dependencies>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<type>jar</type>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<description>Basic Java bindings for the OGF Usage Record Format, version 1.0, plus a simple wrapper to make working with it easier.</description>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>jaxb2-maven-plugin</artifactId>
+				<version>1.3.1</version>
+				<executions>
+					<execution>
+						<id>xsd2java</id>
+						<goals>
+							<goal>xjc</goal>
+						</goals>
+						<configuration>
+							<outputDirectory>${project.build.directory}/generated-sources/xjc</outputDirectory>
+							<schemaDirectory>${basedir}/src/main/xsd</schemaDirectory>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+		</plugins>
+		<pluginManagement>
+			<plugins>
+				<!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+				<plugin>
+					<groupId>org.eclipse.m2e</groupId>
+					<artifactId>lifecycle-mapping</artifactId>
+					<version>1.0.0</version>
+					<configuration>
+						<lifecycleMappingMetadata>
+							<pluginExecutions>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>org.codehaus.mojo</groupId>
+										<artifactId>jaxb2-maven-plugin</artifactId>
+										<versionRange>[1.3.1,)</versionRange>
+										<goals>
+											<goal>xjc</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<execute />
+									</action>
+								</pluginExecution>
+							</pluginExecutions>
+						</lifecycleMappingMetadata>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
+</project>
diff --git a/server-usagerecord/src/main/java/org/ogf/usage/JobUsageRecord.java b/server-usagerecord/src/main/java/org/ogf/usage/JobUsageRecord.java
new file mode 100644
index 0000000..d12d3d8
--- /dev/null
+++ b/server-usagerecord/src/main/java/org/ogf/usage/JobUsageRecord.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.ogf.usage;
+
+import static java.util.UUID.randomUUID;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.GregorianCalendar;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.datatype.DatatypeFactory;
+import javax.xml.transform.dom.DOMSource;
+
+import org.ogf.usage.v1_0.Charge;
+import org.ogf.usage.v1_0.ConsumableResourceType;
+import org.ogf.usage.v1_0.CpuDuration;
+import org.ogf.usage.v1_0.Disk;
+import org.ogf.usage.v1_0.EndTime;
+import org.ogf.usage.v1_0.Host;
+import org.ogf.usage.v1_0.JobName;
+import org.ogf.usage.v1_0.MachineName;
+import org.ogf.usage.v1_0.Memory;
+import org.ogf.usage.v1_0.Network;
+import org.ogf.usage.v1_0.NodeCount;
+import org.ogf.usage.v1_0.Processors;
+import org.ogf.usage.v1_0.ProjectName;
+import org.ogf.usage.v1_0.Queue;
+import org.ogf.usage.v1_0.RecordIdentity;
+import org.ogf.usage.v1_0.ResourceType;
+import org.ogf.usage.v1_0.ServiceLevel;
+import org.ogf.usage.v1_0.StartTime;
+import org.ogf.usage.v1_0.Status;
+import org.ogf.usage.v1_0.SubmitHost;
+import org.ogf.usage.v1_0.Swap;
+import org.ogf.usage.v1_0.TimeDuration;
+import org.ogf.usage.v1_0.TimeInstant;
+import org.ogf.usage.v1_0.UserIdentity;
+import org.ogf.usage.v1_0.WallDuration;
+import org.w3c.dom.Element;
+
+@XmlRootElement(name = "UsageRecord", namespace = "http://schema.ogf.org/urf/2003/09/urf")
+public class JobUsageRecord extends org.ogf.usage.v1_0.UsageRecordType {
+	/**
+	 * Create a new usage record with a random UUID as its identity.
+	 * 
+	 * @throws DatatypeConfigurationException
+	 *             If the factory for XML-relevant datatypes fails to build; not
+	 *             expected.
+	 */
+	public JobUsageRecord() throws DatatypeConfigurationException {
+		datatypeFactory = DatatypeFactory.newInstance();
+		RecordIdentity recid = new RecordIdentity();
+		recid.setRecordId(randomUUID().toString());
+		recid.setCreateTime(datatypeFactory
+				.newXMLGregorianCalendar(new GregorianCalendar()));
+		setRecordIdentity(recid);
+	}
+
+	/**
+	 * Create a new usage record with a random UUID as its identity.
+	 * 
+	 * @param name
+	 *            The name of the job to which this record pertains.
+	 * @throws DatatypeConfigurationException
+	 *             If the factory for XML-relevant datatypes fails to build; not
+	 *             expected.
+	 */
+	public JobUsageRecord(String name) throws DatatypeConfigurationException {
+		this();
+		setJobName(name);
+	}
+
+	@XmlTransient
+	private DatatypeFactory datatypeFactory;
+
+	public Status setStatus(String status) {
+		Status s = new Status();
+		s.setValue(status);
+		setStatus(s);
+		return s;
+	}
+
+	public WallDuration addWallDuration(long millis) {
+		WallDuration wall = new WallDuration();
+		wall.setValue(datatypeFactory.newDuration(millis));
+		getWallDurationOrCpuDurationOrNodeCount().add(wall);
+		return wall;
+	}
+
+	public CpuDuration addCpuDuration(long millis) {
+		CpuDuration cpu = new CpuDuration();
+		cpu.setValue(datatypeFactory.newDuration(millis));
+		getWallDurationOrCpuDurationOrNodeCount().add(cpu);
+		return cpu;
+	}
+
+	public NodeCount addNodeCount(int nodes) {
+		NodeCount nc = new NodeCount();
+		nc.setValue(BigInteger.valueOf(nodes));
+		getWallDurationOrCpuDurationOrNodeCount().add(nc);
+		return nc;
+	}
+
+	public Processors addProcessors(int processors) {
+		Processors pc = new Processors();
+		pc.setValue(BigInteger.valueOf(processors));
+		getWallDurationOrCpuDurationOrNodeCount().add(pc);
+		return pc;
+	}
+
+	public SubmitHost addSubmitHost(String host) {
+		SubmitHost sh = new SubmitHost();
+		sh.setValue(host);
+		getWallDurationOrCpuDurationOrNodeCount().add(sh);
+		return sh;
+	}
+
+	public Host addHost(String host) {
+		Host h = new Host();
+		h.setValue(host);
+		getWallDurationOrCpuDurationOrNodeCount().add(h);
+		return h;
+	}
+
+	public MachineName addMachine(String host) {
+		MachineName machine = new MachineName();
+		machine.setValue(host);
+		getWallDurationOrCpuDurationOrNodeCount().add(machine);
+		return machine;
+	}
+
+	public ProjectName addProject(String project) {
+		ProjectName p = new ProjectName();
+		p.setValue(project);
+		getWallDurationOrCpuDurationOrNodeCount().add(p);
+		return p;
+	}
+
+	public void addStartAndEnd(Date start, Date end) {
+		GregorianCalendar gc;
+
+		gc = new GregorianCalendar();
+		gc.setTime(start);
+		StartTime st = new StartTime();
+		st.setValue(datatypeFactory.newXMLGregorianCalendar(gc));
+		getWallDurationOrCpuDurationOrNodeCount().add(st);
+
+		gc = new GregorianCalendar();
+		gc.setTime(end);
+		EndTime et = new EndTime();
+		et.setValue(datatypeFactory.newXMLGregorianCalendar(gc));
+		getWallDurationOrCpuDurationOrNodeCount().add(et);
+	}
+
+	public Queue addQueue(String queue) {
+		Queue q = new Queue();
+		q.setValue(queue);
+		getWallDurationOrCpuDurationOrNodeCount().add(q);
+		return q;
+	}
+
+	public void addResource(ConsumableResourceType consumable) {
+		getWallDurationOrCpuDurationOrNodeCount().add(consumable);
+	}
+
+	public ResourceType addResource(ResourceType resource) {
+		getWallDurationOrCpuDurationOrNodeCount().add(resource);
+		return resource;
+	}
+
+	public ResourceType addResource(String description, String value) {
+		ResourceType resource = new ResourceType();
+		resource.setDescription(description);
+		resource.setValue(value);
+		getWallDurationOrCpuDurationOrNodeCount().add(resource);
+		return resource;
+	}
+
+	public ServiceLevel addServiceLevel(String service) {
+		ServiceLevel sl = new ServiceLevel();
+		sl.setValue(service);
+		getDiskOrMemoryOrSwap().add(sl);
+		return sl;
+	}
+
+	public Memory addMemory(long memory) {
+		Memory mem = new Memory();
+		mem.setValue(BigInteger.valueOf(memory));
+		getDiskOrMemoryOrSwap().add(mem);
+		return mem;
+	}
+
+	public TimeInstant addTimestamp(Date timestamp, String type) {
+		TimeInstant instant = new TimeInstant();
+		GregorianCalendar gc = new GregorianCalendar();
+		gc.setTime(timestamp);
+		instant.setValue(datatypeFactory.newXMLGregorianCalendar(gc));
+		instant.setType(type);
+		getDiskOrMemoryOrSwap().add(instant);
+		return instant;
+	}
+
+	public TimeDuration addDuration(long millis, String type) {
+		TimeDuration duration = new TimeDuration();
+		duration.setValue(datatypeFactory.newDuration(millis));
+		duration.setType(type);
+		getDiskOrMemoryOrSwap().add(duration);
+		return duration;
+	}
+
+	public Network addNetwork(long value) {
+		Network net = new Network();
+		net.setValue(BigInteger.valueOf(value));
+		getDiskOrMemoryOrSwap().add(net);
+		return net;
+	}
+
+	public Disk addDisk(long value) {
+		Disk disk = new Disk();
+		disk.setValue(BigInteger.valueOf(value));
+		getDiskOrMemoryOrSwap().add(disk);
+		return disk;
+	}
+
+	public Swap addSwap(long value) {
+		Swap net = new Swap();
+		net.setValue(BigInteger.valueOf(value));
+		getDiskOrMemoryOrSwap().add(net);
+		return net;
+	}
+
+	public UserIdentity addUser(String localUID, String globalName) {
+		UserIdentity user = new UserIdentity();
+		user.setLocalUserId(localUID);
+		user.setGlobalUserName(globalName);
+		getUserIdentity().add(user);
+		return user;
+	}
+
+	public JobName setJobName(String name) {
+		JobName jn = new JobName();
+		jn.setValue(name);
+		this.setJobName(jn);
+		return jn;
+	}
+
+	public Charge addCharge(float value) {
+		Charge c = new Charge();
+		c.setValue(value);
+		this.setCharge(c);
+		return c;
+	}
+
+	@SuppressWarnings("unchecked")
+	public <T> T getOfType(Class<T> clazz) {
+		for (Object o : getWallDurationOrCpuDurationOrNodeCount())
+			if (clazz.isInstance(o))
+				return (T) o;
+		for (Object o : getDiskOrMemoryOrSwap())
+			if (clazz.isInstance(o))
+				return (T) o;
+		return null;
+	}
+
+	public String marshal() throws JAXBException {
+		StringWriter writer = new StringWriter();
+		JAXBContext.newInstance(getClass()).createMarshaller()
+				.marshal(this, writer);
+		return writer.toString();
+	}
+
+	private static JAXBContext context;
+	static {
+		try {
+			context = JAXBContext.newInstance(JobUsageRecord.class);
+		} catch (JAXBException e) {
+			throw new RuntimeException("failed to handle JAXB annotated class",
+					e);
+		}
+	}
+
+	public static JobUsageRecord unmarshal(String s) throws JAXBException {
+		return (JobUsageRecord) context.createUnmarshaller().unmarshal(
+				new StringReader(s));
+	}
+
+	public static JobUsageRecord unmarshal(Element elem) throws JAXBException {
+		return context.createUnmarshaller()
+				.unmarshal(new DOMSource(elem), JobUsageRecord.class)
+				.getValue();
+	}
+
+	// TODO: Add signing support
+}
diff --git a/server-usagerecord/src/main/xsd/ur.xsd b/server-usagerecord/src/main/xsd/ur.xsd
new file mode 100644
index 0000000..16344a7
--- /dev/null
+++ b/server-usagerecord/src/main/xsd/ur.xsd
@@ -0,0 +1,425 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsd:schema attributeFormDefault="qualified" jaxb:version="2.1"
+	elementFormDefault="qualified" targetNamespace="http://schema.ogf.org/urf/2003/09/urf"
+	xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:urf="http://schema.ogf.org/urf/2003/09/urf"
+	xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
+	xsi:schemaLocation="http://www.w3.org/2001/XMLSchema http://www.w3.org/2001/XMLSchema.xsd">
+	<xsd:annotation>
+		<!-- Copyright (c) 2003-2008 Open Grid Forum -->
+		<xsd:documentation xml:lang="en">Usage Record Working Group XML Schema definition</xsd:documentation>
+		<xsd:appinfo>
+			<jaxb:schemaBindings>
+				<jaxb:package name="org.ogf.usage.v1_0" />
+			</jaxb:schemaBindings>
+		</xsd:appinfo>
+	</xsd:annotation>
+	<xsd:import namespace="http://www.w3.org/2000/09/xmldsig#"
+		schemaLocation="xmlds.xsd" /> <!-- http://www.w3.org/TR/xmldsig-core/xmldsig-core-schema.xsd -->
+	<xsd:complexType name="UsageRecordType">
+		<xsd:sequence>
+			<xsd:element ref="urf:RecordIdentity" />
+			<xsd:element minOccurs="0" ref="urf:JobIdentity" />
+			<xsd:element maxOccurs="unbounded" minOccurs="0" ref="urf:UserIdentity" />
+			<xsd:element minOccurs="0" ref="urf:JobName" />
+			<xsd:element minOccurs="0" ref="urf:Charge" />
+			<xsd:element ref="urf:Status" />
+			<xsd:choice maxOccurs="unbounded" minOccurs="0">
+				<xsd:annotation>
+					<xsd:documentation>
+						The elements grouped together in this choice may be represented
+						within a usage record multiple times. Each of these appearances
+						must be differentiated by the metric and/or type associated with
+						the element.
+					</xsd:documentation>
+				</xsd:annotation>
+				<xsd:element ref="urf:Disk" />
+				<xsd:element ref="urf:Memory" />
+				<xsd:element ref="urf:Swap" />
+				<xsd:element ref="urf:Network" />
+				<xsd:element ref="urf:TimeDuration" />
+				<xsd:element ref="urf:TimeInstant" />
+				<xsd:element ref="urf:ServiceLevel" />
+			</xsd:choice>
+			<xsd:choice maxOccurs="unbounded" minOccurs="0">
+				<xsd:element minOccurs="0" ref="urf:WallDuration" />
+				<xsd:sequence minOccurs="0">
+					<xsd:element maxOccurs="2" minOccurs="0" ref="urf:CpuDuration" />
+				</xsd:sequence>
+				<xsd:element minOccurs="0" ref="urf:NodeCount" />
+				<xsd:element minOccurs="0" ref="urf:Processors" />
+				<xsd:element minOccurs="0" ref="urf:EndTime" />
+				<xsd:element minOccurs="0" ref="urf:StartTime" />
+				<xsd:element minOccurs="0" ref="urf:MachineName" />
+				<xsd:element minOccurs="0" ref="urf:SubmitHost" />
+				<xsd:element minOccurs="0" ref="urf:Queue" />
+				<xsd:sequence minOccurs="0">
+					<xsd:element maxOccurs="unbounded" minOccurs="0" ref="urf:ProjectName" />
+				</xsd:sequence>
+				<xsd:sequence minOccurs="0">
+					<xsd:element maxOccurs="unbounded" minOccurs="0" ref="urf:Host" />
+				</xsd:sequence>
+				<xsd:sequence minOccurs="0">
+					<xsd:choice maxOccurs="unbounded" minOccurs="0">
+						<xsd:element ref="urf:PhaseResource" />
+						<xsd:element ref="urf:VolumeResource" />
+						<xsd:element ref="urf:Resource" />
+						<xsd:element ref="urf:ConsumableResource" />
+					</xsd:choice>
+				</xsd:sequence>
+			</xsd:choice>
+		</xsd:sequence>
+	</xsd:complexType>
+	<xsd:element abstract="true" name="Usage" type="urf:UsageRecordType" />
+	<xsd:element name="UsageRecord" substitutionGroup="urf:Usage"
+		type="urf:UsageRecordType" />
+	<xsd:element name="JobUsageRecord" substitutionGroup="urf:Usage">
+		<xsd:complexType>
+			<xsd:complexContent>
+				<xsd:extension base="urf:UsageRecordType" />
+			</xsd:complexContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="UsageRecords">
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element maxOccurs="unbounded" minOccurs="0" ref="urf:Usage" />
+			</xsd:sequence>
+		</xsd:complexType>
+	</xsd:element>
+	<!-- Common properties that may be measured with several different metrics 
+		within the same usage record -->
+	<xsd:element name="Network">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:positiveInteger">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attributeGroup ref="urf:intervallicVolume" />
+					<xsd:attribute default="total" ref="urf:metric" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="Disk">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:positiveInteger">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attributeGroup ref="urf:intervallicVolume" />
+					<xsd:attribute default="total" ref="urf:metric" use="optional" />
+					<xsd:attribute ref="urf:type" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="Memory">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:positiveInteger">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attributeGroup ref="urf:intervallicVolume" />
+					<xsd:attribute default="total" ref="urf:metric" use="optional" />
+					<xsd:attribute ref="urf:type" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="Swap">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:positiveInteger">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attributeGroup ref="urf:intervallicVolume" />
+					<xsd:attribute default="total" ref="urf:metric" use="optional" />
+					<xsd:attribute ref="urf:type" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="NodeCount">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:positiveInteger">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attribute default="total" ref="urf:metric" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="Processors">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:positiveInteger">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attribute ref="urf:metric" use="optional" />
+					<xsd:attribute name="consumptionRate" type="xsd:float"
+						use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="TimeDuration">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:duration">
+					<xsd:attribute ref="urf:type" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="TimeInstant">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:dateTime">
+					<xsd:attribute ref="urf:type" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="ServiceLevel">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:token">
+					<xsd:attribute ref="urf:type" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<!-- This element should appear at most twice within a usage record, with 
+		differing values for usageType for each appearance -->
+	<xsd:element name="CpuDuration">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:duration">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attribute name="usageType">
+						<xsd:simpleType>
+							<xsd:restriction base="xsd:token">
+								<xsd:enumeration value="user" />
+								<xsd:enumeration value="system" />
+							</xsd:restriction>
+						</xsd:simpleType>
+					</xsd:attribute>
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<!-- These common properties should appear at most once within a usage record, 
+		rather that at most once per metric per usage record -->
+	<xsd:element name="WallDuration">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:duration">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="EndTime">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:dateTime">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="StartTime">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:dateTime">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="MachineName">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="urf:domainNameType">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="SubmitHost">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="urf:domainNameType">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="Host">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="urf:domainNameType">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attribute default="false" name="primary" type="xsd:boolean" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="Queue">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:string">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="JobName">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:string">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="ProjectName">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:string">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="Status">
+		<xsd:annotation>
+			<xsd:documentation>
+				Minimum required set = {Aborted, Completed, Failed,
+				Held, Queued, Started, Suspended}
+			</xsd:documentation>
+		</xsd:annotation>
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:token">
+					<xsd:attribute ref="urf:description" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="Charge">
+		<xsd:complexType>
+			<xsd:simpleContent>
+				<xsd:extension base="xsd:float">
+					<xsd:attribute ref="urf:description" use="optional" />
+					<xsd:attribute ref="urf:unit" use="optional" />
+					<xsd:attribute name="formula" type="xsd:string" use="optional" />
+				</xsd:extension>
+			</xsd:simpleContent>
+		</xsd:complexType>
+	</xsd:element>
+	<!-- identity elements -->
+	<xsd:element name="JobIdentity">
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element minOccurs="0" name="GlobalJobId" type="xsd:string" />
+				<xsd:element minOccurs="0" name="LocalJobId" type="xsd:string" />
+				<xsd:sequence>
+					<xsd:element maxOccurs="unbounded" minOccurs="0"
+						name="ProcessId" type="xsd:string" />
+				</xsd:sequence>
+			</xsd:sequence>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="UserIdentity">
+		<xsd:complexType>
+			<xsd:sequence>
+				<xsd:element minOccurs="0" name="LocalUserId" type="xsd:string" />
+				<xsd:element minOccurs="0" name="GlobalUserName" type="xsd:string" />
+				<xsd:element minOccurs="0" ref="ds:KeyInfo" />
+			</xsd:sequence>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="RecordIdentity">
+		<xsd:complexType>
+			<xsd:sequence minOccurs="0">
+				<xsd:element ref="ds:KeyInfo" />
+			</xsd:sequence>
+			<xsd:attribute name="recordId" type="xsd:token" use="required" />
+			<xsd:attribute name="createTime" type="xsd:dateTime" use="optional" />
+		</xsd:complexType>
+	</xsd:element>
+	<!-- Extensibility Framework -->
+	<xsd:element name="Resource" type="urf:ResourceType" />
+	<xsd:element name="ConsumableResource" type="urf:ConsumableResourceType" />
+	<xsd:element name="PhaseResource">
+		<xsd:complexType>
+			<xsd:complexContent>
+				<xsd:extension base="urf:ConsumableResourceType">
+					<xsd:attribute ref="urf:phaseUnit" use="optional" />
+				</xsd:extension>
+			</xsd:complexContent>
+		</xsd:complexType>
+	</xsd:element>
+	<xsd:element name="VolumeResource">
+		<xsd:complexType>
+			<xsd:complexContent>
+				<xsd:extension base="urf:ConsumableResourceType">
+					<xsd:attribute ref="urf:storageUnit" use="optional" />
+				</xsd:extension>
+			</xsd:complexContent>
+		</xsd:complexType>
+	</xsd:element>
+	<!-- Create a generic consumable resource. Carries the units attribute -->
+	<xsd:complexType name="ConsumableResourceType">
+		<xsd:simpleContent>
+			<xsd:extension base="xsd:float">
+				<xsd:attribute name="units" type="xsd:string" use="optional" />
+				<xsd:attribute ref="urf:description" use="optional" />
+			</xsd:extension>
+		</xsd:simpleContent>
+	</xsd:complexType>
+	<!-- Create a generic resource type -->
+	<xsd:complexType name="ResourceType">
+		<xsd:simpleContent>
+			<xsd:extension base="xsd:string">
+				<xsd:attribute ref="urf:description" use="optional" />
+			</xsd:extension>
+		</xsd:simpleContent>
+	</xsd:complexType>
+	<!-- Global Attribute Definitions -->
+	<xsd:attribute name="description" type="xsd:string" />
+	<!-- Units of measure attribute definitions -->
+	<xsd:attribute name="unit" type="xsd:token" />
+	<xsd:attribute name="storageUnit">
+		<xsd:simpleType>
+			<xsd:restriction base="xsd:token">
+				<xsd:enumeration value="b" />
+				<xsd:enumeration value="B" />
+				<xsd:enumeration value="KB" />
+				<xsd:enumeration value="MB" />
+				<xsd:enumeration value="GB" />
+				<xsd:enumeration value="PB" />
+				<xsd:enumeration value="EB" />
+				<xsd:enumeration value="Kb" />
+				<xsd:enumeration value="Mb" />
+				<xsd:enumeration value="Gb" />
+				<xsd:enumeration value="Pb" />
+				<xsd:enumeration value="Eb" />
+			</xsd:restriction>
+		</xsd:simpleType>
+	</xsd:attribute>
+	<xsd:attribute name="phaseUnit" type="xsd:duration" />
+	<xsd:attributeGroup name="intervallicVolume">
+		<xsd:attribute ref="urf:storageUnit" use="optional" />
+		<xsd:attribute ref="urf:phaseUnit" use="optional" />
+	</xsd:attributeGroup>
+	<!-- End units attributes -->
+	<xsd:attribute name="metric" type="xsd:token" />
+	<xsd:attribute name="type" type="xsd:token" />
+	<!-- Simple type definitions used to constrain values of attributes -->
+	<xsd:simpleType name="domainNameType">
+		<xsd:restriction base="xsd:string">
+			<xsd:pattern
+				value="([a-zA-Z0-9][a-zA-Z0-9'\-']*[a-zA-Z0-9]\.)*([a-zA-Z0-9][a-zA-Z0-9'\-']*[a-zA-Z0-9])?" />
+			<xsd:maxLength value="255" />
+		</xsd:restriction>
+	</xsd:simpleType>
+</xsd:schema> 
+  
\ No newline at end of file
diff --git a/server-usagerecord/src/main/xsd/xmlds.xsd b/server-usagerecord/src/main/xsd/xmlds.xsd
new file mode 100644
index 0000000..c3421c4
--- /dev/null
+++ b/server-usagerecord/src/main/xsd/xmlds.xsd
@@ -0,0 +1,318 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE schema
+  PUBLIC "-//W3C//DTD XMLSchema 200102//EN" "http://www.w3.org/2001/XMLSchema.dtd"
+ [
+   <!ATTLIST schema 
+     xmlns:ds CDATA #FIXED "http://www.w3.org/2000/09/xmldsig#">
+   <!ENTITY dsig 'http://www.w3.org/2000/09/xmldsig#'>
+   <!ENTITY % p ''>
+   <!ENTITY % s ''>
+  ]>
+
+<!-- Schema for XML Signatures
+    http://www.w3.org/2000/09/xmldsig#
+    $Revision: 1.1 $ on $Date: 2002/02/08 20:32:26 $ by $Author: reagle $
+
+    Copyright 2001 The Internet Society and W3C (Massachusetts Institute
+    of Technology, Institut National de Recherche en Informatique et en
+    Automatique, Keio University). All Rights Reserved.
+    http://www.w3.org/Consortium/Legal/
+
+    This document is governed by the W3C Software License [1] as described
+    in the FAQ [2].
+
+    [1] http://www.w3.org/Consortium/Legal/copyright-software-19980720
+    [2] http://www.w3.org/Consortium/Legal/IPR-FAQ-20000620.html#DTD
+-->
+
+
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+        xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+        targetNamespace="http://www.w3.org/2000/09/xmldsig#"
+        version="0.1" elementFormDefault="qualified"> 
+
+<!-- Basic Types Defined for Signatures -->
+
+<simpleType name="CryptoBinary">
+  <restriction base="base64Binary">
+  </restriction>
+</simpleType>
+
+<!-- Start Signature -->
+
+<element name="Signature" type="ds:SignatureType"/>
+<complexType name="SignatureType">
+  <sequence> 
+    <element ref="ds:SignedInfo"/> 
+    <element ref="ds:SignatureValue"/> 
+    <element ref="ds:KeyInfo" minOccurs="0"/> 
+    <element ref="ds:Object" minOccurs="0" maxOccurs="unbounded"/> 
+  </sequence>  
+  <attribute name="Id" type="ID" use="optional"/>
+</complexType>
+
+  <element name="SignatureValue" type="ds:SignatureValueType"/> 
+  <complexType name="SignatureValueType">
+    <simpleContent>
+      <extension base="base64Binary">
+        <attribute name="Id" type="ID" use="optional"/>
+      </extension>
+    </simpleContent>
+  </complexType>
+
+<!-- Start SignedInfo -->
+
+<element name="SignedInfo" type="ds:SignedInfoType"/>
+<complexType name="SignedInfoType">
+  <sequence> 
+    <element ref="ds:CanonicalizationMethod"/> 
+    <element ref="ds:SignatureMethod"/> 
+    <element ref="ds:Reference" maxOccurs="unbounded"/> 
+  </sequence>  
+  <attribute name="Id" type="ID" use="optional"/> 
+</complexType>
+
+  <element name="CanonicalizationMethod" type="ds:CanonicalizationMethodType"/> 
+  <complexType name="CanonicalizationMethodType" mixed="true">
+    <sequence>
+      <any namespace="##any" minOccurs="0" maxOccurs="unbounded"/>
+      <!-- (0,unbounded) elements from (1,1) namespace -->
+    </sequence>
+    <attribute name="Algorithm" type="anyURI" use="required"/> 
+  </complexType>
+
+  <element name="SignatureMethod" type="ds:SignatureMethodType"/>
+  <complexType name="SignatureMethodType" mixed="true">
+    <sequence>
+      <element name="HMACOutputLength" minOccurs="0" type="ds:HMACOutputLengthType"/>
+      <any namespace="##other" minOccurs="0" maxOccurs="unbounded"/>
+      <!-- (0,unbounded) elements from (1,1) external namespace -->
+    </sequence>
+    <attribute name="Algorithm" type="anyURI" use="required"/> 
+  </complexType>
+
+<!-- Start Reference -->
+
+<element name="Reference" type="ds:ReferenceType"/>
+<complexType name="ReferenceType">
+  <sequence> 
+    <element ref="ds:Transforms" minOccurs="0"/> 
+    <element ref="ds:DigestMethod"/> 
+    <element ref="ds:DigestValue"/> 
+  </sequence>
+  <attribute name="Id" type="ID" use="optional"/> 
+  <attribute name="URI" type="anyURI" use="optional"/> 
+  <attribute name="Type" type="anyURI" use="optional"/> 
+</complexType>
+
+  <element name="Transforms" type="ds:TransformsType"/>
+  <complexType name="TransformsType">
+    <sequence>
+      <element ref="ds:Transform" maxOccurs="unbounded"/>  
+    </sequence>
+  </complexType>
+
+  <element name="Transform" type="ds:TransformType"/>
+  <complexType name="TransformType" mixed="true">
+    <choice minOccurs="0" maxOccurs="unbounded"> 
+      <any namespace="##other" processContents="lax"/>
+      <!-- (1,1) elements from (0,unbounded) namespaces -->
+      <element name="XPath" type="string"/> 
+    </choice>
+    <attribute name="Algorithm" type="anyURI" use="required"/> 
+  </complexType>
+
+<!-- End Reference -->
+
+<element name="DigestMethod" type="ds:DigestMethodType"/>
+<complexType name="DigestMethodType" mixed="true"> 
+  <sequence>
+    <any namespace="##other" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>
+  </sequence>    
+  <attribute name="Algorithm" type="anyURI" use="required"/> 
+</complexType>
+
+<element name="DigestValue" type="ds:DigestValueType"/>
+<simpleType name="DigestValueType">
+  <restriction base="base64Binary"/>
+</simpleType>
+
+<!-- End SignedInfo -->
+
+<!-- Start KeyInfo -->
+
+<element name="KeyInfo" type="ds:KeyInfoType"/> 
+<complexType name="KeyInfoType" mixed="true">
+  <choice maxOccurs="unbounded">     
+    <element ref="ds:KeyName"/> 
+    <element ref="ds:KeyValue"/> 
+    <element ref="ds:RetrievalMethod"/> 
+    <element ref="ds:X509Data"/> 
+    <element ref="ds:PGPData"/> 
+    <element ref="ds:SPKIData"/>
+    <element ref="ds:MgmtData"/>
+    <any processContents="lax" namespace="##other"/>
+    <!-- (1,1) elements from (0,unbounded) namespaces -->
+  </choice>
+  <attribute name="Id" type="ID" use="optional"/> 
+</complexType>
+
+  <element name="KeyName" type="string"/>
+  <element name="MgmtData" type="string"/>
+
+  <element name="KeyValue" type="ds:KeyValueType"/> 
+  <complexType name="KeyValueType" mixed="true">
+   <choice>
+     <element ref="ds:DSAKeyValue"/>
+     <element ref="ds:RSAKeyValue"/>
+     <any namespace="##other" processContents="lax"/>
+   </choice>
+  </complexType>
+
+  <element name="RetrievalMethod" type="ds:RetrievalMethodType"/> 
+  <complexType name="RetrievalMethodType">
+    <sequence>
+      <element ref="ds:Transforms" minOccurs="0"/> 
+    </sequence>  
+    <attribute name="URI" type="anyURI"/>
+    <attribute name="Type" type="anyURI" use="optional"/>
+  </complexType>
+
+<!-- Start X509Data -->
+
+<element name="X509Data" type="ds:X509DataType"/> 
+<complexType name="X509DataType">
+  <sequence maxOccurs="unbounded">
+    <choice>
+      <element name="X509IssuerSerial" type="ds:X509IssuerSerialType"/>
+      <element name="X509SKI" type="base64Binary"/>
+      <element name="X509SubjectName" type="string"/>
+      <element name="X509Certificate" type="base64Binary"/>
+      <element name="X509CRL" type="base64Binary"/>
+      <any namespace="##other" processContents="lax"/>
+    </choice>
+  </sequence>
+</complexType>
+
+<complexType name="X509IssuerSerialType"> 
+  <sequence> 
+    <element name="X509IssuerName" type="string"/> 
+    <element name="X509SerialNumber" type="integer"/> 
+  </sequence>
+</complexType>
+
+<!-- End X509Data -->
+
+<!-- Begin PGPData -->
+
+<element name="PGPData" type="ds:PGPDataType"/> 
+<complexType name="PGPDataType"> 
+  <choice>
+    <sequence>
+      <element name="PGPKeyID" type="base64Binary"/> 
+      <element name="PGPKeyPacket" type="base64Binary" minOccurs="0"/> 
+      <any namespace="##other" processContents="lax" minOccurs="0"
+       maxOccurs="unbounded"/>
+    </sequence>
+    <sequence>
+      <element name="PGPKeyPacket" type="base64Binary"/> 
+      <any namespace="##other" processContents="lax" minOccurs="0"
+       maxOccurs="unbounded"/>
+    </sequence>
+  </choice>
+</complexType>
+
+<!-- End PGPData -->
+
+<!-- Begin SPKIData -->
+
+<element name="SPKIData" type="ds:SPKIDataType"/> 
+<complexType name="SPKIDataType">
+  <sequence maxOccurs="unbounded">
+    <element name="SPKISexp" type="base64Binary"/>
+    <any namespace="##other" processContents="lax" minOccurs="0"/>
+  </sequence>
+</complexType> 
+
+<!-- End SPKIData -->
+
+<!-- End KeyInfo -->
+
+<!-- Start Object (Manifest, SignatureProperty) -->
+
+<element name="Object" type="ds:ObjectType"/> 
+<complexType name="ObjectType" mixed="true">
+  <sequence minOccurs="0" maxOccurs="unbounded">
+    <any namespace="##any" processContents="lax"/>
+  </sequence>
+  <attribute name="Id" type="ID" use="optional"/> 
+  <attribute name="MimeType" type="string" use="optional"/> <!-- add a grep facet -->
+  <attribute name="Encoding" type="anyURI" use="optional"/> 
+</complexType>
+
+<element name="Manifest" type="ds:ManifestType"/> 
+<complexType name="ManifestType">
+  <sequence>
+    <element ref="ds:Reference" maxOccurs="unbounded"/> 
+  </sequence>
+  <attribute name="Id" type="ID" use="optional"/> 
+</complexType>
+
+<element name="SignatureProperties" type="ds:SignaturePropertiesType"/> 
+<complexType name="SignaturePropertiesType">
+  <sequence>
+    <element ref="ds:SignatureProperty" maxOccurs="unbounded"/> 
+  </sequence>
+  <attribute name="Id" type="ID" use="optional"/> 
+</complexType>
+
+   <element name="SignatureProperty" type="ds:SignaturePropertyType"/> 
+   <complexType name="SignaturePropertyType" mixed="true">
+     <choice maxOccurs="unbounded">
+       <any namespace="##other" processContents="lax"/>
+       <!-- (1,1) elements from (1,unbounded) namespaces -->
+     </choice>
+     <attribute name="Target" type="anyURI" use="required"/> 
+     <attribute name="Id" type="ID" use="optional"/> 
+   </complexType>
+
+<!-- End Object (Manifest, SignatureProperty) -->
+
+<!-- Start Algorithm Parameters -->
+
+<simpleType name="HMACOutputLengthType">
+  <restriction base="integer"/>
+</simpleType>
+
+<!-- Start KeyValue Element-types -->
+
+<element name="DSAKeyValue" type="ds:DSAKeyValueType"/>
+<complexType name="DSAKeyValueType">
+  <sequence>
+    <sequence minOccurs="0">
+      <element name="P" type="ds:CryptoBinary"/>
+      <element name="Q" type="ds:CryptoBinary"/>
+    </sequence>
+    <element name="G" type="ds:CryptoBinary" minOccurs="0"/>
+    <element name="Y" type="ds:CryptoBinary"/>
+    <element name="J" type="ds:CryptoBinary" minOccurs="0"/>
+    <sequence minOccurs="0">
+      <element name="Seed" type="ds:CryptoBinary"/>
+      <element name="PgenCounter" type="ds:CryptoBinary"/>
+    </sequence>
+  </sequence>
+</complexType>
+
+<element name="RSAKeyValue" type="ds:RSAKeyValueType"/>
+<complexType name="RSAKeyValueType">
+  <sequence>
+    <element name="Modulus" type="ds:CryptoBinary"/> 
+    <element name="Exponent" type="ds:CryptoBinary"/> 
+  </sequence>
+</complexType> 
+
+<!-- End KeyValue Element-types -->
+
+<!-- End Signature -->
+
+</schema>
\ No newline at end of file
diff --git a/server-usagerecord/src/test/java/TestUR.java b/server-usagerecord/src/test/java/TestUR.java
new file mode 100644
index 0000000..42e9000
--- /dev/null
+++ b/server-usagerecord/src/test/java/TestUR.java
@@ -0,0 +1,120 @@
+import static java.lang.Runtime.getRuntime;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.JAXBIntrospector;
+import javax.xml.bind.SchemaOutputResolver;
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.transform.Result;
+import javax.xml.transform.stream.StreamResult;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.ogf.usage.JobUsageRecord;
+import org.ogf.usage.v1_0.Charge;
+import org.ogf.usage.v1_0.CpuDuration;
+import org.ogf.usage.v1_0.Disk;
+import org.ogf.usage.v1_0.EndTime;
+import org.ogf.usage.v1_0.Host;
+import org.ogf.usage.v1_0.JobIdentity;
+import org.ogf.usage.v1_0.JobName;
+import org.ogf.usage.v1_0.MachineName;
+import org.ogf.usage.v1_0.Memory;
+import org.ogf.usage.v1_0.Network;
+import org.ogf.usage.v1_0.NodeCount;
+import org.ogf.usage.v1_0.PhaseResource;
+import org.ogf.usage.v1_0.Processors;
+import org.ogf.usage.v1_0.ProjectName;
+import org.ogf.usage.v1_0.Queue;
+import org.ogf.usage.v1_0.RecordIdentity;
+import org.ogf.usage.v1_0.ServiceLevel;
+import org.ogf.usage.v1_0.StartTime;
+import org.ogf.usage.v1_0.Status;
+import org.ogf.usage.v1_0.SubmitHost;
+import org.ogf.usage.v1_0.Swap;
+import org.ogf.usage.v1_0.TimeDuration;
+import org.ogf.usage.v1_0.TimeInstant;
+import org.ogf.usage.v1_0.UserIdentity;
+import org.ogf.usage.v1_0.VolumeResource;
+import org.ogf.usage.v1_0.WallDuration;
+
+public class TestUR {
+	SchemaOutputResolver sink;
+	StringWriter writer;
+
+	String result() {
+		return writer.toString();
+	}
+
+	@Before
+	public void setUp() throws Exception {
+		writer = new StringWriter();
+		sink = new SchemaOutputResolver() {
+			@Override
+			public Result createOutput(String namespaceUri,
+					String suggestedFileName) throws IOException {
+				StreamResult sr = new StreamResult(writer);
+				sr.setSystemId("/dev/null");
+				return sr;
+			}
+		};
+		Assert.assertNull(null);// Shut up, Eclipse!
+		Assert.assertEquals("", result());
+	}
+
+	@Test
+	public void testSchema() throws JAXBException, IOException {
+		JAXBContext.newInstance(JobUsageRecord.class).generateSchema(sink);
+		Assert.assertNotSame("", result());
+	}
+
+	@Test
+	public void testSchemaCompleteness() throws JAXBException, DatatypeConfigurationException {
+		JAXBIntrospector info = JAXBContext.newInstance(JobUsageRecord.class).createJAXBIntrospector();
+		Assert.assertTrue(info.isElement(new Charge()));
+		Assert.assertTrue(info.isElement(new CpuDuration()));
+		Assert.assertTrue(info.isElement(new Disk()));
+		Assert.assertTrue(info.isElement(new EndTime()));
+		Assert.assertTrue(info.isElement(new Host()));
+		Assert.assertTrue(info.isElement(new JobIdentity()));
+		Assert.assertTrue(info.isElement(new JobName()));
+		Assert.assertTrue(info.isElement(new JobUsageRecord()));
+		Assert.assertTrue(info.isElement(new MachineName()));
+		Assert.assertTrue(info.isElement(new Memory()));
+		Assert.assertTrue(info.isElement(new Network()));
+		Assert.assertTrue(info.isElement(new NodeCount()));
+		Assert.assertTrue(info.isElement(new PhaseResource()));
+		Assert.assertTrue(info.isElement(new Processors()));
+		Assert.assertTrue(info.isElement(new ProjectName()));
+		Assert.assertTrue(info.isElement(new Queue()));
+		Assert.assertTrue(info.isElement(new RecordIdentity()));
+		Assert.assertTrue(info.isElement(new ServiceLevel()));
+		Assert.assertTrue(info.isElement(new StartTime()));
+		Assert.assertTrue(info.isElement(new Status()));
+		Assert.assertTrue(info.isElement(new SubmitHost()));
+		Assert.assertTrue(info.isElement(new Swap()));
+		Assert.assertTrue(info.isElement(new TimeDuration()));
+		Assert.assertTrue(info.isElement(new TimeInstant()));
+		Assert.assertTrue(info.isElement(new UserIdentity()));
+		Assert.assertTrue(info.isElement(new VolumeResource()));
+		Assert.assertTrue(info.isElement(new WallDuration()));
+	}
+
+	@Test
+	public void testGenerate() throws DatatypeConfigurationException,
+			JAXBException {
+		JobUsageRecord ur = new JobUsageRecord();
+		ur.setStatus("Completed");
+		ur.addWallDuration(1000 * 65);
+		ur.addHost("localhost");
+		ur.addMemory(getRuntime().totalMemory() - getRuntime().freeMemory()).setType("vm");
+
+		String record = ur.marshal();
+		Assert.assertNotSame("", record);
+		//System.out.println(record);
+	}
+}
diff --git a/server-webapp/.gitignore b/server-webapp/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/server-webapp/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/server-webapp/.springBeans b/server-webapp/.springBeans
new file mode 100644
index 0000000..0b2cbbc
--- /dev/null
+++ b/server-webapp/.springBeans
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beansProjectDescription>
+	<version>1</version>
+	<pluginVersion><![CDATA[3.3.0.201307091516-RELEASE]]></pluginVersion>
+	<configSuffixes>
+		<configSuffix><![CDATA[xml]]></configSuffix>
+	</configSuffixes>
+	<enableImports><![CDATA[true]]></enableImports>
+	<configs>
+		<config>src/main/webapp/WEB-INF/beans.xml</config>
+		<config>src/test/resources/example.xml</config>
+		<config>src/main/webapp/WEB-INF/insecure.xml</config>
+		<config>src/main/webapp/WEB-INF/providers.xml</config>
+		<config>src/main/webapp/WEB-INF/secure.xml</config>
+		<config>src/main/webapp/WEB-INF/webappBeans.xml</config>
+		<config>src/main/webapp/WEB-INF/partsecure.xml</config>
+	</configs>
+	<configSets>
+		<configSet>
+			<name><![CDATA[Secure Configuration]]></name>
+			<allowBeanDefinitionOverriding>true</allowBeanDefinitionOverriding>
+			<incomplete>false</incomplete>
+			<configs>
+				<config>src/main/webapp/WEB-INF/beans.xml</config>
+				<config>src/main/webapp/WEB-INF/providers.xml</config>
+				<config>src/main/webapp/WEB-INF/secure.xml</config>
+				<config>src/main/webapp/WEB-INF/webappBeans.xml</config>
+			</configs>
+			<profiles>
+			</profiles>
+		</configSet>
+		<configSet>
+			<name><![CDATA[Insecure Configuration]]></name>
+			<allowBeanDefinitionOverriding>true</allowBeanDefinitionOverriding>
+			<incomplete>false</incomplete>
+			<configs>
+				<config>src/main/webapp/WEB-INF/beans.xml</config>
+				<config>src/main/webapp/WEB-INF/insecure.xml</config>
+				<config>src/main/webapp/WEB-INF/providers.xml</config>
+				<config>src/main/webapp/WEB-INF/webappBeans.xml</config>
+			</configs>
+			<profiles>
+			</profiles>
+		</configSet>
+		<configSet>
+			<name><![CDATA[Semi-Secure Configuration]]></name>
+			<allowBeanDefinitionOverriding>true</allowBeanDefinitionOverriding>
+			<incomplete>false</incomplete>
+			<configs>
+				<config>src/main/webapp/WEB-INF/beans.xml</config>
+				<config>src/main/webapp/WEB-INF/providers.xml</config>
+				<config>src/main/webapp/WEB-INF/webappBeans.xml</config>
+				<config>src/main/webapp/WEB-INF/partsecure.xml</config>
+			</configs>
+			<profiles>
+			</profiles>
+		</configSet>
+	</configSets>
+</beansProjectDescription>
diff --git a/server-webapp/pom.xml b/server-webapp/pom.xml
new file mode 100644
index 0000000..15f1eb4
--- /dev/null
+++ b/server-webapp/pom.xml
@@ -0,0 +1,881 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<artifactId>server-webapp</artifactId>
+	<packaging>war</packaging>
+	<name>Web Application Core</name>
+	<description>This is the implementation of the web-app that provides the Taverna Server with its SOAP and REST faces. It relies on the worker process to handle the actual launching of workflow runs.</description>
+	<parent>
+		<groupId>uk.org.taverna.server</groupId>
+		<artifactId>server</artifactId>
+		<version>3.0-SNAPSHOT</version>
+		<relativePath>..</relativePath>
+	</parent>
+	<scm>
+		<url>${scmBrowseRoot}/server-webapp</url>
+	</scm>
+
+	<properties>
+		<version.cxf>2.7.7</version.cxf>
+		<version.spring>3.2.5.RELEASE</version.spring>
+		<version.spring-security>3.1.4.RELEASE</version.spring-security>
+		<version.asm>3.3.1</version.asm>
+		<version.smack>3.2.1</version.smack>
+		<!--<version.commandline>3.0.1-SNAPSHOT</version.commandline>-->
+		<version.commandline>3.0.0</version.commandline>
+		<edition.commandline>enterprise</edition.commandline>
+		<version.jdoapi>3.0.1</version.jdoapi>
+		<forker.module>server-unix-forker</forker.module>
+		<util.dir>${project.build.directory}/${project.build.finalName}/WEB-INF/classes/util</util.dir>
+		<scufl2.version>0.9.2</scufl2.version>
+		<cmdline.dir>${util.dir}/taverna-commandline-${edition.commandline}-${version.commandline}</cmdline.dir>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.apache.cxf</groupId>
+			<artifactId>cxf-rt-frontend-jaxws</artifactId>
+			<version>${version.cxf}</version>
+			<exclusions>
+				<exclusion>
+					<artifactId>jaxb-impl</artifactId>
+					<groupId>com.sun.xml.bind</groupId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.cxf</groupId>
+			<artifactId>cxf-rt-frontend-jaxrs</artifactId>
+			<version>${version.cxf}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.cxf</groupId>
+			<artifactId>cxf-rt-transports-http</artifactId>
+			<version>${version.cxf}</version>
+		</dependency>
+		<dependency>
+			<groupId>commons-logging</groupId>
+			<artifactId>commons-logging</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-aop</artifactId>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.security</groupId>
+			<artifactId>spring-security-core</artifactId>
+			<version>${version.spring-security}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.security</groupId>
+			<artifactId>spring-security-web</artifactId>
+			<version>${version.spring-security}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.security</groupId>
+			<artifactId>spring-security-config</artifactId>
+			<version>${version.spring-security}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-test</artifactId>
+			<version>${version.spring}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId><!--$NO-MVN-MAN-VER$-->
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-port-description</artifactId>
+			<version>${project.parent.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-runinterface</artifactId>
+			<scope>compile</scope>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-usagerecord</artifactId>
+			<version>${project.parent.version}</version>
+			<scope>compile</scope>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-worker</artifactId>
+			<version>${project.parent.version}</version>
+			<classifier>jar-with-dependencies</classifier>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>${forker.module}</artifactId>
+			<version>${project.parent.version}</version>
+			<classifier>jar-with-dependencies</classifier>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-rmidaemon</artifactId>
+			<version>${project.parent.version}</version>
+			<classifier>jar-with-dependencies</classifier>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>servlet-api</artifactId>
+			<version>2.5</version>
+			<scope>provided</scope>
+		</dependency>
+		<!--
+		<dependency>
+			<groupId>org.hibernate</groupId>
+			<artifactId>hibernate-annotations</artifactId>
+			<version>3.4.0.GA</version>
+			<scope>compile</scope>
+		</dependency>
+		-->
+		<!--
+		<dependency>
+			<groupId>org.hibernate</groupId>
+			<artifactId>hibernate-validator</artifactId>
+			<version>4.0.2.GA</version>
+			<scope>compile</scope>
+		</dependency>
+		-->
+		<dependency>
+			<groupId>org.apache.derby</groupId>
+			<artifactId>derby</artifactId>
+			<version>10.10.1.1</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>commons-dbcp</groupId>
+			<artifactId>commons-dbcp</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>javax.jdo</groupId>
+			<artifactId>jdo-api</artifactId>
+			<version>3.0.1</version>
+		</dependency>
+		<dependency>
+			<groupId>org.datanucleus</groupId>
+			<artifactId>datanucleus-api-jdo</artifactId>
+			<version>[3.2.0, 3.2.99)</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.datanucleus</groupId>
+			<artifactId>datanucleus-core</artifactId>
+			<version>3.2.10</version>
+		</dependency>
+		<dependency>
+			<groupId>org.datanucleus</groupId>
+			<artifactId>datanucleus-rdbms</artifactId>
+			<version>[3.2.0, 3.2.99)</version>
+			<scope>compile</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.jdo</groupId>
+			<artifactId>jdo2-core</artifactId>
+			<version>2.0</version>
+		</dependency>
+		<dependency>
+			<groupId>joda-time</groupId>
+			<artifactId>joda-time</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>net.sf.mime-util</groupId>
+			<artifactId>mime-util</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.twitter4j</groupId>
+			<artifactId>twitter4j-core</artifactId>
+			<version>[3.0,)</version>
+		</dependency>
+		<dependency>
+			<groupId>cglib</groupId>
+			<artifactId>cglib</artifactId>
+			<version>3.0</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.aspectj</groupId>
+			<artifactId>aspectjrt</artifactId>
+			<version>1.7.4</version>
+		</dependency>
+		<dependency>
+			<groupId>org.igniterealtime.smack</groupId>
+			<artifactId>smack</artifactId>
+			<version>${version.smack}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.igniterealtime.smack</groupId>
+			<artifactId>smackx</artifactId>
+			<version>${version.smack}</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.httpcomponents</groupId>
+			<artifactId>httpclient</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>javax.annotation</groupId>
+			<artifactId>jsr250-api</artifactId>
+			<version>1.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.abdera</groupId>
+			<artifactId>abdera-core</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.abdera</groupId>
+			<artifactId>abdera-parser</artifactId>
+			<version>1.1.3</version>
+			<scope>runtime</scope>
+			<exclusions>
+				<exclusion>
+					<artifactId>xercesImpl</artifactId>
+					<groupId>xerces</groupId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+		<dependency>
+			<groupId>org.bouncycastle</groupId>
+			<artifactId>bcprov-jdk15on</artifactId>
+			<version>1.49</version>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-context-support</artifactId>
+			<version>${version.spring}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.aspectj</groupId>
+			<artifactId>aspectjweaver</artifactId>
+			<version>1.7.4</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>javax.mail</groupId>
+			<artifactId>mail</artifactId>
+			<version>1.4.4</version>
+			<scope>runtime</scope>
+			<exclusions>
+				<exclusion>
+					<groupId>javax.activation</groupId>
+					<artifactId>activation</artifactId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.velocity</groupId>
+			<artifactId>velocity</artifactId>
+			<version>1.7</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.cxf</groupId>
+			<artifactId>cxf-rt-rs-extension-providers</artifactId>
+			<version>${version.cxf}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.codehaus.jettison</groupId>
+			<artifactId>jettison</artifactId>
+			<version>1.3.4</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.scufl2</groupId>
+			<artifactId>scufl2-api</artifactId>
+			<version>${scufl2.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.scufl2</groupId>
+			<artifactId>scufl2-t2flow</artifactId>
+			<version>${scufl2.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.scufl2</groupId>
+			<artifactId>scufl2-rdfxml</artifactId>
+			<version>${scufl2.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.commons</groupId>
+			<artifactId>commons-compress</artifactId>
+			<version>1.4.1</version>
+		</dependency>
+	</dependencies>
+
+	<dependencyManagement>
+		<dependencies>
+			<dependency>
+				<groupId>uk.org.taverna.server</groupId>
+				<artifactId>server-runinterface</artifactId>
+				<version>${project.parent.version}</version>
+				<scope>compile</scope>
+			</dependency>
+			<dependency>
+				<groupId>asm</groupId>
+				<artifactId>asm</artifactId>
+				<version>${version.asm}</version>
+				<scope>runtime</scope>
+			</dependency>
+			<dependency>
+				<groupId>com.sun.xml.bind</groupId>
+				<artifactId>jaxb-impl</artifactId>
+				<version>2.2.7</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-aop</artifactId>
+				<version>${version.spring}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-core</artifactId>
+				<version>${version.spring}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-beans</artifactId>
+				<version>${version.spring}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-web</artifactId>
+				<version>${version.spring}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-jdbc</artifactId>
+				<version>${version.spring}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-tx</artifactId>
+				<version>${version.spring}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-context</artifactId>
+				<version>${version.spring}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.springframework</groupId>
+				<artifactId>spring-expression</artifactId>
+				<version>${version.spring}</version>
+			</dependency>
+			<dependency>
+				<groupId>org.apache.abdera</groupId>
+				<artifactId>abdera-core</artifactId>
+				<version>1.1.3</version>
+			</dependency>
+			<dependency>
+				<groupId>org.codehaus.woodstox</groupId>
+				<artifactId>wstx-asl</artifactId>
+				<version>4.0.6</version>
+			</dependency>
+			<dependency>
+				<groupId>org.apache.geronimo.specs</groupId>
+				<artifactId>geronimo-javamail_1.4_spec</artifactId>
+				<version>1.7.1</version>
+				<scope>provided</scope>
+			</dependency>
+			<dependency>
+				<groupId>org.codehaus.woodstox</groupId>
+				<artifactId>woodstox-core-asl</artifactId>
+				<version>4.2.0</version>
+				<scope>runtime</scope>
+			</dependency>
+			<dependency>
+				<groupId>jaxen</groupId>
+				<artifactId>jaxen</artifactId>
+				<version>1.1.4</version>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
+	<build>
+		<finalName>TavernaServer.${project.parent.version}</finalName>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>org.codehaus.mojo</groupId>
+					<artifactId>tomcat-maven-plugin</artifactId>
+					<version>1.1</version>
+					<configuration>
+						<server>deployhost</server>
+						<path>/taverna-server</path>
+					</configuration>
+				</plugin>
+				<plugin>
+					<groupId>org.apache.maven.plugins</groupId>
+					<artifactId>maven-compiler-plugin</artifactId>
+					<configuration>
+						<encoding>US-ASCII</encoding>
+						<source>1.7</source>
+						<target>1.7</target>
+					</configuration>
+				</plugin>
+				<plugin>
+					<groupId>org.datanucleus</groupId>
+					<artifactId>datanucleus-maven-plugin</artifactId>
+					<version>3.3.0-release</version>
+					<configuration>
+						<jdkLogConfiguration>${project.basedir}/src/build/resources/datanucleus-log.properties</jdkLogConfiguration>
+						<log4jConfiguration>${project.basedir}/src/build/resources/datanucleus_log4j.properties</log4jConfiguration>
+						<verbose>true</verbose>
+					</configuration>
+					<dependencies>
+						<!-- Sucks that I have to say these explicitly -->
+						<dependency>
+							<groupId>org.datanucleus</groupId>
+							<artifactId>datanucleus-core</artifactId>
+							<version>3.2.10</version>
+						</dependency>
+						<dependency>
+							<groupId>org.datanucleus</groupId>
+							<artifactId>datanucleus-enhancer</artifactId>
+							<version>3.1.1</version>
+						</dependency>
+						<dependency>
+							<groupId>org.datanucleus</groupId>
+							<artifactId>datanucleus-api-jdo</artifactId>
+							<version>3.2.5</version>
+						</dependency>
+						<dependency>
+							<groupId>javax.jdo</groupId>
+							<artifactId>jdo-api</artifactId>
+							<version>${version.jdoapi}</version>
+						</dependency>
+					</dependencies>
+				</plugin>
+				<!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+				<plugin>
+					<groupId>org.eclipse.m2e</groupId>
+					<artifactId>lifecycle-mapping</artifactId>
+					<version>1.0.0</version>
+					<configuration>
+						<lifecycleMappingMetadata>
+							<pluginExecutions>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>org.apache.maven.plugins</groupId>
+										<artifactId>maven-dependency-plugin</artifactId>
+										<versionRange>[2.0,)</versionRange>
+										<goals>
+											<goal>copy-dependencies</goal>
+											<goal>unpack</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<ignore />
+									</action>
+								</pluginExecution>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>org.datanucleus</groupId>
+										<artifactId>datanucleus-maven-plugin</artifactId>
+										<versionRange>3.3.0-release</versionRange>
+										<goals>
+											<goal>enhance</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<execute />
+									</action>
+								</pluginExecution>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>pl.project13.maven</groupId>
+										<artifactId>git-commit-id-plugin</artifactId>
+										<versionRange>[2.1.4,)</versionRange>
+										<goals>
+											<goal>revision</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<execute />
+									</action>
+								</pluginExecution>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>org.datanucleus</groupId>
+										<artifactId>datanucleus-maven-plugin</artifactId>
+										<versionRange>3.3.0-release</versionRange>
+										<goals>
+											<goal>schema-create</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<ignore />
+									</action>
+								</pluginExecution>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>net.alchim31.maven</groupId>
+										<artifactId>yuicompressor-maven-plugin</artifactId>
+										<versionRange>[1.0.0,)</versionRange>
+										<goals>
+											<goal>compress</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<execute/>
+									</action>
+								</pluginExecution>
+							</pluginExecutions>
+						</lifecycleMappingMetadata>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+
+		<defaultGoal>package</defaultGoal>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-dependency-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>copy-executable-library-jars</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>copy</goal>
+						</goals>
+						<configuration>
+							<artifactItems>
+								<artifactItem>
+									<groupId>uk.org.taverna.server</groupId>
+									<artifactId>server-worker</artifactId>
+									<version>${project.parent.version}</version>
+									<classifier>jar-with-dependencies</classifier>
+									<overWrite>false</overWrite>
+									<destFileName>server.worker.jar</destFileName>
+								</artifactItem>
+								<artifactItem>
+									<groupId>uk.org.taverna.server</groupId>
+									<artifactId>${forker.module}</artifactId>
+									<version>${project.parent.version}</version>
+									<classifier>jar-with-dependencies</classifier>
+									<overWrite>false</overWrite>
+									<destFileName>secure.fork.jar</destFileName>
+								</artifactItem>
+								<artifactItem>
+									<groupId>uk.org.taverna.server</groupId>
+									<artifactId>server-rmidaemon</artifactId>
+									<version>${project.parent.version}</version>
+									<classifier>jar-with-dependencies</classifier>
+									<overWrite>false</overWrite>
+									<destFileName>rmi.daemon.jar</destFileName>
+								</artifactItem>
+							</artifactItems>
+							<outputDirectory>${util.dir}</outputDirectory>
+							<overWriteReleases>false</overWriteReleases>
+							<overWriteSnapshots>true</overWriteSnapshots>
+							<excludeTransitive>true</excludeTransitive>
+						</configuration>
+					</execution>
+					<execution>
+						<id>unpack-taverna-commandline</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>unpack</goal>
+						</goals>
+						<configuration>
+							<artifactItems>
+								<artifactItem>
+									<groupId>net.sf.taverna.t2.taverna-commandline</groupId>
+									<artifactId>taverna-commandline-${edition.commandline}</artifactId>
+									<version>${version.commandline}</version>
+									<classifier>bin</classifier>
+									<type>zip</type>
+									<classifier>bin</classifier>
+									<outputDirectory>${util.dir}</outputDirectory>
+								</artifactItem>
+							</artifactItems>
+							<overWriteReleases>false</overWriteReleases>
+							<overWriteSnapshots>true</overWriteSnapshots>
+							<excludeTransitive>true</excludeTransitive>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.datanucleus</groupId>
+				<artifactId>datanucleus-maven-plugin</artifactId>
+				<configuration>
+					<fork>false</fork>
+					<metadataIncludes>
+						org/taverna/server/master/*.class,
+						org/taverna/server/master/identity/*.class,
+						org/taverna/server/master/localworker/*.class,
+						org/taverna/server/master/notification/atom/*.class,
+						org/taverna/server/master/usage/*.class,
+						org/taverna/server/master/worker/*.class
+					</metadataIncludes>
+				</configuration>
+				<executions>
+					<execution>
+						<id>enhance</id>
+						<phase>process-classes</phase>
+						<goals>
+							<goal>enhance</goal>
+						</goals>
+					</execution>
+					<!--
+					<execution>
+						<id>gen-db-schema</id>
+						<phase>process-classes</phase>
+						<goals>
+							<goal>schema-create</goal>
+						</goals>
+						<configuration>
+							<completeDdl>true</completeDdl>
+							<ddlFile>${util.dir}/schema.sql</ddlFile>
+						</configuration>
+					</execution>
+					-->
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<encoding>US-ASCII</encoding>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>pl.project13.maven</groupId>
+				<artifactId>git-commit-id-plugin</artifactId>
+				<version>2.1.4</version>
+				<executions>
+					<execution>
+						<id>buildinfo</id>
+						<phase>generate-resources</phase>
+						<goals>
+							<goal>revision</goal>
+						</goals>
+					</execution>
+				</executions>
+				<configuration>
+					<dotGitDirectory>${project.basedir}/../.git</dotGitDirectory>
+					<prefix>git</prefix>
+					<verbose>true</verbose>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-war-plugin</artifactId>
+				<version>2.4</version>
+				<configuration>
+					<webXml>src/main/webapp/WEB-INF/web-sec.xml</webXml>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-eclipse-plugin</artifactId>
+				<configuration>
+					<additionalProjectnatures>
+						<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
+					</additionalProjectnatures>
+					<additionalConfig>
+						<file>
+							<name>.springBeans</name>
+							<content><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<beansProjectDescription>
+	<version>1</version>
+	<pluginVersion>3.3.0.201307091516-RELEASE</pluginVersion>
+	<configSuffixes>
+		<configSuffix>xml</configSuffix>
+	</configSuffixes>
+	<enableImports>true</enableImports>
+	<configs>
+		<config>src/main/webapp/WEB-INF/partsecure.xml</config>
+		<config>src/main/webapp/WEB-INF/beans.xml</config>
+		<config>src/main/webapp/WEB-INF/insecure.xml</config>
+		<config>src/main/webapp/WEB-INF/providers.xml</config>
+		<config>src/main/webapp/WEB-INF/secure.xml</config>
+		<config>src/main/webapp/WEB-INF/webappBeans.xml</config>
+	</configs>
+	<configSets>
+		<configSet>
+			<name>Secure Configuration</name>
+			<allowBeanDefinitionOverriding>true</allowBeanDefinitionOverriding>
+			<incomplete>false</incomplete>
+			<configs>
+				<config>src/main/webapp/WEB-INF/beans.xml</config>
+				<config>src/main/webapp/WEB-INF/providers.xml</config>
+				<config>src/main/webapp/WEB-INF/secure.xml</config>
+				<config>src/main/webapp/WEB-INF/webappBeans.xml</config>
+			</configs>
+			<profiles>
+			</profiles>
+		</configSet>
+		<configSet>
+			<name>Insecure Configuration</name>
+			<allowBeanDefinitionOverriding>true</allowBeanDefinitionOverriding>
+			<incomplete>false</incomplete>
+			<configs>
+				<config>src/main/webapp/WEB-INF/beans.xml</config>
+				<config>src/main/webapp/WEB-INF/insecure.xml</config>
+				<config>src/main/webapp/WEB-INF/providers.xml</config>
+				<config>src/main/webapp/WEB-INF/webappBeans.xml</config>
+			</configs>
+			<profiles>
+			</profiles>
+		</configSet>
+		<configSet>
+			<name>Semi-Secure Configuration</name>
+			<allowBeanDefinitionOverriding>true</allowBeanDefinitionOverriding>
+			<incomplete>false</incomplete>
+			<configs>
+				<config>src/main/webapp/WEB-INF/beans.xml</config>
+				<config>src/main/webapp/WEB-INF/providers.xml</config>
+				<config>src/main/webapp/WEB-INF/webappBeans.xml</config>
+				<config>src/main/webapp/WEB-INF/partsecure.xml</config>
+			</configs>
+			<profiles>
+			</profiles>
+		</configSet>
+	</configSets>
+</beansProjectDescription>]]></content>
+						</file>
+					</additionalConfig>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>net.alchim31.maven</groupId>
+				<artifactId>yuicompressor-maven-plugin</artifactId>
+				<version>1.4.0</version>
+				<executions>
+					<execution>
+						<goals>
+							<goal>compress</goal>
+						</goals>
+					</execution>
+				</executions>
+				<configuration>
+					<excludes>
+						<exclude>**/*.min.js</exclude>
+					</excludes>
+					<nosuffix>true</nosuffix>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-resources-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>override-executeworkflow-scripts</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>copy-resources</goal>
+						</goals>
+						<configuration>
+							<outputDirectory>${cmdline.dir}</outputDirectory>
+							<resources>
+								<resource>
+									<directory>src/main/replacementscripts</directory>
+									<filtering>false</filtering>
+								</resource>
+							</resources>
+							<overwrite>true</overwrite>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>exec-maven-plugin</artifactId>
+				<version>1.2.1</version>
+				<executions>
+					<execution>
+						<id>improve-registry-coverage</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<configuration>
+							<executable>/bin/sh</executable>
+							<workingDirectory>${cmdline.dir}</workingDirectory>
+							<environmentVariables>
+								<RAVEN_APPHOME>${cmdline.dir}</RAVEN_APPHOME>
+							</environmentVariables>
+							<arguments>
+								<argument>./executeworkflow.sh</argument>
+								<argument>-help</argument>
+							</arguments>
+							<outputFile>/dev/null</outputFile>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+		<resources>
+			<resource>
+				<directory>src/main/resources</directory>
+				<filtering>true</filtering>
+			</resource>
+			<resource>
+				<directory>src/main/webapp</directory>
+				<filtering>false</filtering>
+			</resource>
+		</resources>
+	</build>
+
+	<profiles>
+		<profile>
+			<id>unix</id>
+			<properties>
+				<forker.module>server-unix-forker</forker.module>
+			</properties>
+		</profile>
+		<profile>
+			<id>win</id>
+			<properties>
+				<!-- This doesn't exist yet. -->
+				<forker.module>server-win-forker</forker.module>
+			</properties>
+		</profile>
+		<profile>
+			<id>nosec</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-war-plugin</artifactId>
+						<configuration>
+							<webXml>src/main/webapp/WEB-INF/web-nosec.xml</webXml>
+						</configuration>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+		<profile>
+			<id>partsec</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.apache.maven.plugins</groupId>
+						<artifactId>maven-war-plugin</artifactId>
+						<configuration>
+							<webXml>src/main/webapp/WEB-INF/web-partsec.xml</webXml>
+						</configuration>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+	</profiles>
+</project>
diff --git a/server-webapp/src/build/resources/datanucleus_log4j.properties b/server-webapp/src/build/resources/datanucleus_log4j.properties
new file mode 100644
index 0000000..6707f55
--- /dev/null
+++ b/server-webapp/src/build/resources/datanucleus_log4j.properties
@@ -0,0 +1,4 @@
+log4j.rootLogger=info, R 
+log4j.appender.R=org.apache.log4j.ConsoleAppender
+log4j.appender.R.layout=org.apache.log4j.PatternLayout
+log4j.appender.R.layout.ConversionPattern=%d{yyyyMMdd'T'HHmmss.SSS} %-5p %c{1} %C{1} - %m%n
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/ContentsDescriptorBuilder.java b/server-webapp/src/main/java/org/taverna/server/master/ContentsDescriptorBuilder.java
new file mode 100644
index 0000000..f5259bd
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/ContentsDescriptorBuilder.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static eu.medsea.util.MimeUtil.getMimeType;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.common.Uri.secure;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.utils.FilenameUtils;
+import org.taverna.server.port_description.AbsentValue;
+import org.taverna.server.port_description.AbstractPortDescription;
+import org.taverna.server.port_description.AbstractValue;
+import org.taverna.server.port_description.ErrorValue;
+import org.taverna.server.port_description.InputDescription;
+import org.taverna.server.port_description.InputDescription.InputPort;
+import org.taverna.server.port_description.LeafValue;
+import org.taverna.server.port_description.ListValue;
+import org.taverna.server.port_description.OutputDescription;
+import org.taverna.server.port_description.OutputDescription.OutputPort;
+
+import uk.org.taverna.scufl2.api.container.WorkflowBundle;
+import uk.org.taverna.scufl2.api.core.Workflow;
+import uk.org.taverna.scufl2.api.port.InputWorkflowPort;
+import uk.org.taverna.scufl2.api.port.OutputWorkflowPort;
+
+/**
+ * A class that is used to build descriptions of the contents of a workflow
+ * run's filesystem.
+ * 
+ * @author Donal Fellows
+ */
+public class ContentsDescriptorBuilder {
+	private Log log = getLog("Taverna.Server.Webapp");
+	private FilenameUtils fileUtils;
+	private UriBuilderFactory uriBuilderFactory;
+
+	@Required
+	public void setUriBuilderFactory(UriBuilderFactory uriBuilderFactory) {
+		this.uriBuilderFactory = uriBuilderFactory;
+	}
+
+	@Required
+	public void setFileUtils(FilenameUtils fileUtils) {
+		this.fileUtils = fileUtils;
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+
+	private Workflow fillInFromWorkflow(TavernaRun run, UriBuilder ub,
+			AbstractPortDescription portDesc) throws IOException {
+		WorkflowBundle bundle = run.getWorkflow().getScufl2Workflow();
+		bundle.getMainWorkflow().getInputPorts();
+		portDesc.fillInBaseData(bundle.getMainWorkflow()
+				.getWorkflowIdentifier().toString(), run.getId(), ub.build());
+		return bundle.getMainWorkflow();
+	}
+
+	/**
+	 * Computes the depth of value in a descriptor.
+	 * 
+	 * @param value
+	 *            The value description to characterise.
+	 * @return Its depth (i.e., the depth of the port outputting the value) or
+	 *         <tt>null</tt> if that is impossible to determine.
+	 */
+	private Integer computeDepth(AbstractValue value) {
+		if (value instanceof ListValue) {
+			int mv = 1;
+			for (AbstractValue v : ((ListValue) value).contents) {
+				Integer d = computeDepth(v);
+				if (d != null && mv <= d)
+					mv = d + 1;
+			}
+			return mv;
+		} else if (value instanceof LeafValue || value instanceof ErrorValue)
+			return 0;
+		else
+			return null;
+	}
+
+	/**
+	 * Build a description of a leaf value.
+	 * 
+	 * @param file
+	 *            The file representing the value.
+	 * @return A value descriptor.
+	 * @throws FilesystemAccessException
+	 *             If anything goes wrong.
+	 */
+	private LeafValue constructLeafValue(File file)
+			throws FilesystemAccessException {
+		LeafValue v = new LeafValue();
+		v.fileName = file.getFullName();
+		v.byteLength = file.getSize();
+		try {
+			byte[] head = file.getContents(0, 1024);
+			v.contentType = getMimeType(new ByteArrayInputStream(head));
+		} catch (Exception e) {
+			v.contentType = APPLICATION_OCTET_STREAM_TYPE.toString();
+		}
+		return v;
+	}
+
+	/**
+	 * Build a description of an error value.
+	 * 
+	 * @param file
+	 *            The file representing the error.
+	 * @return A value descriptor.
+	 * @throws FilesystemAccessException
+	 *             If anything goes wrong.
+	 */
+	private ErrorValue constructErrorValue(File file)
+			throws FilesystemAccessException {
+		ErrorValue v = new ErrorValue();
+		v.fileName = file.getFullName();
+		v.byteLength = file.getSize();
+		return v;
+	}
+
+	/**
+	 * Build a description of a list value.
+	 * 
+	 * @param dir
+	 *            The directory representing the list.
+	 * @param ub
+	 *            The factory for URIs.
+	 * @return A value descriptor.
+	 * @throws FilesystemAccessException
+	 *             If anything goes wrong.
+	 */
+	private ListValue constructListValue(Directory dir, UriBuilder ub)
+			throws FilesystemAccessException {
+		ListValue v = new ListValue();
+		v.length = 0;
+		Set<DirectoryEntry> contents = new HashSet<>(dir.getContents());
+		Iterator<DirectoryEntry> it = contents.iterator();
+		while (it.hasNext())
+			if (!it.next().getName().matches("^[0-9]+([.].*)?$"))
+				it.remove();
+		for (int i = 1; !contents.isEmpty(); i++) {
+			String exact = Integer.toString(i);
+			AbstractValue subval = constructValue(contents, ub, exact);
+			v.contents.add(subval);
+			if (!(subval instanceof AbsentValue)) {
+				v.length = i;
+				String pfx = i + ".";
+				for (DirectoryEntry de : contents)
+					if (de.getName().equals(exact)
+							|| de.getName().startsWith(pfx)) {
+						contents.remove(de);
+						break;
+					}
+			}
+		}
+		return v;
+	}
+
+	/**
+	 * Build a value description.
+	 * 
+	 * @param parentContents
+	 *            The contents of the parent directory.
+	 * @param ub
+	 *            The factory for URIs.
+	 * @param name
+	 *            The name of the value's file/directory representative.
+	 * @return A value descriptor.
+	 * @throws FilesystemAccessException
+	 *             If anything goes wrong.
+	 */
+	private AbstractValue constructValue(
+			Collection<DirectoryEntry> parentContents, UriBuilder ub,
+			String name) throws FilesystemAccessException {
+		String error = name + ".error";
+		String prefix = name + ".";
+		for (DirectoryEntry entry : parentContents) {
+			AbstractValue av;
+			if (entry.getName().equals(error) && entry instanceof File) {
+				av = constructErrorValue((File) entry);
+			} else if (!entry.getName().equals(name)
+					&& !entry.getName().startsWith(prefix))
+				continue;
+			else if (entry instanceof File)
+				av = constructLeafValue((File) entry);
+			else
+				av = constructListValue((Directory) entry, ub);
+			String fullPath = entry.getFullName().replaceFirst("^/", "");
+			av.href = ub.clone().path(fullPath).build();
+			return av;
+		}
+		return new AbsentValue();
+	}
+
+	/**
+	 * Construct a description of the outputs of a workflow run.
+	 * 
+	 * @param run
+	 *            The workflow run whose outputs are to be described.
+	 * @param ui
+	 *            The origin for URIs.
+	 * @return The description, which can be serialized to XML.
+	 * @throws FilesystemAccessException
+	 *             If something goes wrong reading the directories.
+	 * @throws NoDirectoryEntryException
+	 *             If something goes wrong reading the directories.
+	 */
+	public OutputDescription makeOutputDescriptor(TavernaRun run, UriInfo ui)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		OutputDescription descriptor = new OutputDescription();
+		try {
+			UriBuilder ub = getRunUriBuilder(run, ui);
+			Workflow dataflow = fillInFromWorkflow(run, ub, descriptor);
+			Collection<DirectoryEntry> outs = null;
+			ub = ub.path("wd/{path}");
+			for (OutputWorkflowPort output : dataflow.getOutputPorts()) {
+				OutputPort p = descriptor.addPort(output.getName());
+				if (run.getOutputBaclavaFile() == null) {
+					if (outs == null)
+						outs = fileUtils.getDirectory(run, "out").getContents();
+					p.output = constructValue(outs, ub, p.name);
+					p.depth = computeDepth(p.output);
+				}
+			}
+		} catch (IOException e) {
+			log.info("failure in conversion to .scufl2", e);
+		}
+		return descriptor;
+	}
+
+	private UriBuilder getRunUriBuilder(TavernaRun run, UriInfo ui) {
+		if (ui == null)
+			return secure(uriBuilderFactory.getRunUriBuilder(run));
+		else
+			return secure(fromUri(ui.getAbsolutePath().toString()
+					.replaceAll("/(out|in)put/?$", "")));
+	}
+
+	/**
+	 * Constructs input descriptions.
+	 * 
+	 * @param run
+	 *            The run to build for.
+	 * @param ui
+	 *            The mechanism for building URIs.
+	 * @return The description of the <i>expected</i> inputs of the run.
+	 */
+	public InputDescription makeInputDescriptor(TavernaRun run, UriInfo ui) {
+		InputDescription desc = new InputDescription();
+		try {
+			UriBuilder ub = getRunUriBuilder(run, ui);
+			Workflow workflow = fillInFromWorkflow(run, ub, desc);
+			ub = ub.path("input/{name}");
+			for (InputWorkflowPort port : workflow.getInputPorts()) {
+				InputPort in = desc.addPort(port.getName());
+				in.href = ub.build(in.name);
+				try {
+					in.depth = port.getDepth();
+				} catch (NumberFormatException ex) {
+					in.depth = null;
+				}
+			}
+		} catch (IOException e) {
+			log.info("failure in conversion to .scufl2", e);
+		}
+		return desc;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/DirectoryREST.java b/server-webapp/src/main/java/org/taverna/server/master/DirectoryREST.java
new file mode 100644
index 0000000..48969fa
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/DirectoryREST.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.ok;
+import static javax.ws.rs.core.Response.seeOther;
+import static javax.ws.rs.core.Response.status;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.api.ContentTypes.APPLICATION_ZIP_TYPE;
+import static org.taverna.server.master.api.ContentTypes.DIRECTORY_VARIANTS;
+import static org.taverna.server.master.api.ContentTypes.INITIAL_FILE_VARIANTS;
+import static org.taverna.server.master.common.Roles.SELF;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.PathSegment;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.core.Variant;
+import javax.xml.ws.Holder;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.DirectoryBean;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.DirectoryContents;
+import org.taverna.server.master.rest.FileSegment;
+import org.taverna.server.master.rest.MakeOrUpdateDirEntry;
+import org.taverna.server.master.rest.MakeOrUpdateDirEntry.MakeDirectory;
+import org.taverna.server.master.rest.TavernaServerDirectoryREST;
+import org.taverna.server.master.utils.FilenameUtils;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful access to the filesystem.
+ * 
+ * @author Donal Fellows
+ */
+class DirectoryREST implements TavernaServerDirectoryREST, DirectoryBean {
+	private Log log = getLog("Taverna.Server.Webapp");
+	private TavernaServerSupport support;
+	private TavernaRun run;
+	private FilenameUtils fileUtils;
+
+	@Override
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Override
+	@Required
+	public void setFileUtils(FilenameUtils fileUtils) {
+		this.fileUtils = fileUtils;
+	}
+
+	@Override
+	public DirectoryREST connect(TavernaRun run) {
+		this.run = run;
+		return this;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed({ USER, SELF })
+	public Response destroyDirectoryEntry(List<PathSegment> path)
+			throws NoUpdateException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		support.permitUpdate(run);
+		fileUtils.getDirEntry(run, path).destroy();
+		return noContent().build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed({ USER, SELF })
+	public DirectoryContents getDescription(UriInfo ui)
+			throws FilesystemAccessException {
+		return new DirectoryContents(ui, run.getWorkingDirectory()
+				.getContents());
+	}
+
+	@Override
+	@CallCounted
+	public Response options(List<PathSegment> path) {
+		return opt("PUT", "POST", "DELETE");
+	}
+
+	/*
+	 * // Nasty! This can have several different responses...
+	 * 
+	 * @Override @CallCounted private Response
+	 * getDirectoryOrFileContents(List<PathSegment> path, UriInfo ui, Request
+	 * req) throws FilesystemAccessException, NoDirectoryEntryException {
+	 * 
+	 * DirectoryEntry de = fileUtils.getDirEntry(run, path);
+	 * 
+	 * // How did the user want the result?
+	 * 
+	 * List<Variant> variants = getVariants(de); Variant v =
+	 * req.selectVariant(variants); if (v == null) return
+	 * notAcceptable(variants).type(TEXT_PLAIN)
+	 * .entity("Do not know what type of response to produce.") .build();
+	 * 
+	 * // Produce the content to deliver up
+	 * 
+	 * Object result; if
+	 * (v.getMediaType().equals(APPLICATION_OCTET_STREAM_TYPE))
+	 * 
+	 * // Only for files...
+	 * 
+	 * result = de; else if (v.getMediaType().equals(APPLICATION_ZIP_TYPE))
+	 * 
+	 * // Only for directories...
+	 * 
+	 * result = ((Directory) de).getContentsAsZip(); else
+	 * 
+	 * // Only for directories... // XML or JSON; let CXF pick what to do
+	 * 
+	 * result = new DirectoryContents(ui, ((Directory) de).getContents());
+	 * return ok(result).type(v.getMediaType()).build();
+	 * 
+	 * }
+	 */
+
+	private boolean matchType(MediaType a, MediaType b) {
+		if (log.isDebugEnabled())
+			log.debug("comparing " + a.getType() + "/" + a.getSubtype()
+					+ " and " + b.getType() + "/" + b.getSubtype());
+		return (a.isWildcardType() || b.isWildcardType() || a.getType().equals(
+				b.getType()))
+				&& (a.isWildcardSubtype() || b.isWildcardSubtype() || a
+						.getSubtype().equals(b.getSubtype()));
+	}
+
+	/**
+	 * What are we willing to serve up a directory or file as?
+	 * 
+	 * @param de
+	 *            The reference to the object to serve.
+	 * @return The variants we can serve it as.
+	 * @throws FilesystemAccessException
+	 *             If we fail to read data necessary to detection of its media
+	 *             type.
+	 */
+	private List<Variant> getVariants(DirectoryEntry de)
+			throws FilesystemAccessException {
+		if (de instanceof Directory)
+			return DIRECTORY_VARIANTS;
+		else if (!(de instanceof File))
+			throw new FilesystemAccessException("not a directory or file!");
+		File f = (File) de;
+		List<Variant> variants = new ArrayList<>(INITIAL_FILE_VARIANTS);
+		String contentType = support.getEstimatedContentType(f);
+		if (!contentType.equals(APPLICATION_OCTET_STREAM)) {
+			String[] ct = contentType.split("/");
+			variants.add(0,
+					new Variant(new MediaType(ct[0], ct[1]), (String) null, null));
+		}
+		return variants;
+	}
+
+	/** How did the user want the result? */
+	private MediaType pickType(HttpHeaders headers, DirectoryEntry de)
+			throws FilesystemAccessException, NegotiationFailedException {
+		List<Variant> variants = getVariants(de);
+		// Manual content negotiation!!! Ugh!
+		for (MediaType mt : headers.getAcceptableMediaTypes())
+			for (Variant v : variants)
+				if (matchType(mt, v.getMediaType()))
+					return v.getMediaType();
+		throw new NegotiationFailedException(
+				"Do not know what type of response to produce.", variants);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed({ USER, SELF })
+	public Response getDirectoryOrFileContents(List<PathSegment> path,
+			UriInfo ui, HttpHeaders headers) throws FilesystemAccessException,
+			NoDirectoryEntryException, NegotiationFailedException {
+		DirectoryEntry de = fileUtils.getDirEntry(run, path);
+
+		// How did the user want the result?
+		MediaType wanted = pickType(headers, de);
+
+		log.info("producing content of type " + wanted);
+		// Produce the content to deliver up
+		Object result;
+		if (de instanceof File) {
+			// Only for files...
+			result = de;
+			List<String> range = headers.getRequestHeader("Range");
+			if (range != null && range.size() == 1)
+				return new FileSegment((File) de, range.get(0))
+						.toResponse(wanted);
+		} else {
+			// Only for directories...
+			Directory d = (Directory) de;
+			if (wanted.getType().equals(APPLICATION_ZIP_TYPE.getType())
+					&& wanted.getSubtype().equals(
+							APPLICATION_ZIP_TYPE.getSubtype()))
+				result = d.getContentsAsZip();
+			else
+				// XML or JSON; let CXF pick what to do
+				result = new DirectoryContents(ui, d.getContents());
+		}
+		return ok(result).type(wanted).build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed({ USER, SELF })
+	public Response makeDirectoryOrUpdateFile(List<PathSegment> parent,
+			MakeOrUpdateDirEntry op, UriInfo ui) throws NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException {
+		support.permitUpdate(run);
+		DirectoryEntry container = fileUtils.getDirEntry(run, parent);
+		if (!(container instanceof Directory))
+			throw new FilesystemAccessException("You may not "
+					+ ((op instanceof MakeDirectory) ? "make a subdirectory of"
+							: "place a file in") + " a file.");
+		if (op.name == null || op.name.length() == 0)
+			throw new FilesystemAccessException("missing name attribute");
+		Directory d = (Directory) container;
+		UriBuilder ub = secure(ui).path("{name}");
+
+		// Make a directory in the context directory
+
+		if (op instanceof MakeDirectory) {
+			Directory target = d.makeSubdirectory(support.getPrincipal(),
+					op.name);
+			return created(ub.build(target.getName())).build();
+		}
+
+		// Make or set the contents of a file
+
+		File f = null;
+		for (DirectoryEntry e : d.getContents()) {
+			if (e.getName().equals(op.name)) {
+				if (e instanceof Directory)
+					throw new FilesystemAccessException(
+							"You may not overwrite a directory with a file.");
+				f = (File) e;
+				break;
+			}
+		}
+		if (f == null) {
+			f = d.makeEmptyFile(support.getPrincipal(), op.name);
+			f.setContents(op.contents);
+			return created(ub.build(f.getName())).build();
+		}
+		f.setContents(op.contents);
+		return seeOther(ub.build(f.getName())).build();
+	}
+
+	private File getFileForWrite(List<PathSegment> filePath,
+			Holder<Boolean> isNew) throws FilesystemAccessException,
+			NoDirectoryEntryException, NoUpdateException {
+		support.permitUpdate(run);
+		if (filePath == null || filePath.size() == 0)
+			throw new FilesystemAccessException(
+					"Cannot create a file that is not in a directory.");
+
+		List<PathSegment> dirPath = new ArrayList<>(filePath);
+		String name = dirPath.remove(dirPath.size() - 1).getPath();
+		DirectoryEntry de = fileUtils.getDirEntry(run, dirPath);
+		if (!(de instanceof Directory)) {
+			throw new FilesystemAccessException(
+					"Cannot create a file that is not in a directory.");
+		}
+		Directory d = (Directory) de;
+
+		File f = null;
+		isNew.value = false;
+		for (DirectoryEntry e : d.getContents())
+			if (e.getName().equals(name)) {
+				if (e instanceof File) {
+					f = (File) e;
+					break;
+				}
+				throw new FilesystemAccessException(
+						"Cannot create a file that is not in a directory.");
+			}
+
+		if (f == null) {
+			f = d.makeEmptyFile(support.getPrincipal(), name);
+			isNew.value = true;
+		} else
+			f.setContents(new byte[0]);
+		return f;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed({ USER, SELF })
+	public Response setFileContents(List<PathSegment> filePath,
+			InputStream contents, UriInfo ui) throws NoDirectoryEntryException,
+			NoUpdateException, FilesystemAccessException {
+		Holder<Boolean> isNew = new Holder<>(true);
+		support.copyStreamToFile(contents, getFileForWrite(filePath, isNew));
+
+		if (isNew.value)
+			return created(ui.getAbsolutePath()).build();
+		else
+			return noContent().build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Response setFileContentsFromURL(List<PathSegment> filePath,
+			List<URI> referenceList, UriInfo ui)
+			throws NoDirectoryEntryException, NoUpdateException,
+			FilesystemAccessException {
+		support.permitUpdate(run);
+		if (referenceList.isEmpty() || referenceList.size() > 1)
+			return status(422).entity("URI list must have single URI in it")
+					.build();
+		URI uri = referenceList.get(0);
+		try {
+			uri.toURL();
+		} catch (MalformedURLException e) {
+			return status(422).entity("URI list must have value URL in it")
+					.build();
+		}
+		Holder<Boolean> isNew = new Holder<>(true);
+		File f = getFileForWrite(filePath, isNew);
+
+		try {
+			support.copyDataToFile(uri, f);
+		} catch (MalformedURLException ex) {
+			// Should not happen; called uri.toURL() successfully above
+			throw new NoUpdateException("failed to parse URI", ex);
+		} catch (IOException ex) {
+			throw new FilesystemAccessException(
+					"failed to transfer data from URI", ex);
+		}
+
+		if (isNew.value)
+			return created(ui.getAbsolutePath()).build();
+		else
+			return noContent().build();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/FileConcatenation.java b/server-webapp/src/main/java/org/taverna/server/master/FileConcatenation.java
new file mode 100644
index 0000000..3893b3d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/FileConcatenation.java
@@ -0,0 +1,68 @@
+package org.taverna.server.master;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+/**
+ * Simple concatenation of files.
+ * 
+ * @author Donal Fellows
+ */
+public class FileConcatenation implements Iterable<File> {
+	private List<File> files = new ArrayList<>();
+
+	public void add(File f) {
+		files.add(f);
+	}
+
+	public boolean isEmpty() {
+		return files.isEmpty();
+	}
+
+	/**
+	 * @return The total length of the files, or -1 if this cannot be
+	 *         determined.
+	 */
+	public long size() {
+		long size = 0;
+		for (File f : files)
+			try {
+				size += f.getSize();
+			} catch (FilesystemAccessException e) {
+				// Ignore; shouldn't happen but can't guarantee
+			}
+		return (size == 0 && !files.isEmpty() ? -1 : size);
+	}
+
+	/**
+	 * Get the concatenated files.
+	 * 
+	 * @param encoding
+	 *            The encoding to use.
+	 * @return The concatenated files.
+	 * @throws UnsupportedEncodingException
+	 *             If the encoding doesn't exist.
+	 */
+	public String get(String encoding) throws UnsupportedEncodingException {
+		ByteArrayOutputStream baos = new ByteArrayOutputStream();
+		for (File f : files)
+			try {
+				baos.write(f.getContents(0, -1));
+			} catch (FilesystemAccessException | IOException e) {
+				continue;
+			}
+		return baos.toString(encoding);
+	}
+
+	@Override
+	public Iterator<File> iterator() {
+		return files.iterator();
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/InputREST.java b/server-webapp/src/main/java/org/taverna/server/master/InputREST.java
new file mode 100644
index 0000000..0f48207
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/InputREST.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.util.Date;
+
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.jaxrs.model.URITemplate;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.InputBean;
+import org.taverna.server.master.common.DirEntryReference;
+import org.taverna.server.master.exceptions.BadInputPortNameException;
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerInputREST;
+import org.taverna.server.master.rest.TavernaServerInputREST.InDesc.AbstractContents;
+import org.taverna.server.master.rest.TavernaServerInputREST.InDesc.Reference;
+import org.taverna.server.master.utils.FilenameUtils;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+import org.taverna.server.port_description.InputDescription;
+
+/**
+ * RESTful interface to the input descriptor of a single workflow run.
+ * 
+ * @author Donal Fellows
+ */
+class InputREST implements TavernaServerInputREST, InputBean {
+	private UriInfo ui;
+	private TavernaServerSupport support;
+	private TavernaRun run;
+	private ContentsDescriptorBuilder cdBuilder;
+	private FilenameUtils fileUtils;
+
+	@Override
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Override
+	@Required
+	public void setCdBuilder(ContentsDescriptorBuilder cdBuilder) {
+		this.cdBuilder = cdBuilder;
+	}
+
+	@Override
+	@Required
+	public void setFileUtils(FilenameUtils fileUtils) {
+		this.fileUtils = fileUtils;
+	}
+
+	@Override
+	public InputREST connect(TavernaRun run, UriInfo ui) {
+		this.run = run;
+		this.ui = ui;
+		return this;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public InputsDescriptor get() {
+		return new InputsDescriptor(ui, run);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public InputDescription getExpected() {
+		return cdBuilder.makeInputDescriptor(run, ui);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public String getBaclavaFile() {
+		String i = run.getInputBaclavaFile();
+		return i == null ? "" : i;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public InDesc getInput(String name, UriInfo ui) throws BadInputPortNameException {
+		Input i = support.getInput(run, name);
+		if (i == null)
+			throw new BadInputPortNameException("unknown input port name");
+		return new InDesc(i, ui);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public String setBaclavaFile(String filename) throws NoUpdateException,
+			BadStateChangeException, FilesystemAccessException {
+		support.permitUpdate(run);
+		run.setInputBaclavaFile(filename);
+		String i = run.getInputBaclavaFile();
+		return i == null ? "" : i;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public InDesc setInput(String name, InDesc inputDescriptor, UriInfo ui)
+			throws NoUpdateException, BadStateChangeException,
+			FilesystemAccessException, BadInputPortNameException,
+			BadPropertyValueException {
+		inputDescriptor.descriptorRef = null;
+		AbstractContents ac = inputDescriptor.assignment;
+		if (name == null || name.isEmpty())
+			throw new BadInputPortNameException("bad input name");
+		if (ac == null)
+			throw new BadPropertyValueException("no content!");
+		if (inputDescriptor.delimiter != null
+				&& inputDescriptor.delimiter.isEmpty())
+			inputDescriptor.delimiter = null;
+		if (ac instanceof InDesc.Reference)
+			return setRemoteInput(name, (InDesc.Reference) ac,
+					inputDescriptor.delimiter, ui);
+		if (!(ac instanceof InDesc.File || ac instanceof InDesc.Value))
+			throw new BadPropertyValueException("unknown content type");
+		support.permitUpdate(run);
+		Input i = support.getInput(run, name);
+		if (i == null)
+			i = run.makeInput(name);
+		if (ac instanceof InDesc.File)
+			i.setFile(ac.contents);
+		else
+			i.setValue(ac.contents);
+		i.setDelimiter(inputDescriptor.delimiter);
+		return new InDesc(i, ui);
+	}
+
+	private InDesc setRemoteInput(String name, Reference ref, String delimiter,
+			UriInfo ui) throws BadStateChangeException,
+			BadPropertyValueException, FilesystemAccessException {
+		URITemplate tmpl = new URITemplate(ui.getBaseUri()
+				+ "/runs/{runName}/wd/{path:.+}");
+		MultivaluedMap<String, String> mvm = new MetadataMap<>();
+		if (!tmpl.match(ref.contents, mvm)) {
+			throw new BadPropertyValueException(
+					"URI in reference does not refer to local disk resource");
+		}
+		try {
+			File from = fileUtils.getFile(
+					support.getRun(mvm.get("runName").get(0)),
+					SyntheticDirectoryEntry.make(mvm.get("path").get(0)));
+			File to = run.getWorkingDirectory().makeEmptyFile(
+					support.getPrincipal(), randomUUID().toString());
+
+			to.copy(from);
+
+			Input i = support.getInput(run, name);
+			if (i == null)
+				i = run.makeInput(name);
+			i.setFile(to.getFullName());
+			i.setDelimiter(delimiter);
+			return new InDesc(i, ui);
+		} catch (UnknownRunException e) {
+			throw new BadStateChangeException("may not copy from that run", e);
+		} catch (NoDirectoryEntryException e) {
+			throw new BadStateChangeException("source does not exist", e);
+		}
+	}
+
+	@Override
+	@CallCounted
+	public Response options() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response expectedOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response baclavaOptions() {
+		return opt("PUT");
+	}
+
+	@Override
+	@CallCounted
+	public Response inputOptions(@PathParam("name") String name) {
+		return opt("PUT");
+	}
+}
+
+/**
+ * A way to create synthetic directory entries, used during deletion.
+ * 
+ * @author Donal Fellows
+ */
+class SyntheticDirectoryEntry implements DirectoryEntry {
+	public static DirEntryReference make(String path) {
+		return DirEntryReference.newInstance(new SyntheticDirectoryEntry(path));
+	}
+
+	private SyntheticDirectoryEntry(String p) {
+		this.p = p;
+		this.d = new Date();
+	}
+
+	private String p;
+	private Date d;
+
+	@Override
+	public String getName() {
+		return null;
+	}
+
+	@Override
+	public String getFullName() {
+		return p;
+	}
+
+	@Override
+	public void destroy() {
+	}
+
+	@Override
+	public int compareTo(DirectoryEntry o) {
+		return p.compareTo(o.getFullName());
+	}
+
+	@Override
+	public Date getModificationDate() {
+		return d;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/InteractionFeed.java b/server-webapp/src/main/java/org/taverna/server/master/InteractionFeed.java
new file mode 100644
index 0000000..b686491
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/InteractionFeed.java
@@ -0,0 +1,107 @@
+/*

+ * Copyright (C) 2013 The University of Manchester

+ * 

+ * See the file "LICENSE" for license terms.

+ */

+package org.taverna.server.master;

+

+import static org.taverna.server.master.common.Roles.SELF;

+import static org.taverna.server.master.common.Roles.USER;

+import static org.taverna.server.master.utils.RestUtils.opt;

+

+import java.net.MalformedURLException;

+import java.net.URI;

+import java.net.URISyntaxException;

+

+import javax.annotation.security.RolesAllowed;

+import javax.ws.rs.core.Response;

+

+import org.apache.abdera.model.Entry;

+import org.apache.abdera.model.Feed;

+import org.taverna.server.master.api.FeedBean;

+import org.taverna.server.master.exceptions.FilesystemAccessException;

+import org.taverna.server.master.exceptions.NoDirectoryEntryException;

+import org.taverna.server.master.exceptions.NoUpdateException;

+import org.taverna.server.master.interaction.InteractionFeedSupport;

+import org.taverna.server.master.interfaces.TavernaRun;

+import org.taverna.server.master.rest.InteractionFeedREST;

+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;

+import org.taverna.server.master.utils.InvocationCounter.CallCounted;

+

+/**

+ * How to connect an interaction feed to the webapp.

+ * 

+ * @author Donal Fellows

+ */

+public class InteractionFeed implements InteractionFeedREST, FeedBean {

+	private InteractionFeedSupport interactionFeed;

+	private TavernaRun run;

+

+	@Override

+	public void setInteractionFeedSupport(InteractionFeedSupport feed) {

+		this.interactionFeed = feed;

+	}

+

+	InteractionFeed connect(TavernaRun run) {

+		this.run = run;

+		return this;

+	}

+

+	@Override

+	@CallCounted

+	@PerfLogged

+	@RolesAllowed({ USER, SELF })

+	public Feed getFeed() throws FilesystemAccessException,

+			NoDirectoryEntryException {

+		return interactionFeed.getRunFeed(run);

+	}

+

+	@Override

+	@CallCounted

+	@PerfLogged

+	@RolesAllowed({ USER, SELF })

+	public Response addEntry(Entry entry) throws MalformedURLException,

+			FilesystemAccessException, NoDirectoryEntryException,

+			NoUpdateException {

+		Entry realEntry = interactionFeed.addRunFeedEntry(run, entry);

+		URI location;

+		try {

+			location = realEntry.getSelfLink().getHref().toURI();

+		} catch (URISyntaxException e) {

+			throw new RuntimeException("failed to make URI from link?!", e);

+		}

+		return Response.created(location).entity(realEntry)

+				.type("application/atom+xml;type=entry").build();

+	}

+

+	@Override

+	@CallCounted

+	@PerfLogged

+	@RolesAllowed({ USER, SELF })

+	public Entry getEntry(String id) throws FilesystemAccessException,

+			NoDirectoryEntryException {

+		return interactionFeed.getRunFeedEntry(run, id);

+	}

+

+	@Override

+	@CallCounted

+	@PerfLogged

+	@RolesAllowed({ USER, SELF })

+	public String deleteEntry(String id) throws FilesystemAccessException,

+			NoDirectoryEntryException, NoUpdateException {

+		interactionFeed.removeRunFeedEntry(run, id);

+		return "entry successfully deleted";

+	}

+

+	@Override

+	@CallCounted

+	public Response feedOptions() {

+		return opt("POST");

+	}

+

+	@Override

+	@CallCounted

+	public Response entryOptions(String id) {

+		return opt("DELETE");

+	}

+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/ListenerPropertyREST.java b/server-webapp/src/main/java/org/taverna/server/master/ListenerPropertyREST.java
new file mode 100644
index 0000000..3e983a9
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/ListenerPropertyREST.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.commons.logging.Log;
+import org.taverna.server.master.api.ListenerPropertyBean;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful interface to a single property of a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+class ListenerPropertyREST implements TavernaServerListenersREST.Property,
+		ListenerPropertyBean {
+	private Log log = getLog("Taverna.Server.Webapp");
+	private TavernaServerSupport support;
+	private Listener listen;
+	private String propertyName;
+	private TavernaRun run;
+
+	@Override
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Override
+	public ListenerPropertyREST connect(Listener listen, TavernaRun run,
+			String propertyName) {
+		this.listen = listen;
+		this.propertyName = propertyName;
+		this.run = run;
+		return this;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public String getValue() {
+		try {
+			return listen.getProperty(propertyName);
+		} catch (NoListenerException e) {
+			log.error("unexpected exception; property \"" + propertyName
+					+ "\" should exist", e);
+			return null;
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public String setValue(String value) throws NoUpdateException,
+			NoListenerException {
+		support.permitUpdate(run);
+		listen.setProperty(propertyName, value);
+		return listen.getProperty(propertyName);
+	}
+
+	@Override
+	@CallCounted
+	public Response options() {
+		return opt("PUT");
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/ListenersREST.java b/server-webapp/src/main/java/org/taverna/server/master/ListenersREST.java
new file mode 100644
index 0000000..4b7d7f3
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/ListenersREST.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.taverna.server.master.api.ListenersBean;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.ListenerDefinition;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful interface to a single workflow run's event listeners.
+ * 
+ * @author Donal Fellows
+ */
+abstract class ListenersREST implements TavernaServerListenersREST,
+		ListenersBean {
+	private TavernaRun run;
+	private TavernaServerSupport support;
+
+	@Override
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Override
+	public ListenersREST connect(TavernaRun run) {
+		this.run = run;
+		return this;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response addListener(ListenerDefinition typeAndConfiguration,
+			UriInfo ui) throws NoUpdateException, NoListenerException {
+		String name = support.makeListener(run, typeAndConfiguration.type,
+				typeAndConfiguration.configuration).getName();
+		return created(secure(ui).path("{listenerName}").build(name)).build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public TavernaServerListenerREST getListener(String name)
+			throws NoListenerException {
+		Listener l = support.getListener(run, name);
+		if (l == null)
+			throw new NoListenerException();
+		return makeListenerInterface().connect(l, run);
+	}
+
+	@Nonnull
+	protected abstract SingleListenerREST makeListenerInterface();
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Listeners getDescription(UriInfo ui) {
+		List<ListenerDescription> result = new ArrayList<>();
+		UriBuilder ub = secure(ui).path("{name}");
+		for (Listener l : run.getListeners())
+			result.add(new ListenerDescription(l,
+					fromUri(ub.build(l.getName()))));
+		return new Listeners(result, ub);
+	}
+
+	@Override
+	@CallCounted
+	public Response listenersOptions() {
+		return opt();
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/ManagementState.java b/server-webapp/src/main/java/org/taverna/server/master/ManagementState.java
new file mode 100644
index 0000000..9d4a651
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/ManagementState.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import javax.annotation.PostConstruct;
+import javax.jdo.Query;
+import javax.jdo.annotations.PersistenceAware;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.PrimaryKey;
+
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.ManagementModel;
+import org.taverna.server.master.utils.JDOSupport;
+
+/** The persistent, manageable state of the Taverna Server web application. */
+@PersistenceAware
+class ManagementState extends JDOSupport<WebappState> implements
+		ManagementModel {
+	public ManagementState() {
+		super(WebappState.class);
+	}
+
+	/** Whether we should log all workflows sent to us. */
+	private boolean logIncomingWorkflows = false;
+
+	/** Whether we allow the creation of new workflow runs. */
+	private boolean allowNewWorkflowRuns = true;
+
+	/**
+	 * Whether outgoing exceptions should be logged before being converted to
+	 * responses.
+	 */
+	private boolean logOutgoingExceptions = false;
+
+	/**
+	 * The file that all usage records should be appended to, or <tt>null</tt>
+	 * if they should be just dropped.
+	 */
+	private String usageRecordLogFile = null;
+
+	@Override
+	public void setLogIncomingWorkflows(boolean logIncomingWorkflows) {
+		this.logIncomingWorkflows = logIncomingWorkflows;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public boolean getLogIncomingWorkflows() {
+		self.load();
+		return logIncomingWorkflows;
+	}
+
+	@Override
+	public void setAllowNewWorkflowRuns(boolean allowNewWorkflowRuns) {
+		this.allowNewWorkflowRuns = allowNewWorkflowRuns;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public boolean getAllowNewWorkflowRuns() {
+		self.load();
+		return allowNewWorkflowRuns;
+	}
+
+	@Override
+	public void setLogOutgoingExceptions(boolean logOutgoingExceptions) {
+		this.logOutgoingExceptions = logOutgoingExceptions;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public boolean getLogOutgoingExceptions() {
+		self.load();
+		return logOutgoingExceptions || true;
+	}
+
+	@Override
+	public String getUsageRecordLogFile() {
+		self.load();
+		return usageRecordLogFile;
+	}
+
+	@Override
+	public void setUsageRecordLogFile(String usageRecordLogFile) {
+		this.usageRecordLogFile = usageRecordLogFile;
+		if (loadedState)
+			self.store();
+	}
+
+	private static final int KEY = 42; // whatever
+
+	private WebappState get() {
+		Query q = query("id == " + KEY);
+		q.setUnique(true);
+		return (WebappState) q.execute();
+	}
+
+	private boolean loadedState;
+	private ManagementState self;
+
+	@Required
+	public void setSelf(ManagementState self) {
+		this.self = self;
+	}
+
+	@PostConstruct
+	@WithinSingleTransaction
+	public void load() {
+		if (loadedState || !isPersistent())
+			return;
+		WebappState state = get();
+		if (state == null)
+			return;
+		allowNewWorkflowRuns = state.getAllowNewWorkflowRuns();
+		logIncomingWorkflows = state.getLogIncomingWorkflows();
+		logOutgoingExceptions = state.getLogOutgoingExceptions();
+		usageRecordLogFile = state.getUsageRecordLogFile();
+		loadedState = true;
+	}
+
+	@WithinSingleTransaction
+	public void store() {
+		if (!isPersistent())
+			return;
+		WebappState state = get();
+		if (state == null) {
+			state = new WebappState();
+			// save state
+			state.id = KEY; // whatever...
+			state = persist(state);
+		}
+		state.setAllowNewWorkflowRuns(allowNewWorkflowRuns);
+		state.setLogIncomingWorkflows(logIncomingWorkflows);
+		state.setLogOutgoingExceptions(logOutgoingExceptions);
+		state.setUsageRecordLogFile(usageRecordLogFile);
+		loadedState = true;
+	}
+}
+
+// WARNING! If you change the name of this class, update persistence.xml as
+// well!
+@PersistenceCapable(table = "MANAGEMENTSTATE__WEBAPPSTATE")
+class WebappState implements ManagementModel {
+	public WebappState() {
+	}
+
+	@PrimaryKey
+	protected int id;
+
+	/** Whether we should log all workflows sent to us. */
+	@Persistent
+	private boolean logIncomingWorkflows;
+
+	/** Whether we allow the creation of new workflow runs. */
+	@Persistent
+	private boolean allowNewWorkflowRuns;
+
+	/**
+	 * Whether outgoing exceptions should be logged before being converted to
+	 * responses.
+	 */
+	@Persistent
+	private boolean logOutgoingExceptions;
+
+	/** Where to write usage records. */
+	@Persistent
+	private String usageRecordLogFile;
+
+	@Override
+	public void setLogIncomingWorkflows(boolean logIncomingWorkflows) {
+		this.logIncomingWorkflows = logIncomingWorkflows;
+	}
+
+	@Override
+	public boolean getLogIncomingWorkflows() {
+		return logIncomingWorkflows;
+	}
+
+	@Override
+	public void setAllowNewWorkflowRuns(boolean allowNewWorkflowRuns) {
+		this.allowNewWorkflowRuns = allowNewWorkflowRuns;
+	}
+
+	@Override
+	public boolean getAllowNewWorkflowRuns() {
+		return allowNewWorkflowRuns;
+	}
+
+	@Override
+	public void setLogOutgoingExceptions(boolean logOutgoingExceptions) {
+		this.logOutgoingExceptions = logOutgoingExceptions;
+	}
+
+	@Override
+	public boolean getLogOutgoingExceptions() {
+		return logOutgoingExceptions;
+	}
+
+	@Override
+	public String getUsageRecordLogFile() {
+		return usageRecordLogFile;
+	}
+
+	@Override
+	public void setUsageRecordLogFile(String usageRecordLogFile) {
+		this.usageRecordLogFile = usageRecordLogFile;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/RunREST.java b/server-webapp/src/main/java/org/taverna/server/master/RunREST.java
new file mode 100644
index 0000000..563a822
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/RunREST.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.ok;
+import static javax.ws.rs.core.Response.status;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.joda.time.format.ISODateTimeFormat.dateTime;
+import static org.joda.time.format.ISODateTimeFormat.dateTimeParser;
+import static org.taverna.server.master.common.Roles.SELF;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.common.Status.Initialized;
+import static org.taverna.server.master.common.Status.Operating;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.util.Date;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.logging.Log;
+import org.joda.time.DateTime;
+import org.ogf.usage.JobUsageRecord;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.RunBean;
+import org.taverna.server.master.common.ProfileList;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.NotOwnerException;
+import org.taverna.server.master.exceptions.OverloadedException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.rest.InteractionFeedREST;
+import org.taverna.server.master.rest.TavernaServerInputREST;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+import org.taverna.server.master.rest.TavernaServerRunREST;
+import org.taverna.server.master.rest.TavernaServerSecurityREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+import org.taverna.server.port_description.OutputDescription;
+
+/**
+ * RESTful interface to a single workflow run.
+ * 
+ * @author Donal Fellows
+ */
+abstract class RunREST implements TavernaServerRunREST, RunBean {
+	private Log log = getLog("Taverna.Server.Webapp");
+	private String runName;
+	private TavernaRun run;
+	private TavernaServerSupport support;
+	private ContentsDescriptorBuilder cdBuilder;
+
+	@Override
+	@Required
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Override
+	@Required
+	public void setCdBuilder(ContentsDescriptorBuilder cdBuilder) {
+		this.cdBuilder = cdBuilder;
+	}
+
+	@Override
+	public void setRunName(String runName) {
+		this.runName = runName;
+	}
+
+	@Override
+	public void setRun(TavernaRun run) {
+		this.run = run;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public RunDescription getDescription(UriInfo ui) {
+		return new RunDescription(run, ui);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Response destroy() throws NoUpdateException {
+		try {
+			support.unregisterRun(runName, run);
+		} catch (UnknownRunException e) {
+			log.fatal("can't happen", e);
+		}
+		return noContent().build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public TavernaServerListenersREST getListeners() {
+		return makeListenersInterface().connect(run);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public TavernaServerSecurityREST getSecurity() throws NotOwnerException {
+		TavernaSecurityContext secContext = run.getSecurityContext();
+		if (!support.getPrincipal().equals(secContext.getOwner()))
+			throw new NotOwnerException();
+
+		// context.getBean("run.security", run, secContext);
+		return makeSecurityInterface().connect(secContext, run);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getExpiryTime() {
+		return dateTime().print(new DateTime(run.getExpiry()));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getCreateTime() {
+		return dateTime().print(new DateTime(run.getCreationTimestamp()));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getFinishTime() {
+		Date f = run.getFinishTimestamp();
+		return f == null ? "" : dateTime().print(new DateTime(f));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getStartTime() {
+		Date f = run.getStartTimestamp();
+		return f == null ? "" : dateTime().print(new DateTime(f));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getStatus() {
+		return run.getStatus().toString();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Workflow getWorkflow() {
+		return run.getWorkflow();
+	}
+
+	@Override
+	@CallCounted
+	public String getMainProfileName() {
+		String name = run.getWorkflow().getMainProfileName();
+		return (name == null ? "" : name);
+	}
+
+	@Override
+	@CallCounted
+	public ProfileList getProfiles() {
+		return support.getProfileDescriptor(run.getWorkflow());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed({ USER, SELF })
+	public DirectoryREST getWorkingDirectory() {
+		return makeDirectoryInterface().connect(run);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String setExpiryTime(String expiry) throws NoUpdateException,
+			IllegalArgumentException {
+		DateTime wanted = dateTimeParser().parseDateTime(expiry.trim());
+		Date achieved = support.updateExpiry(run, wanted.toDate());
+		return dateTime().print(new DateTime(achieved));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Response setStatus(String status) throws NoUpdateException {
+		Status newStatus = Status.valueOf(status.trim());
+		support.permitUpdate(run);
+		if (newStatus == Operating && run.getStatus() == Initialized) {
+			if (!support.getAllowStartWorkflowRuns())
+				throw new OverloadedException();
+			String issue = run.setStatus(newStatus);
+			if (issue == null)
+				issue = "starting run...";
+			return status(202).entity(issue).type("text/plain").build();
+		}
+		run.setStatus(newStatus); // Ignore the result
+		return ok(run.getStatus().toString()).type("text/plain").build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public TavernaServerInputREST getInputs(UriInfo ui) {
+		return makeInputInterface().connect(run, ui);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getOutputFile() {
+		String o = run.getOutputBaclavaFile();
+		return o == null ? "" : o;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String setOutputFile(String filename) throws NoUpdateException,
+			FilesystemAccessException, BadStateChangeException {
+		support.permitUpdate(run);
+		if (filename != null && filename.length() == 0)
+			filename = null;
+		run.setOutputBaclavaFile(filename);
+		String o = run.getOutputBaclavaFile();
+		return o == null ? "" : o;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public OutputDescription getOutputDescription(UriInfo ui)
+			throws BadStateChangeException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		if (run.getStatus() == Initialized)
+			throw new BadStateChangeException(
+					"may not get output description in initial state");
+		return cdBuilder.makeOutputDescriptor(run, ui);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed({ USER, SELF })
+	public InteractionFeedREST getInteractionFeed() {
+		return makeInteractionFeed().connect(run);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getName() {
+		return run.getName();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String setName(String name) throws NoUpdateException {
+		support.permitUpdate(run);
+		run.setName(name);
+		return run.getName();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getStdout() throws NoListenerException {
+		return support.getProperty(run, "io", "stdout");
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getStderr() throws NoListenerException {
+		return support.getProperty(run, "io", "stderr");
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Response getUsage() throws NoListenerException, JAXBException {
+		String ur = support.getProperty(run, "io", "usageRecord");
+		if (ur.isEmpty())
+			return noContent().build();
+		return ok(JobUsageRecord.unmarshal(ur), APPLICATION_XML).build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Response getLogContents() {
+		FileConcatenation fc = support.getLogs(run);
+		if (fc.isEmpty())
+			return Response.noContent().build();
+		return Response.ok(fc, TEXT_PLAIN).build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Response getRunBundle() {
+		FileConcatenation fc = support.getProv(run);
+		if (fc.isEmpty())
+			return Response.status(404).entity("no provenance currently available").build();
+		return Response.ok(fc, "application/vnd.wf4ever.robundle+zip").build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public boolean getGenerateProvenance() {
+		return run.getGenerateProvenance();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public boolean setGenerateProvenance(boolean newValue) throws NoUpdateException {
+		support.permitUpdate(run);
+		run.setGenerateProvenance(newValue);
+		return run.getGenerateProvenance();
+	}
+
+	/**
+	 * Construct a RESTful interface to a run's filestore.
+	 * 
+	 * @return The handle to the interface, as decorated by Spring.
+	 */
+	protected abstract DirectoryREST makeDirectoryInterface();
+
+	/**
+	 * Construct a RESTful interface to a run's input descriptors.
+	 * 
+	 * @return The handle to the interface, as decorated by Spring.
+	 */
+	protected abstract InputREST makeInputInterface();
+
+	/**
+	 * Construct a RESTful interface to a run's listeners.
+	 * 
+	 * @return The handle to the interface, as decorated by Spring.
+	 */
+	protected abstract ListenersREST makeListenersInterface();
+
+	/**
+	 * Construct a RESTful interface to a run's security.
+	 * 
+	 * @return The handle to the interface, as decorated by Spring.
+	 */
+	protected abstract RunSecurityREST makeSecurityInterface();
+
+	/**
+	 * Construct a RESTful interface to a run's interaction feed.
+	 * 
+	 * @return The handle to the interaface, as decorated by Spring.
+	 */
+	protected abstract InteractionFeed makeInteractionFeed();
+
+	@Override
+	@CallCounted
+	public Response runOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response workflowOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response profileOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response expiryOptions() {
+		return opt("PUT");
+	}
+
+	@Override
+	@CallCounted
+	public Response createTimeOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response startTimeOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response finishTimeOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response statusOptions() {
+		return opt("PUT");
+	}
+
+	@Override
+	@CallCounted
+	public Response outputOptions() {
+		return opt("PUT");
+	}
+
+	@Override
+	@CallCounted
+	public Response nameOptions() {
+		return opt("PUT");
+	}
+
+	@Override
+	@CallCounted
+	public Response stdoutOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response stderrOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response usageOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response logOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response runBundleOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response generateProvenanceOptions() {
+		return opt("PUT");
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/RunSecurityREST.java b/server-webapp/src/main/java/org/taverna/server/master/RunSecurityREST.java
new file mode 100644
index 0000000..5a366b2
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/RunSecurityREST.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static java.util.UUID.randomUUID;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static org.taverna.server.master.common.Status.Initialized;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.net.URI;
+import java.util.Map;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.taverna.server.master.api.SecurityBean;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.Permission;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.exceptions.NoCredentialException;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.rest.TavernaServerSecurityREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful interface to a single workflow run's security settings.
+ * 
+ * @author Donal Fellows
+ */
+class RunSecurityREST implements TavernaServerSecurityREST, SecurityBean {
+	private TavernaServerSupport support;
+	private TavernaSecurityContext context;
+	private TavernaRun run;
+
+	@Override
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Override
+	public RunSecurityREST connect(TavernaSecurityContext context,
+			TavernaRun run) {
+		this.context = context;
+		this.run = run;
+		return this;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Descriptor describe(UriInfo ui) {
+		return new Descriptor(secure(ui).path("{element}"), context.getOwner()
+				.getName(), context.getCredentials(), context.getTrusted());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public String getOwner() {
+		return context.getOwner().getName();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public CredentialList listCredentials() {
+		return new CredentialList(context.getCredentials());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public CredentialHolder getParticularCredential(String id)
+			throws NoCredentialException {
+		for (Credential c : context.getCredentials())
+			if (c.id.equals(id))
+				return new CredentialHolder(c);
+		throw new NoCredentialException();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public CredentialHolder setParticularCredential(String id,
+			CredentialHolder cred, UriInfo ui)
+			throws InvalidCredentialException, BadStateChangeException {
+		if (run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		Credential c = cred.credential;
+		c.id = id;
+		c.href = ui.getAbsolutePath().toString();
+		context.validateCredential(c);
+		context.deleteCredential(c);
+		context.addCredential(c);
+		return new CredentialHolder(c);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response addCredential(CredentialHolder cred, UriInfo ui)
+			throws InvalidCredentialException, BadStateChangeException {
+		if (run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		Credential c = cred.credential;
+		c.id = randomUUID().toString();
+		URI uri = secure(ui).path("{id}").build(c.id);
+		c.href = uri.toString();
+		context.validateCredential(c);
+		context.addCredential(c);
+		return created(uri).build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response deleteAllCredentials(UriInfo ui)
+			throws BadStateChangeException {
+		if (run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		for (Credential c : context.getCredentials())
+			context.deleteCredential(c);
+		return noContent().build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response deleteCredential(String id, UriInfo ui)
+			throws BadStateChangeException {
+		if (run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		context.deleteCredential(new Credential.Dummy(id));
+		return noContent().build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public TrustList listTrusted() {
+		return new TrustList(context.getTrusted());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Trust getParticularTrust(String id) throws NoCredentialException {
+		for (Trust t : context.getTrusted())
+			if (t.id.equals(id))
+				return t;
+		throw new NoCredentialException();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Trust setParticularTrust(String id, Trust t, UriInfo ui)
+			throws InvalidCredentialException, BadStateChangeException {
+		if (run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		t.id = id;
+		t.href = ui.getAbsolutePath().toString();
+		context.validateTrusted(t);
+		context.deleteTrusted(t);
+		context.addTrusted(t);
+		return t;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response addTrust(Trust t, UriInfo ui)
+			throws InvalidCredentialException, BadStateChangeException {
+		if (run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		t.id = randomUUID().toString();
+		URI uri = secure(ui).path("{id}").build(t.id);
+		t.href = uri.toString();
+		context.validateTrusted(t);
+		context.addTrusted(t);
+		return created(uri).build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response deleteAllTrusts(UriInfo ui) throws BadStateChangeException {
+		if (run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		for (Trust t : context.getTrusted())
+			context.deleteTrusted(t);
+		return noContent().build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response deleteTrust(String id, UriInfo ui)
+			throws BadStateChangeException {
+		if (run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		Trust toDelete = new Trust();
+		toDelete.id = id;
+		context.deleteTrusted(toDelete);
+		return noContent().build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public PermissionsDescription describePermissions(UriInfo ui) {
+		Map<String, Permission> perm = support.getPermissionMap(context);
+		return new PermissionsDescription(secure(ui).path("{id}"), perm);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Permission describePermission(String id) {
+		return support.getPermission(context, id);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Permission setPermission(String id, Permission perm) {
+		support.setPermission(context, id, perm);
+		return support.getPermission(context, id);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response deletePermission(String id, UriInfo ui) {
+		support.setPermission(context, id, Permission.None);
+		return noContent().build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public Response makePermission(PermissionDescription desc, UriInfo ui) {
+		support.setPermission(context, desc.userName, desc.permission);
+		return created(secure(ui).path("{user}").build(desc.userName)).build();
+	}
+
+	@Override
+	@CallCounted
+	public Response descriptionOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response ownerOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response credentialsOptions() {
+		return opt("POST", "DELETE");
+	}
+
+	@Override
+	@CallCounted
+	public Response credentialOptions(String id) {
+		return opt("PUT", "DELETE");
+	}
+
+	@Override
+	@CallCounted
+	public Response trustsOptions() {
+		return opt("POST", "DELETE");
+	}
+
+	@Override
+	@CallCounted
+	public Response trustOptions(String id) {
+		return opt("PUT", "DELETE");
+	}
+
+	@Override
+	@CallCounted
+	public Response permissionsOptions() {
+		return opt("POST");
+	}
+
+	@Override
+	@CallCounted
+	public Response permissionOptions(String id) {
+		return opt("PUT", "DELETE");
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/SingleListenerREST.java b/server-webapp/src/main/java/org/taverna/server/master/SingleListenerREST.java
new file mode 100644
index 0000000..6c9e8d8
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/SingleListenerREST.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static java.util.Arrays.asList;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.util.List;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.taverna.server.master.api.OneListenerBean;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+import org.taverna.server.master.rest.TavernaServerListenersREST.ListenerDescription;
+import org.taverna.server.master.rest.TavernaServerListenersREST.TavernaServerListenerREST;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * RESTful interface to a single listener attached to a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+abstract class SingleListenerREST implements TavernaServerListenerREST,
+		OneListenerBean {
+	private Listener listen;
+	private TavernaRun run;
+
+	@Override
+	public SingleListenerREST connect(Listener listen, TavernaRun run) {
+		this.listen = listen;
+		this.run = run;
+		return this;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public String getConfiguration() {
+		return listen.getConfiguration();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public ListenerDescription getDescription(UriInfo ui) {
+		return new ListenerDescription(listen, secure(ui));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public TavernaServerListenersREST.Properties getProperties(UriInfo ui) {
+		return new TavernaServerListenersREST.Properties(secure(ui).path(
+				"{prop}"), listen.listProperties());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public TavernaServerListenersREST.Property getProperty(
+			final String propertyName) throws NoListenerException {
+		List<String> p = asList(listen.listProperties());
+		if (p.contains(propertyName)) {
+			return makePropertyInterface().connect(listen, run, propertyName);
+		}
+		throw new NoListenerException("no such property");
+	}
+
+	protected abstract ListenerPropertyREST makePropertyInterface();
+
+	@Override
+	@CallCounted
+	public Response listenerOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response configurationOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response propertiesOptions() {
+		return opt();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/TavernaServer.java b/server-webapp/src/main/java/org/taverna/server/master/TavernaServer.java
new file mode 100644
index 0000000..0f98da6
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/TavernaServer.java
@@ -0,0 +1,1425 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static java.lang.Math.min;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.sort;
+import static java.util.UUID.randomUUID;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static javax.xml.ws.handler.MessageContext.HTTP_REQUEST_HEADERS;
+import static javax.xml.ws.handler.MessageContext.PATH_INFO;
+import static org.apache.commons.io.IOUtils.toByteArray;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.TavernaServerSupport.PROV_BUNDLE;
+import static org.taverna.server.master.common.DirEntryReference.newInstance;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Roles.ADMIN;
+import static org.taverna.server.master.common.Roles.SELF;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.common.Status.Initialized;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.soap.DirEntry.convert;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.PreDestroy;
+import javax.annotation.Resource;
+import javax.annotation.security.DeclareRoles;
+import javax.annotation.security.RolesAllowed;
+import javax.jws.WebService;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBException;
+import javax.xml.ws.WebServiceContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.cxf.annotations.WSDLDocumentation;
+import org.ogf.usage.JobUsageRecord;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.SupportAware;
+import org.taverna.server.master.api.TavernaServerBean;
+import org.taverna.server.master.common.Capability;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.DirEntryReference;
+import org.taverna.server.master.common.InputDescription;
+import org.taverna.server.master.common.Permission;
+import org.taverna.server.master.common.ProfileList;
+import org.taverna.server.master.common.RunReference;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.common.version.Version;
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoCredentialException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.NotOwnerException;
+import org.taverna.server.master.exceptions.OverloadedException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.factories.ListenerFactory;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.Policy;
+import org.taverna.server.master.interfaces.RunStore;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.notification.NotificationEngine;
+import org.taverna.server.master.notification.atom.EventDAO;
+import org.taverna.server.master.rest.TavernaServerREST;
+import org.taverna.server.master.rest.TavernaServerREST.EnabledNotificationFabrics;
+import org.taverna.server.master.rest.TavernaServerREST.PermittedListeners;
+import org.taverna.server.master.rest.TavernaServerREST.PermittedWorkflows;
+import org.taverna.server.master.rest.TavernaServerREST.PolicyView;
+import org.taverna.server.master.rest.TavernaServerRunREST;
+import org.taverna.server.master.soap.DirEntry;
+import org.taverna.server.master.soap.FileContents;
+import org.taverna.server.master.soap.PermissionList;
+import org.taverna.server.master.soap.TavernaServerSOAP;
+import org.taverna.server.master.soap.WrappedWorkflow;
+import org.taverna.server.master.soap.ZippedDirectory;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.FilenameUtils;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+import org.taverna.server.port_description.OutputDescription;
+
+/**
+ * The core implementation of the web application.
+ * 
+ * @author Donal Fellows
+ */
+@Path("/")
+@DeclareRoles({ USER, ADMIN })
+@WebService(endpointInterface = "org.taverna.server.master.soap.TavernaServerSOAP", serviceName = "TavernaServer", targetNamespace = SERVER_SOAP)
+@WSDLDocumentation("An instance of Taverna " + Version.JAVA + " Server.")
+public abstract class TavernaServer implements TavernaServerSOAP,
+		TavernaServerREST, TavernaServerBean {
+	/**
+	 * The root of descriptions of the server in JMX.
+	 */
+	public static final String JMX_ROOT = "Taverna:group=Server-"
+			+ Version.JAVA + ",name=";
+
+	/** The logger for the server framework. */
+	public Log log = getLog("Taverna.Server.Webapp");
+
+	@PreDestroy
+	void closeLog() {
+		log = null;
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+	// CONNECTIONS TO JMX, SPRING AND CXF
+
+	@Resource
+	WebServiceContext jaxws;
+	@Context
+	private HttpHeaders jaxrsHeaders;
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+	// STATE VARIABLES AND SPRING SETTERS
+
+	/**
+	 * For building descriptions of the expected inputs and actual outputs of a
+	 * workflow.
+	 */
+	private ContentsDescriptorBuilder cdBuilder;
+	/**
+	 * Utilities for accessing files on the local-worker.
+	 */
+	private FilenameUtils fileUtils;
+	/** How notifications are dispatched. */
+	private NotificationEngine notificationEngine;
+	/** Main support class. */
+	private TavernaServerSupport support;
+	/** A storage facility for workflow runs. */
+	private RunStore runStore;
+	/** Encapsulates the policies applied by this server. */
+	private Policy policy;
+	/** Where Atom events come from. */
+	EventDAO eventSource;
+	/** Reference to the main interaction feed. */
+	private String interactionFeed;
+
+	@Override
+	@Required
+	public void setFileUtils(FilenameUtils converter) {
+		this.fileUtils = converter;
+	}
+
+	@Override
+	@Required
+	public void setContentsDescriptorBuilder(ContentsDescriptorBuilder cdBuilder) {
+		this.cdBuilder = cdBuilder;
+	}
+
+	@Override
+	@Required
+	public void setNotificationEngine(NotificationEngine notificationEngine) {
+		this.notificationEngine = notificationEngine;
+	}
+
+	/**
+	 * @param support
+	 *            the support to set
+	 */
+	@Override
+	@Required
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Override
+	@Required
+	public void setRunStore(RunStore runStore) {
+		this.runStore = runStore;
+	}
+
+	@Override
+	@Required
+	public void setPolicy(Policy policy) {
+		this.policy = policy;
+	}
+
+	@Override
+	@Required
+	public void setEventSource(EventDAO eventSource) {
+		this.eventSource = eventSource;
+	}
+
+	/**
+	 * The location of a service-wide interaction feed, derived from a
+	 * properties file. Expected to be <i>actually</i> not set (to a real
+	 * value).
+	 * 
+	 * @param interactionFeed
+	 *            The URL, which will be resolved relative to the location of
+	 *            the webapp, or the string "<tt>none</tt>" (which corresponds
+	 *            to a <tt>null</tt>).
+	 */
+	public void setInteractionFeed(String interactionFeed) {
+		if ("none".equals(interactionFeed))
+			interactionFeed = null;
+		else if (interactionFeed != null && interactionFeed.startsWith("${"))
+			interactionFeed = null;
+		this.interactionFeed = interactionFeed;
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+	// REST INTERFACE
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public ServerDescription describeService(UriInfo ui) {
+		jaxrsUriInfo.set(new WeakReference<>(ui));
+		return new ServerDescription(ui, resolve(interactionFeed));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public RunList listUsersRuns(UriInfo ui) {
+		jaxrsUriInfo.set(new WeakReference<>(ui));
+		return new RunList(runs(), secure(ui).path("{name}"));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Response submitWorkflow(Workflow workflow, UriInfo ui)
+			throws NoUpdateException {
+		jaxrsUriInfo.set(new WeakReference<>(ui));
+		checkCreatePolicy(workflow);
+		String name = support.buildWorkflow(workflow);
+		return created(secure(ui).path("{uuid}").build(name)).build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Response submitWorkflowByURL(List<URI> referenceList, UriInfo ui)
+			throws NoCreateException {
+		jaxrsUriInfo.set(new WeakReference<>(ui));
+		if (referenceList == null || referenceList.size() == 0)
+			throw new NoCreateException("no workflow URI supplied");
+		URI workflowURI = referenceList.get(0);
+		checkCreatePolicy(workflowURI);
+		Workflow workflow;
+		try {
+			workflow = support.getWorkflowDocumentFromURI(workflowURI);
+		} catch (IOException e) {
+			throw new NoCreateException("could not read workflow", e);
+		}
+		String name = support.buildWorkflow(workflow);
+		return created(secure(ui).path("{uuid}").build(name)).build();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public int getServerMaxRuns() {
+		return support.getMaxSimultaneousRuns();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed({ USER, SELF })
+	public TavernaServerRunREST getRunResource(String runName, UriInfo ui)
+			throws UnknownRunException {
+		jaxrsUriInfo.set(new WeakReference<>(ui));
+		RunREST rr = makeRunInterface();
+		rr.setRun(support.getRun(runName));
+		rr.setRunName(runName);
+		return rr;
+	}
+
+	private ThreadLocal<Reference<UriInfo>> jaxrsUriInfo = new InheritableThreadLocal<>();
+
+	private UriInfo getUriInfo() {
+		if (jaxrsUriInfo.get() == null)
+			return null;
+		return jaxrsUriInfo.get().get();
+	}
+
+	@Override
+	@CallCounted
+	public abstract PolicyView getPolicyDescription();
+
+	@Override
+	@CallCounted
+	public Response serviceOptions() {
+		return opt();
+	}
+
+	@Override
+	@CallCounted
+	public Response runsOptions() {
+		return opt("POST");
+	}
+
+	/**
+	 * Construct a RESTful interface to a run.
+	 * 
+	 * @return The handle to the interface, as decorated by Spring.
+	 */
+	protected abstract RunREST makeRunInterface();
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+	// SOAP INTERFACE
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public RunReference[] listRuns() {
+		ArrayList<RunReference> ws = new ArrayList<>();
+		UriBuilder ub = getRunUriBuilder();
+		for (String runName : runs().keySet())
+			ws.add(new RunReference(runName, ub));
+		return ws.toArray(new RunReference[ws.size()]);
+	}
+
+	private void checkCreatePolicy(Workflow workflow) throws NoCreateException {
+		List<URI> pwu = policy
+				.listPermittedWorkflowURIs(support.getPrincipal());
+		if (pwu == null || pwu.size() == 0)
+			return;
+		throw new NoCreateException("server policy: will only start "
+				+ "workflows sourced from permitted URI list");
+	}
+
+	private void checkCreatePolicy(URI workflowURI) throws NoCreateException {
+		List<URI> pwu = policy
+				.listPermittedWorkflowURIs(support.getPrincipal());
+		if (pwu == null || pwu.size() == 0 || pwu.contains(workflowURI))
+			return;
+		throw new NoCreateException("workflow URI not on permitted list");
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public RunReference submitWorkflow(Workflow workflow)
+			throws NoUpdateException {
+		checkCreatePolicy(workflow);
+		String name = support.buildWorkflow(workflow);
+		return new RunReference(name, getRunUriBuilder());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public RunReference submitWorkflowMTOM(WrappedWorkflow workflow)
+			throws NoUpdateException {
+		Workflow wf;
+		try {
+			wf = workflow.getWorkflow();
+		} catch (IOException e) {
+			throw new NoCreateException(e.getMessage(), e);
+		}
+		checkCreatePolicy(wf);
+		String name = support.buildWorkflow(wf);
+		return new RunReference(name, getRunUriBuilder());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public RunReference submitWorkflowByURI(URI workflowURI)
+			throws NoCreateException {
+		checkCreatePolicy(workflowURI);
+		Workflow workflow;
+		try {
+			workflow = support.getWorkflowDocumentFromURI(workflowURI);
+		} catch (IOException e) {
+			throw new NoCreateException("could not read workflow", e);
+		}
+		String name = support.buildWorkflow(workflow);
+		return new RunReference(name, getRunUriBuilder());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public URI[] getServerWorkflows() {
+		return support.getPermittedWorkflowURIs();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public String[] getServerListeners() {
+		List<String> types = support.getListenerTypes();
+		return types.toArray(new String[types.size()]);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public String[] getServerNotifiers() {
+		List<String> dispatchers = notificationEngine
+				.listAvailableDispatchers();
+		return dispatchers.toArray(new String[dispatchers.size()]);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public List<Capability> getServerCapabilities() {
+		return support.getCapabilities();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void destroyRun(String runName) throws UnknownRunException,
+			NoUpdateException {
+		support.unregisterRun(runName, null);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunDescriptiveName(String runName)
+			throws UnknownRunException {
+		return support.getRun(runName).getName();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunDescriptiveName(String runName, String descriptiveName)
+			throws UnknownRunException, NoUpdateException {
+		TavernaRun run = support.getRun(runName);
+		support.permitUpdate(run);
+		run.setName(descriptiveName);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Workflow getRunWorkflow(String runName) throws UnknownRunException {
+		return support.getRun(runName).getWorkflow();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public WrappedWorkflow getRunWorkflowMTOM(String runName)
+			throws UnknownRunException {
+		WrappedWorkflow ww = new WrappedWorkflow();
+		ww.setWorkflow(support.getRun(runName).getWorkflow());
+		return ww;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public ProfileList getRunWorkflowProfiles(String runName)
+			throws UnknownRunException {
+		return support.getProfileDescriptor(support.getRun(runName)
+				.getWorkflow());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Date getRunExpiry(String runName) throws UnknownRunException {
+		return support.getRun(runName).getExpiry();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunExpiry(String runName, Date d)
+			throws UnknownRunException, NoUpdateException {
+		support.updateExpiry(support.getRun(runName), d);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Date getRunCreationTime(String runName) throws UnknownRunException {
+		return support.getRun(runName).getCreationTimestamp();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Date getRunFinishTime(String runName) throws UnknownRunException {
+		return support.getRun(runName).getFinishTimestamp();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Date getRunStartTime(String runName) throws UnknownRunException {
+		return support.getRun(runName).getStartTimestamp();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Status getRunStatus(String runName) throws UnknownRunException {
+		return support.getRun(runName).getStatus();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String setRunStatus(String runName, Status s)
+			throws UnknownRunException, NoUpdateException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		if (s == Status.Operating && w.getStatus() == Status.Initialized) {
+			if (!support.getAllowStartWorkflowRuns())
+				throw new OverloadedException();
+			try {
+				String issue = w.setStatus(s);
+				if (issue == null)
+					return "";
+				if (issue.isEmpty())
+					return "unknown reason for partial change";
+				return issue;
+			} catch (RuntimeException | NoUpdateException e) {
+				log.info("failed to start run " + runName, e);
+				throw e;
+			}
+		} else {
+			w.setStatus(s);
+			return "";
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunStdout(String runName) throws UnknownRunException {
+		try {
+			return support.getProperty(runName, "io", "stdout");
+		} catch (NoListenerException e) {
+			return "";
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunStderr(String runName) throws UnknownRunException {
+		try {
+			return support.getProperty(runName, "io", "stderr");
+		} catch (NoListenerException e) {
+			return "";
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public JobUsageRecord getRunUsageRecord(String runName)
+			throws UnknownRunException {
+		try {
+			String ur = support.getProperty(runName, "io", "usageRecord");
+			if (ur.isEmpty())
+				return null;
+			return JobUsageRecord.unmarshal(ur);
+		} catch (NoListenerException e) {
+			return null;
+		} catch (JAXBException e) {
+			log.info("failed to deserialize non-empty usage record", e);
+			return null;
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunLog(String runName) throws UnknownRunException {
+		try {
+			return support.getLogs(support.getRun(runName)).get("UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			log.warn("unexpected encoding problem", e);
+			return "";
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public FileContents getRunBundle(String runName)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		File f = fileUtils.getFile(support.getRun(runName), PROV_BUNDLE);
+		FileContents fc = new FileContents();
+		// We *know* the content type, by definition
+		fc.setFile(f, "application/vnd.wf4ever.robundle+zip");
+		return fc;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public boolean getRunGenerateProvenance(String runName)
+			throws UnknownRunException {
+		return support.getRun(runName).getGenerateProvenance();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunGenerateProvenance(String runName, boolean generate)
+			throws UnknownRunException, NoUpdateException {
+		TavernaRun run = support.getRun(runName);
+		support.permitUpdate(run);
+		run.setGenerateProvenance(generate);
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+	// SOAP INTERFACE - Security
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunOwner(String runName) throws UnknownRunException {
+		return support.getRun(runName).getSecurityContext().getOwner()
+				.getName();
+	}
+
+	/**
+	 * Look up a security context, applying access control rules for access to
+	 * the parts of the context that are only open to the owner.
+	 * 
+	 * @param runName
+	 *            The name of the workflow run.
+	 * @param initialOnly
+	 *            Whether to check if we're in the initial state.
+	 * @return The security context. Never <tt>null</tt>.
+	 * @throws UnknownRunException
+	 * @throws NotOwnerException
+	 * @throws BadStateChangeException
+	 */
+	private TavernaSecurityContext getRunSecurityContext(String runName,
+			boolean initialOnly) throws UnknownRunException, NotOwnerException,
+			BadStateChangeException {
+		TavernaRun run = support.getRun(runName);
+		TavernaSecurityContext c = run.getSecurityContext();
+		if (!c.getOwner().equals(support.getPrincipal()))
+			throw new NotOwnerException();
+		if (initialOnly && run.getStatus() != Initialized)
+			throw new BadStateChangeException();
+		return c;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Credential[] getRunCredentials(String runName)
+			throws UnknownRunException, NotOwnerException {
+		try {
+			return getRunSecurityContext(runName, false).getCredentials();
+		} catch (BadStateChangeException e) {
+			Error e2 = new Error("impossible");
+			e2.initCause(e);
+			throw e2;
+		}
+	}
+
+	private Credential findCredential(TavernaSecurityContext c, String id)
+			throws NoCredentialException {
+		for (Credential t : c.getCredentials())
+			if (t.id.equals(id))
+				return t;
+		throw new NoCredentialException();
+	}
+
+	private Trust findTrust(TavernaSecurityContext c, String id)
+			throws NoCredentialException {
+		for (Trust t : c.getTrusted())
+			if (t.id.equals(id))
+				return t;
+		throw new NoCredentialException();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String setRunCredential(String runName, String credentialID,
+			Credential credential) throws UnknownRunException,
+			NotOwnerException, InvalidCredentialException,
+			NoCredentialException, BadStateChangeException {
+		TavernaSecurityContext c = getRunSecurityContext(runName, true);
+		if (credentialID == null || credentialID.isEmpty()) {
+			credential.id = randomUUID().toString();
+		} else {
+			credential.id = findCredential(c, credentialID).id;
+		}
+		URI uri = getRunUriBuilder().path("security/credentials/{credid}")
+				.build(runName, credential.id);
+		credential.href = uri.toString();
+		c.validateCredential(credential);
+		c.addCredential(credential);
+		return credential.id;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void deleteRunCredential(String runName, String credentialID)
+			throws UnknownRunException, NotOwnerException,
+			NoCredentialException, BadStateChangeException {
+		getRunSecurityContext(runName, true).deleteCredential(
+				new Credential.Dummy(credentialID));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Trust[] getRunCertificates(String runName)
+			throws UnknownRunException, NotOwnerException {
+		try {
+			return getRunSecurityContext(runName, false).getTrusted();
+		} catch (BadStateChangeException e) {
+			Error e2 = new Error("impossible");
+			e2.initCause(e);
+			throw e2;
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String setRunCertificates(String runName, String certificateID,
+			Trust certificate) throws UnknownRunException, NotOwnerException,
+			InvalidCredentialException, NoCredentialException,
+			BadStateChangeException {
+		TavernaSecurityContext c = getRunSecurityContext(runName, true);
+		if (certificateID == null || certificateID.isEmpty()) {
+			certificate.id = randomUUID().toString();
+		} else {
+			certificate.id = findTrust(c, certificateID).id;
+		}
+		URI uri = getRunUriBuilder().path("security/trusts/{certid}").build(
+				runName, certificate.id);
+		certificate.href = uri.toString();
+		c.validateTrusted(certificate);
+		c.addTrusted(certificate);
+		return certificate.id;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void deleteRunCertificates(String runName, String certificateID)
+			throws UnknownRunException, NotOwnerException,
+			NoCredentialException, BadStateChangeException {
+		TavernaSecurityContext c = getRunSecurityContext(runName, true);
+		Trust toDelete = new Trust();
+		toDelete.id = certificateID;
+		c.deleteTrusted(toDelete);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public PermissionList listRunPermissions(String runName)
+			throws UnknownRunException, NotOwnerException {
+		PermissionList pl = new PermissionList();
+		pl.permission = new ArrayList<>();
+		Map<String, Permission> perm;
+		try {
+			perm = support.getPermissionMap(getRunSecurityContext(runName,
+					false));
+		} catch (BadStateChangeException e) {
+			log.error("unexpected error from internal API", e);
+			perm = emptyMap();
+		}
+		List<String> users = new ArrayList<>(perm.keySet());
+		sort(users);
+		for (String user : users)
+			pl.permission.add(new PermissionList.SinglePermissionMapping(user,
+					perm.get(user)));
+		return pl;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunPermission(String runName, String userName,
+			Permission permission) throws UnknownRunException,
+			NotOwnerException {
+		try {
+			support.setPermission(getRunSecurityContext(runName, false),
+					userName, permission);
+		} catch (BadStateChangeException e) {
+			log.error("unexpected error from internal API", e);
+		}
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+	// SOAP INTERFACE - Filesystem connection
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public OutputDescription getRunOutputDescription(String runName)
+			throws UnknownRunException, BadStateChangeException,
+			FilesystemAccessException, NoDirectoryEntryException {
+		TavernaRun run = support.getRun(runName);
+		if (run.getStatus() == Initialized)
+			throw new BadStateChangeException(
+					"may not get output description in initial state");
+		return cdBuilder.makeOutputDescriptor(run, null);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public DirEntry[] getRunDirectoryContents(String runName, DirEntry d)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		List<DirEntry> result = new ArrayList<>();
+		for (DirectoryEntry e : fileUtils.getDirectory(support.getRun(runName),
+				convert(d)).getContents())
+			result.add(convert(newInstance(null, e)));
+		return result.toArray(new DirEntry[result.size()]);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public byte[] getRunDirectoryAsZip(String runName, DirEntry d)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		try {
+			return toByteArray(fileUtils.getDirectory(support.getRun(runName),
+					convert(d)).getContentsAsZip());
+		} catch (IOException e) {
+			throw new FilesystemAccessException("problem serializing ZIP data",
+					e);
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public ZippedDirectory getRunDirectoryAsZipMTOM(String runName, DirEntry d)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		return new ZippedDirectory(fileUtils.getDirectory(
+				support.getRun(runName), convert(d)));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public DirEntry makeRunDirectory(String runName, DirEntry parent,
+			String name) throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		Directory dir = fileUtils.getDirectory(w, convert(parent))
+				.makeSubdirectory(support.getPrincipal(), name);
+		return convert(newInstance(null, dir));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public DirEntry makeRunFile(String runName, DirEntry parent, String name)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		File f = fileUtils.getDirectory(w, convert(parent)).makeEmptyFile(
+				support.getPrincipal(), name);
+		return convert(newInstance(null, f));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void destroyRunDirectoryEntry(String runName, DirEntry d)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		fileUtils.getDirEntry(w, convert(d)).destroy();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public byte[] getRunFileContents(String runName, DirEntry d)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		File f = fileUtils.getFile(support.getRun(runName), convert(d));
+		return f.getContents(0, -1);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunFileContents(String runName, DirEntry d,
+			byte[] newContents) throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		fileUtils.getFile(w, convert(d)).setContents(newContents);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public FileContents getRunFileContentsMTOM(String runName, DirEntry d)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		File f = fileUtils.getFile(support.getRun(runName), convert(d));
+		FileContents fc = new FileContents();
+		fc.setFile(f, support.getEstimatedContentType(f));
+		return fc;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunFileContentsFromURI(String runName,
+			DirEntryReference file, URI reference)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException {
+		TavernaRun run = support.getRun(runName);
+		support.permitUpdate(run);
+		File f = fileUtils.getFile(run, file);
+		try {
+			support.copyDataToFile(reference, f);
+		} catch (IOException e) {
+			throw new FilesystemAccessException(
+					"problem transferring data from URI", e);
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunFileContentsMTOM(String runName, FileContents newContents)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException {
+		TavernaRun run = support.getRun(runName);
+		support.permitUpdate(run);
+		File f = fileUtils.getFile(run, newContents.name);
+		f.setContents(new byte[0]);
+		support.copyDataToFile(newContents.fileData, f);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunFileType(String runName, DirEntry d)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		return support.getEstimatedContentType(fileUtils.getFile(
+				support.getRun(runName), convert(d)));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public long getRunFileLength(String runName, DirEntry d)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		return fileUtils.getFile(support.getRun(runName), convert(d)).getSize();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public Date getRunFileModified(String runName, DirEntry d)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException {
+		return fileUtils.getFile(support.getRun(runName), convert(d))
+				.getModificationDate();
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+	// SOAP INTERFACE - Run listeners
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String[] getRunListeners(String runName) throws UnknownRunException {
+		TavernaRun w = support.getRun(runName);
+		List<String> result = new ArrayList<>();
+		for (Listener l : w.getListeners())
+			result.add(l.getName());
+		return result.toArray(new String[result.size()]);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String addRunListener(String runName, String listenerType,
+			String configuration) throws UnknownRunException,
+			NoUpdateException, NoListenerException {
+		return support.makeListener(support.getRun(runName), listenerType,
+				configuration).getName();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunListenerConfiguration(String runName,
+			String listenerName) throws UnknownRunException,
+			NoListenerException {
+		return support.getListener(runName, listenerName).getConfiguration();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String[] getRunListenerProperties(String runName, String listenerName)
+			throws UnknownRunException, NoListenerException {
+		return support.getListener(runName, listenerName).listProperties()
+				.clone();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunListenerProperty(String runName, String listenerName,
+			String propName) throws UnknownRunException, NoListenerException {
+		return support.getListener(runName, listenerName).getProperty(propName);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunListenerProperty(String runName, String listenerName,
+			String propName, String value) throws UnknownRunException,
+			NoUpdateException, NoListenerException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		Listener l = support.getListener(w, listenerName);
+		try {
+			l.getProperty(propName); // sanity check!
+			l.setProperty(propName, value);
+		} catch (RuntimeException e) {
+			throw new NoListenerException("problem setting property: "
+					+ e.getMessage(), e);
+		}
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public InputDescription getRunInputs(String runName)
+			throws UnknownRunException {
+		return new InputDescription(support.getRun(runName));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getRunOutputBaclavaFile(String runName)
+			throws UnknownRunException {
+		return support.getRun(runName).getOutputBaclavaFile();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunInputBaclavaFile(String runName, String fileName)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, BadStateChangeException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		w.setInputBaclavaFile(fileName);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunInputPortFile(String runName, String portName,
+			String portFilename) throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, BadStateChangeException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		Input i = support.getInput(w, portName);
+		if (i == null)
+			i = w.makeInput(portName);
+		i.setFile(portFilename);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunInputPortValue(String runName, String portName,
+			String portValue) throws UnknownRunException, NoUpdateException,
+			BadStateChangeException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		Input i = support.getInput(w, portName);
+		if (i == null)
+			i = w.makeInput(portName);
+		i.setValue(portValue);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunInputPortListDelimiter(String runName, String portName,
+			String delimiter) throws UnknownRunException, NoUpdateException,
+			BadStateChangeException, BadPropertyValueException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		Input i = support.getInput(w, portName);
+		if (i == null)
+			i = w.makeInput(portName);
+		if (delimiter != null && delimiter.isEmpty())
+			delimiter = null;
+		if (delimiter != null) {
+			if (delimiter.length() > 1)
+				throw new BadPropertyValueException("delimiter too long");
+			if (delimiter.charAt(0) < 1 || delimiter.charAt(0) > 127)
+				throw new BadPropertyValueException(
+						"delimiter character must be non-NUL ASCII");
+		}
+		i.setDelimiter(delimiter);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public void setRunOutputBaclavaFile(String runName, String outputFile)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, BadStateChangeException {
+		TavernaRun w = support.getRun(runName);
+		support.permitUpdate(w);
+		w.setOutputBaclavaFile(outputFile);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public org.taverna.server.port_description.InputDescription getRunInputDescriptor(
+			String runName) throws UnknownRunException {
+		return cdBuilder.makeInputDescriptor(support.getRun(runName), null);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	@RolesAllowed(USER)
+	public String getServerStatus() {
+		return support.getAllowNewWorkflowRuns() ? "operational" : "suspended";
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+	// SUPPORT METHODS
+
+	@Override
+	public boolean initObsoleteSOAPSecurity(TavernaSecurityContext c) {
+		try {
+			javax.xml.ws.handler.MessageContext msgCtxt = (jaxws == null ? null
+					: jaxws.getMessageContext());
+			if (msgCtxt == null)
+				return true;
+			c.initializeSecurityFromSOAPContext(msgCtxt);
+			return false;
+		} catch (IllegalStateException e) {
+			/* ignore; not much we can do */
+			return true;
+		}
+	}
+
+	@Override
+	public boolean initObsoleteRESTSecurity(TavernaSecurityContext c) {
+		if (jaxrsHeaders == null)
+			return true;
+		c.initializeSecurityFromRESTContext(jaxrsHeaders);
+		return false;
+	}
+
+	/**
+	 * A creator of substitute {@link URI} builders.
+	 * 
+	 * @return A URI builder configured so that it takes a path parameter that
+	 *         corresponds to the run ID (but with no such ID applied).
+	 */
+	UriBuilder getRunUriBuilder() {
+		return getBaseUriBuilder().path("runs/{uuid}");
+	}
+
+	@Override
+	public UriBuilder getRunUriBuilder(TavernaRun run) {
+		return fromUri(getRunUriBuilder().build(run.getId()));
+	}
+
+	private final String DEFAULT_HOST = "localhost:8080"; // Crappy default
+
+	private String getHostLocation() {
+		@java.lang.SuppressWarnings("unchecked")
+		Map<String, List<String>> headers = (Map<String, List<String>>) jaxws
+				.getMessageContext().get(HTTP_REQUEST_HEADERS);
+		if (headers != null) {
+			List<String> host = headers.get("HOST");
+			if (host != null && !host.isEmpty())
+				return host.get(0);
+		}
+		return DEFAULT_HOST;
+	}
+
+	@Nonnull
+	private URI getPossiblyInsecureBaseUri() {
+		// See if JAX-RS can supply the info
+		UriInfo ui = getUriInfo();
+		if (ui != null && ui.getBaseUri() != null)
+			return ui.getBaseUri();
+		// See if JAX-WS *cannot* supply the info
+		if (jaxws == null || jaxws.getMessageContext() == null)
+			// Hack to make the test suite work
+			return URI.create("http://" + DEFAULT_HOST
+					+ "/taverna-server/rest/");
+		String pathInfo = (String) jaxws.getMessageContext().get(PATH_INFO);
+		pathInfo = pathInfo.replaceFirst("/soap$", "/rest/");
+		pathInfo = pathInfo.replaceFirst("/rest/.+$", "/rest/");
+		return URI.create("http://" + getHostLocation() + pathInfo);
+	}
+
+	@Override
+	public UriBuilder getBaseUriBuilder() {
+		return secure(fromUri(getPossiblyInsecureBaseUri()));
+	}
+
+	@Override
+	@Nullable
+	public String resolve(@Nullable String uri) {
+		if (uri == null)
+			return null;
+		return secure(getPossiblyInsecureBaseUri(), uri).toString();
+	}
+
+	private Map<String, TavernaRun> runs() {
+		return runStore.listRuns(support.getPrincipal(), policy);
+	}
+}
+
+/**
+ * RESTful interface to the policies of a Taverna Server installation.
+ * 
+ * @author Donal Fellows
+ */
+class PolicyREST implements PolicyView, SupportAware {
+	private TavernaServerSupport support;
+	private Policy policy;
+	private ListenerFactory listenerFactory;
+	private NotificationEngine notificationEngine;
+
+	@Override
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Required
+	public void setPolicy(Policy policy) {
+		this.policy = policy;
+	}
+
+	@Required
+	public void setListenerFactory(ListenerFactory listenerFactory) {
+		this.listenerFactory = listenerFactory;
+	}
+
+	@Required
+	public void setNotificationEngine(NotificationEngine notificationEngine) {
+		this.notificationEngine = notificationEngine;
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public PolicyDescription getDescription(UriInfo ui) {
+		return new PolicyDescription(ui);
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public int getMaxSimultaneousRuns() {
+		Integer limit = policy.getMaxRuns(support.getPrincipal());
+		if (limit == null)
+			return policy.getMaxRuns();
+		return min(limit.intValue(), policy.getMaxRuns());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public PermittedListeners getPermittedListeners() {
+		return new PermittedListeners(
+				listenerFactory.getSupportedListenerTypes());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public PermittedWorkflows getPermittedWorkflows() {
+		return new PermittedWorkflows(policy.listPermittedWorkflowURIs(support
+				.getPrincipal()));
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public EnabledNotificationFabrics getEnabledNotifiers() {
+		return new EnabledNotificationFabrics(
+				notificationEngine.listAvailableDispatchers());
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public int getMaxOperatingRuns() {
+		return policy.getOperatingLimit();
+	}
+
+	@Override
+	@CallCounted
+	@PerfLogged
+	public CapabilityList getCapabilities() {
+		CapabilityList cl = new CapabilityList();
+		cl.capability.addAll(support.getCapabilities());
+		return cl;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/TavernaServerSupport.java b/server-webapp/src/main/java/org/taverna/server/master/TavernaServerSupport.java
new file mode 100644
index 0000000..ce36bd3
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/TavernaServerSupport.java
@@ -0,0 +1,957 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static eu.medsea.util.MimeUtil.UNKNOWN_MIME_TYPE;
+import static eu.medsea.util.MimeUtil.getExtensionMimeTypes;
+import static eu.medsea.util.MimeUtil.getMimeType;
+import static java.lang.Math.min;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.springframework.jmx.support.MetricType.COUNTER;
+import static org.springframework.jmx.support.MetricType.GAUGE;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+import static org.taverna.server.master.common.Roles.ADMIN;
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.activation.DataHandler;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.PreDestroy;
+import javax.ws.rs.WebApplicationException;
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedMetric;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.taverna.server.master.api.ManagementModel;
+import org.taverna.server.master.api.TavernaServerBean;
+import org.taverna.server.master.common.Capability;
+import org.taverna.server.master.common.Permission;
+import org.taverna.server.master.common.ProfileList;
+import org.taverna.server.master.common.VersionedElement;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.common.version.Version;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.factories.ListenerFactory;
+import org.taverna.server.master.factories.RunFactory;
+import org.taverna.server.master.identity.WorkflowInternalAuthProvider.WorkflowSelfAuthority;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.interfaces.Policy;
+import org.taverna.server.master.interfaces.RunStore;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.rest.handler.T2FlowDocumentHandler;
+import org.taverna.server.master.utils.CapabilityLister;
+import org.taverna.server.master.utils.FilenameUtils;
+import org.taverna.server.master.utils.InvocationCounter;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+import uk.org.taverna.scufl2.api.profiles.Profile;
+
+/**
+ * Web application support utilities.
+ * 
+ * @author Donal Fellows
+ */
+@ManagedResource(objectName = JMX_ROOT + "Webapp", description = "The main Taverna Server "
+		+ Version.JAVA + " web-application interface.")
+public class TavernaServerSupport {
+	/** The main webapp log. */
+	private Log log = getLog("Taverna.Server.Webapp");
+	private Log accessLog = getLog("Taverna.Server.Webapp.Access");;
+	/** Bean used to log counts of external calls. */
+	private InvocationCounter counter;
+	/** A storage facility for workflow runs. */
+	private RunStore runStore;
+	/** Encapsulates the policies applied by this server. */
+	private Policy policy;
+	/** Connection to the persistent state of this service. */
+	private ManagementModel stateModel;
+	/** A factory for event listeners to attach to workflow runs. */
+	private ListenerFactory listenerFactory;
+	/** A factory for workflow runs. */
+	private RunFactory runFactory;
+	/** How to map the user ID to who to run as. */
+	private LocalIdentityMapper idMapper;
+	/** The code that is coupled to CXF. */
+	private TavernaServerBean webapp;
+	/** How to handle files. */
+	private FilenameUtils fileUtils;
+	/** How to get the server capabilities. */
+	private CapabilityLister capabilitySource;
+	/**
+	 * Whether to log failures during principal retrieval. Should be normally on
+	 * as it indicates a serious problem, but can be switched off for testing.
+	 */
+	private boolean logGetPrincipalFailures = true;
+	private Map<String, String> contentTypeMap;
+	/** Number of bytes to read when guessing the MIME type. */
+	private static final int SAMPLE_SIZE = 1024;
+	/** Number of bytes to ask for when copying a stream to a file. */
+	private static final int TRANSFER_SIZE = 32768;
+
+	@PreDestroy
+	void closeLog() {
+		log = null;
+	}
+
+	/**
+	 * @return Count of the number of external calls into this webapp.
+	 */
+	@ManagedMetric(description = "Count of the number of external calls into this webapp.", metricType = COUNTER, category = "throughput")
+	public int getInvocationCount() {
+		return counter.getCount();
+	}
+
+	/**
+	 * @return Current number of runs.
+	 */
+	@ManagedMetric(description = "Current number of runs.", metricType = GAUGE, category = "utilization")
+	public int getCurrentRunCount() {
+		return runStore.listRuns(null, policy).size();
+	}
+
+	/**
+	 * @return Whether to write submitted workflows to the log.
+	 */
+	@ManagedAttribute(description = "Whether to write submitted workflows to the log.")
+	public boolean getLogIncomingWorkflows() {
+		return stateModel.getLogIncomingWorkflows();
+	}
+
+	/**
+	 * @param logIncomingWorkflows
+	 *            Whether to write submitted workflows to the log.
+	 */
+	@ManagedAttribute(description = "Whether to write submitted workflows to the log.")
+	public void setLogIncomingWorkflows(boolean logIncomingWorkflows) {
+		stateModel.setLogIncomingWorkflows(logIncomingWorkflows);
+	}
+
+	/**
+	 * @return Whether outgoing exceptions should be logged before being
+	 *         converted to responses.
+	 */
+	@ManagedAttribute(description = "Whether outgoing exceptions should be logged before being converted to responses.")
+	public boolean getLogOutgoingExceptions() {
+		return stateModel.getLogOutgoingExceptions();
+	}
+
+	/**
+	 * @param logOutgoing
+	 *            Whether outgoing exceptions should be logged before being
+	 *            converted to responses.
+	 */
+	@ManagedAttribute(description = "Whether outgoing exceptions should be logged before being converted to responses.")
+	public void setLogOutgoingExceptions(boolean logOutgoing) {
+		stateModel.setLogOutgoingExceptions(logOutgoing);
+	}
+
+	/**
+	 * @return Whether to permit any new workflow runs to be created.
+	 */
+	@ManagedAttribute(description = "Whether to permit any new workflow runs to be created; has no effect on existing runs.")
+	public boolean getAllowNewWorkflowRuns() {
+		return stateModel.getAllowNewWorkflowRuns();
+	}
+
+	/**
+	 * @param allowNewWorkflowRuns
+	 *            Whether to permit any new workflow runs to be created.
+	 */
+	@ManagedAttribute(description = "Whether to permit any new workflow runs to be created; has no effect on existing runs.")
+	public void setAllowNewWorkflowRuns(boolean allowNewWorkflowRuns) {
+		stateModel.setAllowNewWorkflowRuns(allowNewWorkflowRuns);
+	}
+
+	/**
+	 * @return The server's version identifier.
+	 */
+	@ManagedAttribute(description = "The installed version of the server.")
+	public String getServerVersion() {
+		return VersionedElement.VERSION + " " + VersionedElement.REVISION + " "
+				+ VersionedElement.TIMESTAMP;
+	}
+
+	@ManagedAttribute(description = "The URIs of the workfows that this server will allow to be instantiated.")
+	public URI[] getPermittedWorkflowURIs() {
+		List<URI> pw = policy.listPermittedWorkflowURIs(null);
+		if (pw == null)
+			return new URI[0];
+		return pw.toArray(new URI[pw.size()]);
+	}
+
+	@ManagedAttribute(description = "The URIs of the workfows that this server will allow to be instantiated.")
+	public void setPermittedWorkflowURIs(URI[] pw) {
+		if (pw == null)
+			policy.setPermittedWorkflowURIs(null, new ArrayList<URI>());
+		else
+			policy.setPermittedWorkflowURIs(null, Arrays.asList(pw));
+	}
+
+	public int getMaxSimultaneousRuns() {
+		Integer limit = policy.getMaxRuns(getPrincipal());
+		if (limit == null)
+			return policy.getMaxRuns();
+		return min(limit.intValue(), policy.getMaxRuns());
+	}
+
+	@Autowired
+	private T2FlowDocumentHandler t2flowHandler;
+
+	public Workflow getWorkflowDocumentFromURI(URI uri)
+			throws WebApplicationException, IOException {
+		URLConnection conn = uri.toURL().openConnection();
+		conn.setRequestProperty("Accept", T2FLOW);
+		conn.connect();
+		// Tricky point: we know the reader part of the handler only cares
+		// about the stream argument.
+		return t2flowHandler.readFrom(null, null, null, null, null,
+				conn.getInputStream());
+	}
+
+	public List<String> getListenerTypes() {
+		return listenerFactory.getSupportedListenerTypes();
+	}
+
+	/**
+	 * @param policy
+	 *            The policy being installed by Spring.
+	 */
+	@Required
+	public void setPolicy(Policy policy) {
+		this.policy = policy;
+	}
+
+	/**
+	 * @param listenerFactory
+	 *            The listener factory being installed by Spring.
+	 */
+	@Required
+	public void setListenerFactory(ListenerFactory listenerFactory) {
+		this.listenerFactory = listenerFactory;
+	}
+
+	/**
+	 * @param runFactory
+	 *            The run factory being installed by Spring.
+	 */
+	@Required
+	public void setRunFactory(RunFactory runFactory) {
+		this.runFactory = runFactory;
+	}
+
+	/**
+	 * @param runStore
+	 *            The run store being installed by Spring.
+	 */
+	@Required
+	public void setRunStore(RunStore runStore) {
+		this.runStore = runStore;
+	}
+
+	/**
+	 * @param stateModel
+	 *            The state model engine being installed by Spring.
+	 */
+	@Required
+	public void setStateModel(ManagementModel stateModel) {
+		this.stateModel = stateModel;
+	}
+
+	/**
+	 * @param mapper
+	 *            The identity mapper being installed by Spring.
+	 */
+	@Required
+	public void setIdMapper(LocalIdentityMapper mapper) {
+		this.idMapper = mapper;
+	}
+
+	/**
+	 * @param counter
+	 *            The object whose job it is to manage the counting of
+	 *            invocations. Installed by Spring.
+	 */
+	@Required
+	public void setInvocationCounter(InvocationCounter counter) {
+		this.counter = counter;
+	}
+
+	/**
+	 * @param webapp
+	 *            The web-app being installed by Spring.
+	 */
+	@Required
+	public void setWebapp(TavernaServerBean webapp) {
+		this.webapp = webapp;
+	}
+
+	/**
+	 * @param fileUtils
+	 *            The file handling utilities.
+	 */
+	@Required
+	public void setFileUtils(FilenameUtils fileUtils) {
+		this.fileUtils = fileUtils;
+	}
+
+	/**
+	 * @param logthem
+	 *            Whether to log failures relating to principals.
+	 */
+	public void setLogGetPrincipalFailures(boolean logthem) {
+		logGetPrincipalFailures = logthem;
+	}
+
+	public Map<String, String> getContentTypeMap() {
+		return contentTypeMap;
+	}
+
+	/**
+	 * Mapping from filename suffixes (e.g., "baclava") to content types.
+	 * 
+	 * @param contentTypeMap
+	 *            The mapping to install.
+	 */
+	@Required
+	public void setContentTypeMap(Map<String, String> contentTypeMap) {
+		this.contentTypeMap = contentTypeMap;
+	}
+
+	@Required
+	public void setCapabilitySource(CapabilityLister capabilitySource) {
+		this.capabilitySource = capabilitySource;
+	}
+
+	/**
+	 * Test whether the current user can do updates to the given run.
+	 * 
+	 * @param run
+	 *            The workflow run to do the test on.
+	 * @throws NoUpdateException
+	 *             If the current user is not permitted to update the run.
+	 */
+	public void permitUpdate(@Nonnull TavernaRun run) throws NoUpdateException {
+		if (isSuperUser()) {
+			accessLog
+					.warn("check for admin powers passed; elevated access rights granted for update");
+			return; // Superusers are fully authorized to access others things
+		}
+		if (getSelfAuthority() != null) {
+			// At this point, must already be accessing self as that is checked
+			// in getRun().
+			return;
+		}
+		policy.permitUpdate(getPrincipal(), run);
+	}
+
+	/**
+	 * Test whether the current user can destroy or control the lifespan of the
+	 * given run.
+	 * 
+	 * @param run
+	 *            The workflow run to do the test on.
+	 * @throws NoDestroyException
+	 *             If the current user is not permitted to destroy the run.
+	 */
+	public void permitDestroy(TavernaRun run) throws NoDestroyException {
+		if (isSuperUser()) {
+			accessLog
+					.warn("check for admin powers passed; elevated access rights granted for destroy");
+			return; // Superusers are fully authorized to access others things
+		}
+		if (getSelfAuthority() != null)
+			throw new NoDestroyException();
+		policy.permitDestroy(getPrincipal(), run);
+	}
+
+	/**
+	 * Gets the identity of the user currently accessing the webapp, which is
+	 * stored in a thread-safe way in the webapp's container's context.
+	 * 
+	 * @return The identity of the user accessing the webapp.
+	 */
+	@Nonnull
+	public UsernamePrincipal getPrincipal() {
+		try {
+			Authentication auth = SecurityContextHolder.getContext()
+					.getAuthentication();
+			if (auth == null || !auth.isAuthenticated()) {
+				if (logGetPrincipalFailures)
+					log.warn("failed to get auth; going with <NOBODY>");
+				return new UsernamePrincipal("<NOBODY>");
+			}
+			return new UsernamePrincipal(auth);
+		} catch (RuntimeException e) {
+			if (logGetPrincipalFailures)
+				log.info("failed to map principal", e);
+			throw e;
+		}
+	}
+
+	private WorkflowSelfAuthority getSelfAuthority() {
+		try {
+			Authentication a = SecurityContextHolder.getContext()
+					.getAuthentication();
+			for (GrantedAuthority ga : a.getAuthorities())
+				if (ga instanceof WorkflowSelfAuthority)
+					return (WorkflowSelfAuthority) ga;
+		} catch (RuntimeException e) {
+		}
+		return null;
+	}
+
+	/**
+	 * Obtain the workflow run with a particular name.
+	 * 
+	 * @param name
+	 *            The name of the run to look up.
+	 * @return A workflow run handle that the current user has at least
+	 *         permission to read.
+	 * @throws UnknownRunException
+	 *             If the workflow run doesn't exist or the current user doesn't
+	 *             have permission to see it.
+	 */
+	@Nonnull
+	public TavernaRun getRun(@Nonnull String name) throws UnknownRunException {
+		if (isSuperUser()) {
+			accessLog
+					.info("check for admin powers passed; elevated access rights granted for read");
+			return runStore.getRun(name);
+		}
+		WorkflowSelfAuthority wsa = getSelfAuthority();
+		if (wsa != null) {
+			if (wsa.getWorkflowID().equals(name))
+				return runStore.getRun(name);
+			throw new UnknownRunException();
+		}
+		return runStore.getRun(getPrincipal(), policy, name);
+	}
+
+	/**
+	 * Construct a listener attached to the given run.
+	 * 
+	 * @param run
+	 *            The workflow run to attach the listener to.
+	 * @param type
+	 *            The name of the type of run to create.
+	 * @param configuration
+	 *            The configuration description to pass into the listener. The
+	 *            format of this string is up to the listener to define.
+	 * @return A handle to the listener which can be used to further configure
+	 *         any properties.
+	 * @throws NoListenerException
+	 *             If the listener type is unrecognized or the configuration is
+	 *             invalid.
+	 * @throws NoUpdateException
+	 *             If the run does not permit the current user to add listeners
+	 *             (or perform other types of update).
+	 */
+	@Nonnull
+	public Listener makeListener(@Nonnull TavernaRun run, @Nonnull String type,
+			@Nonnull String configuration) throws NoListenerException,
+			NoUpdateException {
+		permitUpdate(run);
+		return listenerFactory.makeListener(run, type, configuration);
+	}
+
+	/**
+	 * Obtain a listener that is already attached to a workflow run.
+	 * 
+	 * @param run
+	 *            The workflow run to search.
+	 * @param listenerName
+	 *            The name of the listener to look up.
+	 * @return The listener instance interface.
+	 * @throws NoListenerException
+	 *             If no listener with that name exists.
+	 */
+	@Nonnull
+	public Listener getListener(TavernaRun run, String listenerName)
+			throws NoListenerException {
+		for (Listener l : run.getListeners())
+			if (l.getName().equals(listenerName))
+				return l;
+		throw new NoListenerException();
+	}
+
+	/**
+	 * Obtain a property from a listener that is already attached to a workflow
+	 * run.
+	 * 
+	 * @param runName
+	 *            The ID of the workflow run to search.
+	 * @param listenerName
+	 *            The name of the listener to look up in.
+	 * @param propertyName
+	 *            The name of the property to fetch.
+	 * @return The property value.
+	 * @throws NoListenerException
+	 *             If no listener with that name exists, or no property with
+	 *             that name exists.
+	 * @throws UnknownRunException
+	 *             If no run with that name exists.
+	 */
+	@Nonnull
+	public String getProperty(String runName, String listenerName,
+			String propertyName) throws NoListenerException,
+			UnknownRunException {
+		return getListener(runName, listenerName).getProperty(propertyName);
+	}
+
+	/**
+	 * Obtain a property from a listener that is already attached to a workflow
+	 * run.
+	 * 
+	 * @param run
+	 *            The workflow run to search.
+	 * @param listenerName
+	 *            The name of the listener to look up in.
+	 * @param propertyName
+	 *            The name of the property to fetch.
+	 * @return The property value.
+	 * @throws NoListenerException
+	 *             If no listener with that name exists, or no property with
+	 *             that name exists.
+	 */
+	@Nonnull
+	public String getProperty(TavernaRun run, String listenerName,
+			String propertyName) throws NoListenerException {
+		return getListener(run, listenerName).getProperty(propertyName);
+	}
+
+	/**
+	 * Get the permission description for the given user.
+	 * 
+	 * @param context
+	 *            A security context associated with a particular workflow run.
+	 *            Note that only the owner of a workflow run may get the
+	 *            security context in the first place.
+	 * @param userName
+	 *            The name of the user to look up the permission for.
+	 * @return A permission description.
+	 */
+	@Nonnull
+	public Permission getPermission(@Nonnull TavernaSecurityContext context,
+			@Nonnull String userName) {
+		if (context.getPermittedDestroyers().contains(userName))
+			return Permission.Destroy;
+		if (context.getPermittedUpdaters().contains(userName))
+			return Permission.Update;
+		if (context.getPermittedReaders().contains(userName))
+			return Permission.Read;
+		return Permission.None;
+	}
+
+	/**
+	 * Set the permissions for the given user.
+	 * 
+	 * @param context
+	 *            A security context associated with a particular workflow run.
+	 *            Note that only the owner of a workflow run may get the
+	 *            security context in the first place.
+	 * @param userName
+	 *            The name of the user to set the permission for.
+	 * @param permission
+	 *            The description of the permission to grant. Note that the
+	 *            owner of a workflow run always has the equivalent of
+	 *            {@link Permission#Destroy}; this is always enforced before
+	 *            checking for other permissions.
+	 */
+	public void setPermission(TavernaSecurityContext context, String userName,
+			Permission permission) {
+		Set<String> permSet;
+		boolean doRead = false, doWrite = false, doKill = false;
+
+		switch (permission) {
+		case Destroy:
+			doKill = true;
+		case Update:
+			doWrite = true;
+		case Read:
+			doRead = true;
+		default:
+			break;
+		}
+
+		permSet = context.getPermittedReaders();
+		if (doRead) {
+			if (!permSet.contains(userName)) {
+				permSet = new HashSet<>(permSet);
+				permSet.add(userName);
+				context.setPermittedReaders(permSet);
+			}
+		} else if (permSet.contains(userName)) {
+			permSet = new HashSet<>(permSet);
+			permSet.remove(userName);
+			context.setPermittedReaders(permSet);
+		}
+
+		permSet = context.getPermittedUpdaters();
+		if (doWrite) {
+			if (!permSet.contains(userName)) {
+				permSet = new HashSet<>(permSet);
+				permSet.add(userName);
+				context.setPermittedUpdaters(permSet);
+			}
+		} else if (permSet.contains(userName)) {
+			permSet = new HashSet<>(permSet);
+			permSet.remove(userName);
+			context.setPermittedUpdaters(permSet);
+		}
+
+		permSet = context.getPermittedDestroyers();
+		if (doKill) {
+			if (!permSet.contains(userName)) {
+				permSet = new HashSet<>(permSet);
+				permSet.add(userName);
+				context.setPermittedDestroyers(permSet);
+			}
+		} else if (permSet.contains(userName)) {
+			permSet = new HashSet<>(permSet);
+			permSet.remove(userName);
+			context.setPermittedDestroyers(permSet);
+		}
+	}
+
+	public Map<String, Permission> getPermissionMap(
+			TavernaSecurityContext context) {
+		Map<String, Permission> perm = new HashMap<>();
+		for (String u : context.getPermittedReaders())
+			perm.put(u, Permission.Read);
+		for (String u : context.getPermittedUpdaters())
+			perm.put(u, Permission.Update);
+		for (String u : context.getPermittedDestroyers())
+			perm.put(u, Permission.Destroy);
+		return perm;
+	}
+
+	/**
+	 * Stops a run from being possible to be looked up and destroys it.
+	 * 
+	 * @param runName
+	 *            The name of the run.
+	 * @param run
+	 *            The workflow run. <i>Must</i> correspond to the name.
+	 * @throws NoDestroyException
+	 *             If the user is not permitted to destroy the workflow run.
+	 * @throws UnknownRunException
+	 *             If the run is unknown (e.g., because it is already
+	 *             destroyed).
+	 */
+	public void unregisterRun(@Nonnull String runName, @Nonnull TavernaRun run)
+			throws NoDestroyException, UnknownRunException {
+		if (run == null)
+			run = getRun(runName);
+		permitDestroy(run);
+		runStore.unregisterRun(runName);
+		run.destroy();
+	}
+
+	/**
+	 * Changes the expiry date of a workflow run. The expiry date is when the
+	 * workflow run becomes eligible for automated destruction.
+	 * 
+	 * @param run
+	 *            The handle to the workflow run.
+	 * @param date
+	 *            When the workflow run should be expired.
+	 * @return When the workflow run will actually be expired.
+	 * @throws NoDestroyException
+	 *             If the user is not permitted to destroy the workflow run.
+	 *             (Note that lifespan management requires the ability to
+	 *             destroy.)
+	 */
+	@Nonnull
+	public Date updateExpiry(@Nonnull TavernaRun run, @Nonnull Date date)
+			throws NoDestroyException {
+		permitDestroy(run);
+		run.setExpiry(date);
+		return run.getExpiry();
+	}
+
+	/**
+	 * Manufacture a workflow run instance.
+	 * 
+	 * @param workflow
+	 *            The workflow document (t2flow, scufl2?) to instantiate.
+	 * @return The ID of the created workflow run.
+	 * @throws NoCreateException
+	 *             If the user is not permitted to create workflows.
+	 */
+	public String buildWorkflow(Workflow workflow) throws NoCreateException {
+		UsernamePrincipal p = getPrincipal();
+		if (getSelfAuthority() != null)
+			throw new NoCreateException(
+					"runs may not create workflows on their host server");
+		if (!stateModel.getAllowNewWorkflowRuns())
+			throw new NoCreateException("run creation not currently enabled");
+		try {
+			if (stateModel.getLogIncomingWorkflows()) {
+				log.info(workflow.marshal());
+			}
+		} catch (JAXBException e) {
+			log.warn("problem when logging workflow", e);
+		}
+
+		// Security checks
+		policy.permitCreate(p, workflow);
+		if (idMapper != null && idMapper.getUsernameForPrincipal(p) == null) {
+			log.error("cannot map principal to local user id");
+			throw new NoCreateException(
+					"failed to map security token to local user id");
+		}
+
+		TavernaRun run;
+		try {
+			run = runFactory.create(p, workflow);
+			TavernaSecurityContext c = run.getSecurityContext();
+			c.initializeSecurityFromContext(SecurityContextHolder.getContext());
+			/*
+			 * These next pieces of security initialisation are (hopefully)
+			 * obsolete now that we use Spring Security, but we keep them Just
+			 * In Case.
+			 */
+			boolean doRESTinit = webapp.initObsoleteSOAPSecurity(c);
+			if (doRESTinit)
+				webapp.initObsoleteRESTSecurity(c);
+		} catch (Exception e) {
+			log.error("failed to build workflow run worker", e);
+			throw new NoCreateException("failed to build workflow run worker");
+		}
+
+		return runStore.registerRun(run);
+	}
+
+	private boolean isSuperUser() {
+		try {
+			Authentication auth = SecurityContextHolder.getContext()
+					.getAuthentication();
+			if (auth == null || !auth.isAuthenticated())
+				return false;
+			UserDetails details = (UserDetails) auth.getPrincipal();
+			if (log.isDebugEnabled())
+				log.debug("checking for admin role for user <" + auth.getName()
+						+ "> in collection " + details.getAuthorities());
+			return details.getAuthorities().contains(ADMIN);
+		} catch (ClassCastException e) {
+			return false;
+		}
+	}
+
+	/**
+	 * Get a particular input to a workflow run.
+	 * 
+	 * @param run
+	 *            The workflow run to search.
+	 * @param portName
+	 *            The name of the input.
+	 * @return The handle of the input, or <tt>null</tt> if no such handle
+	 *         exists.
+	 */
+	@Nullable
+	public Input getInput(TavernaRun run, String portName) {
+		for (Input i : run.getInputs())
+			if (i.getName().equals(portName))
+				return i;
+		return null;
+	}
+
+	/**
+	 * Get a listener attached to a run.
+	 * 
+	 * @param runName
+	 *            The name of the run to look up
+	 * @param listenerName
+	 *            The name of the listener.
+	 * @return The handle of the listener.
+	 * @throws NoListenerException
+	 *             If no such listener exists.
+	 * @throws UnknownRunException
+	 *             If no such workflow run exists, or if the user does not have
+	 *             permission to access it.
+	 */
+	public Listener getListener(String runName, String listenerName)
+			throws NoListenerException, UnknownRunException {
+		return getListener(getRun(runName), listenerName);
+	}
+
+	/**
+	 * Given a file, produce a guess at its content type. This uses the content
+	 * type map property, and if that search fails it falls back on the Medsea
+	 * mime type library.
+	 * 
+	 * @param f
+	 *            The file handle.
+	 * @return The content type. If all else fails, produces good old
+	 *         "application/octet-stream".
+	 */
+	@Nonnull
+	public String getEstimatedContentType(@Nonnull File f) {
+		String name = f.getName();
+		for (int idx = name.indexOf('.'); idx != -1; idx = name.indexOf('.',
+				idx + 1)) {
+			String mt = contentTypeMap.get(name.substring(idx + 1));
+			if (mt != null)
+				return mt;
+		}
+		@Nonnull
+		String type = getExtensionMimeTypes(name);
+		if (!type.equals(UNKNOWN_MIME_TYPE))
+			return type;
+		try {
+			return getMimeType(new ByteArrayInputStream(f.getContents(0,
+					SAMPLE_SIZE)));
+		} catch (FilesystemAccessException e) {
+			return type;
+		}
+	}
+
+	public void copyDataToFile(DataHandler handler, File file)
+			throws FilesystemAccessException {
+		try {
+			copyStreamToFile(handler.getInputStream(), file);
+		} catch (IOException e) {
+			throw new FilesystemAccessException(
+					"problem constructing stream from data source", e);
+		}
+	}
+
+	public void copyDataToFile(URI uri, File file)
+			throws MalformedURLException, FilesystemAccessException,
+			IOException {
+		copyStreamToFile(uri.toURL().openStream(), file);
+	}
+
+	public void copyStreamToFile(InputStream stream, File file)
+			throws FilesystemAccessException {
+		String name = file.getFullName();
+		long total = 0;
+		try {
+			byte[] buffer = new byte[TRANSFER_SIZE];
+			boolean first = true;
+			while (true) {
+				int len = stream.read(buffer);
+				if (len < 0)
+					break;
+				total += len;
+				if (log.isDebugEnabled())
+					log.debug("read " + len
+							+ " bytes from source stream (total: " + total
+							+ ") bound for " + name);
+				if (len == buffer.length) {
+					if (first)
+						file.setContents(buffer);
+					else
+						file.appendContents(buffer);
+				} else {
+					byte[] newBuf = new byte[len];
+					System.arraycopy(buffer, 0, newBuf, 0, len);
+					if (first)
+						file.setContents(newBuf);
+					else
+						file.appendContents(newBuf);
+				}
+				first = false;
+			}
+		} catch (IOException exn) {
+			throw new FilesystemAccessException("failed to transfer bytes", exn);
+		}
+	}
+
+	/**
+	 * Build a description of the profiles supported by a workflow. Note that we
+	 * expect the set of profiles to be fairly small.
+	 * 
+	 * @param workflow
+	 *            The workflow to describe the profiles of.
+	 * @return The descriptor (which might be empty).
+	 */
+	public ProfileList getProfileDescriptor(Workflow workflow) {
+		ProfileList result = new ProfileList();
+		String main = workflow.getMainProfileName();
+		for (Profile p : workflow.getProfiles()) {
+			ProfileList.Info i = new ProfileList.Info();
+			i.name = p.getName();
+			if (main != null && main.equals(i.name))
+				i.main = true;
+			result.profile.add(i);
+		}
+		return result;
+	}
+
+	public boolean getAllowStartWorkflowRuns() {
+		return runFactory.isAllowingRunsToStart();
+	}
+
+	/**
+	 * The list of filenames that logs may occupy.
+	 */
+	private static final String[] LOGS = { "logs/detail.log.4",
+			"logs/detail.log.3", "logs/detail.log.2", "logs/detail.log.1",
+			"logs/detail.log" };
+
+	public FileConcatenation getLogs(TavernaRun run) {
+		FileConcatenation fc = new FileConcatenation();
+		for (String name : LOGS) {
+			try {
+				fc.add(fileUtils.getFile(run, name));
+			} catch (FilesystemAccessException | NoDirectoryEntryException e) {
+				// Ignore
+			}
+		}
+		return fc;
+	}
+
+	@Nonnull
+	public List<Capability> getCapabilities() {
+		return capabilitySource.getCapabilities();
+	}
+
+	static final String PROV_BUNDLE = "out.bundle.zip";
+
+	public FileConcatenation getProv(TavernaRun run) {
+		FileConcatenation fc = new FileConcatenation();
+		try {
+			fc.add(fileUtils.getFile(run, PROV_BUNDLE));
+		} catch (FilesystemAccessException | NoDirectoryEntryException e) {
+			// Ignore
+		}
+		return fc;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/admin/Admin.java b/server-webapp/src/main/java/org/taverna/server/master/admin/Admin.java
new file mode 100644
index 0000000..184c7b5
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/admin/Admin.java
@@ -0,0 +1,1100 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.admin;
+
+import static org.taverna.server.master.admin.Paths.ALLOW_NEW;
+import static org.taverna.server.master.admin.Paths.ARGS;
+import static org.taverna.server.master.admin.Paths.EXEC_WF;
+import static org.taverna.server.master.admin.Paths.EXITCODE;
+import static org.taverna.server.master.admin.Paths.FACTORIES;
+import static org.taverna.server.master.admin.Paths.GEN_PROV;
+import static org.taverna.server.master.admin.Paths.INVOKES;
+import static org.taverna.server.master.admin.Paths.JAR_FORKER;
+import static org.taverna.server.master.admin.Paths.JAR_WORKER;
+import static org.taverna.server.master.admin.Paths.JAVA;
+import static org.taverna.server.master.admin.Paths.LIFE;
+import static org.taverna.server.master.admin.Paths.LOG_EXN;
+import static org.taverna.server.master.admin.Paths.LOG_WFS;
+import static org.taverna.server.master.admin.Paths.OPERATING;
+import static org.taverna.server.master.admin.Paths.OP_LIMIT;
+import static org.taverna.server.master.admin.Paths.PASSFILE;
+import static org.taverna.server.master.admin.Paths.PERM_WF;
+import static org.taverna.server.master.admin.Paths.REG_HOST;
+import static org.taverna.server.master.admin.Paths.REG_JAR;
+import static org.taverna.server.master.admin.Paths.REG_POLL;
+import static org.taverna.server.master.admin.Paths.REG_PORT;
+import static org.taverna.server.master.admin.Paths.REG_WAIT;
+import static org.taverna.server.master.admin.Paths.ROOT;
+import static org.taverna.server.master.admin.Paths.RUNS;
+import static org.taverna.server.master.admin.Paths.RUN_LIMIT;
+import static org.taverna.server.master.admin.Paths.STARTUP;
+import static org.taverna.server.master.admin.Paths.TOTAL_RUNS;
+import static org.taverna.server.master.admin.Paths.URS;
+import static org.taverna.server.master.admin.Paths.UR_FILE;
+import static org.taverna.server.master.admin.Paths.USER;
+import static org.taverna.server.master.admin.Paths.USERS;
+import static org.taverna.server.master.admin.Types.JSON;
+import static org.taverna.server.master.admin.Types.PLAIN;
+import static org.taverna.server.master.admin.Types.XML;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.ogf.usage.JobUsageRecord;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+
+/**
+ * The administration interface for Taverna Server.
+ * 
+ * @author Donal Fellows
+ */
+@Description("Administration interface for Taverna Server.")
+public interface Admin {
+	/**
+	 * Get a simple administration user interface.
+	 * 
+	 * @return The description document in a response.
+	 * @throws IOException
+	 */
+	@GET
+	@Path(ROOT)
+	@Produces("text/html")
+	@Nonnull
+	Response getUserInterface() throws IOException;
+
+	/**
+	 * Gets support resources for the administration user interface.
+	 * 
+	 * @param file
+	 *            The name of the static resource to provide.
+	 * @return The requested document in a response.
+	 * @throws IOException
+	 */
+	@GET
+	@Path("static/{file}")
+	@Produces("*/*")
+	Response getStaticResource(@PathParam("file") String file)
+			throws IOException;
+
+	/**
+	 * Get a description of the administration interface.
+	 * 
+	 * @param ui
+	 *            What URI was used to access this resource?
+	 * @return The description document.
+	 */
+	@GET
+	@Path(ROOT)
+	@Produces({ XML, JSON })
+	@Nonnull
+	AdminDescription getDescription(@Context UriInfo ui);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(ROOT)
+	Response optionsRoot();
+
+	/**
+	 * Get whether to allow new workflow runs to be created.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(ALLOW_NEW)
+	@Produces(PLAIN)
+	@Description("Whether to allow new workflow runs to be created.")
+	boolean getAllowNew();
+
+	/**
+	 * Set whether to allow new workflow runs to be created.
+	 * 
+	 * @param newValue
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(ALLOW_NEW)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("Whether to allow new workflow runs to be created.")
+	boolean setAllowNew(boolean newValue);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(ALLOW_NEW)
+	@Description("Whether to allow new workflow runs to be created.")
+	Response optionsAllowNew();
+
+	/**
+	 * Get whether to log the workflows submitted.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(LOG_WFS)
+	@Produces(PLAIN)
+	@Description("Whether to log the workflows submitted.")
+	boolean getLogWorkflows();
+
+	/**
+	 * Set whether to log the workflows submitted.
+	 * 
+	 * @param logWorkflows
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(LOG_WFS)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("Whether to log the workflows submitted.")
+	boolean setLogWorkflows(boolean logWorkflows);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(LOG_WFS)
+	@Description("Whether to log the workflows submitted.")
+	Response optionsLogWorkflows();
+
+	/**
+	 * Get whether to log the user-directed faults.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(LOG_EXN)
+	@Produces(PLAIN)
+	@Description("Whether to log the user-directed faults.")
+	boolean getLogFaults();
+
+	/**
+	 * Set whether to log the user-directed faults.
+	 * 
+	 * @param logFaults
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(LOG_EXN)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("Whether to log the user-directed faults.")
+	boolean setLogFaults(boolean logFaults);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(LOG_EXN)
+	@Description("Whether to log the user-directed faults.")
+	Response optionsLogFaults();
+
+	/**
+	 * Get what file to dump usage records to.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(UR_FILE)
+	@Produces(PLAIN)
+	@Description("What file to dump usage records to.")
+	@Nonnull
+	String getURFile();
+
+	/**
+	 * Set what file to dump usage records to.
+	 * 
+	 * @param urFile
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(UR_FILE)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What file to dump usage records to.")
+	@Nonnull
+	String setURFile(@Nonnull String urFile);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(UR_FILE)
+	@Description("What file to dump usage records to.")
+	Response optionsURFile();
+
+	/**
+	 * The property for the number of times the service methods have been
+	 * invoked.
+	 * 
+	 * @return The property value (read-only).
+	 */
+	@GET
+	@Path(INVOKES)
+	@Produces(PLAIN)
+	@Description("How many times have the service methods been invoked.")
+	int invokeCount();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(INVOKES)
+	@Description("How many times have the service methods been invoked.")
+	Response optionsInvokationCount();
+
+	/**
+	 * The property for the number of runs that are currently in existence.
+	 * 
+	 * @return The property value (read-only).
+	 */
+	@GET
+	@Path(TOTAL_RUNS)
+	@Produces(PLAIN)
+	@Description("How many runs are currently in existence.")
+	int runCount();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(TOTAL_RUNS)
+	@Description("How many runs are currently in existence.")
+	Response optionsRunCount();
+
+	/**
+	 * The property for the number of runs that are currently running.
+	 * 
+	 * @return The property value (read-only).
+	 */
+	@GET
+	@Path(OPERATING)
+	@Produces(PLAIN)
+	@Description("How many runs are currently actually running.")
+	int operatingCount();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(OPERATING)
+	@Description("How many runs are currently actually running.")
+	Response optionsOperatingCount();
+
+	/**
+	 * Get the full pathname of the RMI registry's JAR.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(REG_JAR)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the server's custom RMI registry executable JAR file?")
+	@Nonnull
+	String getRegistryJar();
+
+	/**
+	 * Set the full pathname of the RMI registry's JAR.
+	 * 
+	 * @param registryJar
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(REG_JAR)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the server's custom RMI registry executable JAR file?")
+	@Nonnull
+	String setRegistryJar(@Nonnull String registryJar);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(REG_JAR)
+	@Description("What is the full pathname of the server's special custom RMI registry executable JAR file?")
+	Response optionsRegistryJar();
+
+	/**
+	 * Get the location of the RMI registry.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(REG_HOST)
+	@Produces(PLAIN)
+	@Description("Where is the RMI registry?")
+	@Nonnull
+	String getRegistryHost();
+
+	/**
+	 * Set the location of the RMI registry.
+	 * 
+	 * @param registryHost
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(REG_HOST)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("Where is the RMI registry?")
+	@Nonnull
+	String setRegistryHost(@Nonnull String registryHost);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(REG_HOST)
+	@Description("Where is the RMI registry?")
+	Response optionsRegistryHost();
+
+	/**
+	 * Get the port of the RMI registry.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(REG_PORT)
+	@Produces(PLAIN)
+	@Description("On what port is the RMI registry?")
+	int getRegistryPort();
+
+	/**
+	 * Set the port of the RMI registry.
+	 * 
+	 * @param registryPort
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(REG_PORT)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("On what port is the RMI registry?")
+	int setRegistryPort(int registryPort);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(REG_PORT)
+	@Description("On what port is the RMI registry?")
+	Response optionsRegistryPort();
+
+	/**
+	 * Get the maximum number of simultaneous runs.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(RUN_LIMIT)
+	@Produces(PLAIN)
+	@Description("What is the maximum number of simultaneous runs?")
+	int getRunLimit();
+
+	/**
+	 * Set the maximum number of simultaneous runs.
+	 * 
+	 * @param runLimit
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(RUN_LIMIT)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the maximum number of simultaneous runs?")
+	int setRunLimit(int runLimit);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(RUN_LIMIT)
+	@Description("What is the maximum number of simultaneous runs?")
+	Response optionsRunLimit();
+
+	/**
+	 * Get the maximum number of simultaneous executing runs.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(OP_LIMIT)
+	@Produces(PLAIN)
+	@Description("What is the maximum number of simultaneous executing runs?")
+	int getOperatingLimit();
+
+	/**
+	 * Set the maximum number of simultaneous executing runs.
+	 * 
+	 * @param operatingLimit
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(OP_LIMIT)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the maximum number of simultaneous executing runs?")
+	int setOperatingLimit(int operatingLimit);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(OP_LIMIT)
+	@Description("What is the maximum number of simultaneous executing runs?")
+	Response optionsOperatingLimit();
+
+	/**
+	 * Get the default lifetime of workflow runs.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(LIFE)
+	@Produces(PLAIN)
+	@Description("What is the default lifetime of workflow runs, in seconds?")
+	int getDefaultLifetime();
+
+	/**
+	 * Set the default lifetime of workflow runs.
+	 * 
+	 * @param defaultLifetime
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(LIFE)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the default lifetime of workflow runs, in seconds?")
+	int setDefaultLifetime(int defaultLifetime);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(LIFE)
+	@Description("What is the default lifetime of workflow runs, in seconds?")
+	Response optionsDefaultLifetime();
+
+	/**
+	 * The property for the list of IDs of current runs.
+	 * 
+	 * @return The property value (read-only).
+	 */
+	@GET
+	@Path(RUNS)
+	@Produces({ XML, JSON })
+	@Description("List the IDs of all current runs.")
+	StringList currentRuns();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(RUNS)
+	@Description("List the IDs of all current runs.")
+	Response optionsCurrentRuns();
+
+	/**
+	 * Get the Java binary to be used for execution of subprocesses.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(JAVA)
+	@Produces(PLAIN)
+	@Description("Which Java binary should be used for execution of subprocesses?")
+	@Nonnull
+	String getJavaBinary();
+
+	/**
+	 * Set the Java binary to be used for execution of subprocesses.
+	 * 
+	 * @param javaBinary
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(JAVA)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("Which Java binary should be used for execution of subprocesses?")
+	@Nonnull
+	String setJavaBinary(@Nonnull String javaBinary);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(JAVA)
+	@Description("Which Java binary should be used for execution of subprocesses?")
+	Response optionsJavaBinary();
+
+	/**
+	 * Get the extra arguments to be supplied to Java subprocesses.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(ARGS)
+	@Produces({ XML, JSON })
+	@Description("What extra arguments should be supplied to Java subprocesses?")
+	@Nonnull
+	StringList getExtraArguments();
+
+	/**
+	 * Set the extra arguments to be supplied to Java subprocesses.
+	 * 
+	 * @param extraArguments
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(ARGS)
+	@Consumes(XML)
+	@Produces({ XML, JSON })
+	@Description("What extra arguments should be supplied to Java subprocesses?")
+	@Nonnull
+	StringList setExtraArguments(@Nonnull StringList extraArguments);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(ARGS)
+	@Description("What extra arguments should be supplied to Java subprocesses?")
+	Response optionsExtraArguments();
+
+	/**
+	 * Get the full pathname of the worker JAR file.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(JAR_WORKER)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the server's per-user worker executable JAR file?")
+	@Nonnull
+	String getServerWorkerJar();
+
+	/**
+	 * Set the full pathname of the worker JAR file.
+	 * 
+	 * @param serverWorkerJar
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(JAR_WORKER)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the server's per-user worker executable JAR file?")
+	@Nonnull
+	String setServerWorkerJar(@Nonnull String serverWorkerJar);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(JAR_WORKER)
+	@Description("What is the full pathname of the server's per-user worker executable JAR file?")
+	Response optionsServerWorkerJar();
+
+	/**
+	 * Get the full pathname of the executeWorkflow.sh file.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(EXEC_WF)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the core Taverna executeWorkflow script?")
+	@Nonnull
+	String getExecuteWorkflowScript();
+
+	/**
+	 * Set the full pathname of the executeWorkflow.sh file.
+	 * 
+	 * @param executeWorkflowScript
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(EXEC_WF)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the core Taverna executeWorkflow script?")
+	@Nonnull
+	String setExecuteWorkflowScript(@Nonnull String executeWorkflowScript);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(EXEC_WF)
+	@Description("What is the full pathname of the core Taverna executeWorkflow script?")
+	Response optionsExecuteWorkflowScript();
+
+	/**
+	 * Get the total duration of time to wait for the start of the forker
+	 * process.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(REG_WAIT)
+	@Produces(PLAIN)
+	@Description("How long in total should the core wait for registration of the \"forker\" process, in seconds.")
+	int getRegistrationWaitSeconds();
+
+	/**
+	 * Set the total duration of time to wait for the start of the forker
+	 * process.
+	 * 
+	 * @param registrationWaitSeconds
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(REG_WAIT)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("How long in total should the core wait for registration of the \"forker\" process, in seconds.")
+	int setRegistrationWaitSeconds(int registrationWaitSeconds);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(REG_WAIT)
+	@Description("How long in total should the core wait for registration of the \"forker\" process, in seconds.")
+	Response optionsRegistrationWaitSeconds();
+
+	/**
+	 * Get the interval between checks for registration of the forker process.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(REG_POLL)
+	@Produces(PLAIN)
+	@Description("What is the interval between checks for registration of the \"forker\" process, in milliseconds.")
+	int getRegistrationPollMillis();
+
+	/**
+	 * Set the interval between checks for registration of the forker process.
+	 * 
+	 * @param registrationPollMillis
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(REG_POLL)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the interval between checks for registration of the \"forker\" process, in milliseconds.")
+	int setRegistrationPollMillis(int registrationPollMillis);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(REG_POLL)
+	@Description("What is the interval between checks for registration of the \"forker\" process, in milliseconds.")
+	Response optionsRegistrationPollMillis();
+
+	/**
+	 * Get the full pathname of the file containing the impersonation
+	 * credentials for the forker process.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(PASSFILE)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the file containing the password used for impersonating other users? (On Unix, this is the password for the deployment user to use \"sudo\".)")
+	@Nonnull
+	String getRunasPasswordFile();
+
+	/**
+	 * Set the full pathname of the file containing the impersonation
+	 * credentials for the forker process.
+	 * 
+	 * @param runasPasswordFile
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(PASSFILE)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the file containing the password used for impersonating other users? (On Unix, this is the password for the deployment user to use \"sudo\".)")
+	@Nonnull
+	String setRunasPasswordFile(@Nonnull String runasPasswordFile);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(PASSFILE)
+	@Description("What is the full pathname of the file containing the password used for impersonating other users? (On Unix, this is the password for the deployment user to use \"sudo\".)")
+	Response optionsRunasPasswordFile();
+
+	/**
+	 * Get the full pathname of the forker's JAR.
+	 * 
+	 * @return The current setting.
+	 */
+	@GET
+	@Path(JAR_FORKER)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the server's special authorized \"forker\" executable JAR file?")
+	@Nonnull
+	String getServerForkerJar();
+
+	/**
+	 * Set the full pathname of the forker's JAR.
+	 * 
+	 * @param serverForkerJar
+	 *            What to set it to.
+	 * @return The new setting.
+	 */
+	@PUT
+	@Path(JAR_FORKER)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("What is the full pathname of the server's special authorized \"forker\" executable JAR file?")
+	@Nonnull
+	String setServerForkerJar(@Nonnull String serverForkerJar);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(JAR_FORKER)
+	@Description("What is the full pathname of the server's special authorized \"forker\" executable JAR file?")
+	Response optionsServerForkerJar();
+
+	/**
+	 * The property for the length of time it took to start the forker.
+	 * 
+	 * @return The property value (read-only).
+	 */
+	@GET
+	@Path(STARTUP)
+	@Produces(PLAIN)
+	@Description("How long did it take for the back-end \"forker\" to set itself up, in seconds.")
+	int startupTime();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(STARTUP)
+	@Description("How long did it take for the back-end \"forker\" to set itself up, in seconds.")
+	Response optionsStartupTime();
+
+	/**
+	 * The property for the last exit code of the forker process.
+	 * 
+	 * @return The property value (read-only).
+	 */
+	@GET
+	@Path(EXITCODE)
+	@Produces(PLAIN)
+	@Description("What was the last exit code of the \"forker\"? If null, no exit has ever been recorded.")
+	Integer lastExitCode();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(EXITCODE)
+	@Description("What was the last exit code of the \"forker\"? If null, no exit has ever been recorded.")
+	Response optionsLastExitCode();
+
+	/**
+	 * The property for the mapping of usernames to factory process handles.
+	 * 
+	 * @return The property value (read-only).
+	 */
+	@GET
+	@Path(FACTORIES)
+	@Produces({ XML, JSON })
+	@Description("What is the mapping of local usernames to factory process RMI IDs?")
+	StringList factoryProcessMapping();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(FACTORIES)
+	@Description("What is the mapping of local usernames to factory process RMI IDs?")
+	Response optionsFactoryProcessMapping();
+
+	/**
+	 * The property for the list of usage records collected.
+	 * 
+	 * @return The property value (read-only).
+	 */
+	@GET
+	@Path(URS)
+	@Produces(XML)
+	@Description("What is the list of usage records that have been collected?")
+	URList usageRecords();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(URS)
+	@Description("What is the list of usage records that have been collected?")
+	Response optionsUsageRecords();
+
+	/**
+	 * What are the current list of workflow URIs that may be started? Empty
+	 * means allow any, including user-supplied workflows.
+	 * 
+	 * @return List of URIs, encoded as strings.
+	 */
+	@GET
+	@Path(PERM_WF)
+	@Produces({ XML, JSON })
+	@Description("What are the current list of workflow URIs that may be started? Empty means allow any, including user-supplied workflows.")
+	StringList getPermittedWorkflowURIs();
+
+	/** Do we turn on the generate provenance option by default? */
+	@GET
+	@Path(GEN_PROV)
+	@Produces(PLAIN)
+	@Description("Do we turn on the generate provenance option by default? (boolean)")
+	String getGenerateProvenance();
+
+	/** Do we turn on the generate provenance option by default? */
+	@PUT
+	@Path(GEN_PROV)
+	@Consumes(PLAIN)
+	@Produces(PLAIN)
+	@Description("Do we turn on the generate provenance option by default? (boolean)")
+	String setGenerateProvenance(String newValue);
+
+	/** Do we turn on the generate provenance option by default? */
+	@OPTIONS
+	@Path(GEN_PROV)
+	@Description("Do we turn on the generate provenance option by default? (boolean)")
+	Response optionsGenerateProvenance();
+
+	/**
+	 * What are the current list of workflow URIs that may be started? Empty
+	 * means allow any, including user-supplied workflows.
+	 * 
+	 * @param permitted
+	 *            List of URIs, encoded as strings.
+	 * @return List of URIs, encoded as strings.
+	 */
+	@PUT
+	@Path(PERM_WF)
+	@Consumes(XML)
+	@Produces({ XML, JSON })
+	@Description("What are the current list of workflow URIs that may be started? Empty means allow any, including user-supplied workflows.")
+	StringList setPermittedWorkflowURIs(@Nonnull StringList permitted);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(PERM_WF)
+	@Description("What are the current list of workflow URIs that may be started? Empty means allow any, including user-supplied workflows.")
+	Response optionsPermittedWorkflowURIs();
+
+	@GET
+	@Path(USERS)
+	@Produces({ XML, JSON })
+	@Description("What users are known to the server?")
+	UserList users(@Context UriInfo ui);
+
+	@GET
+	@Path(USER)
+	@Produces({ XML, JSON })
+	@Description("What do we know about a particular user?")
+	UserDesc user(@PathParam("id") String username);
+
+	@POST
+	@Path(USERS)
+	@Consumes(XML)
+	@Description("Create a user.")
+	Response useradd(UserDesc userdesc, @Nonnull @Context UriInfo ui);
+
+	@PUT
+	@Path(USER)
+	@Produces({ XML, JSON })
+	@Consumes(XML)
+	@Description("Update a user.")
+	UserDesc userset(@PathParam("id") String username, UserDesc userdesc);
+
+	@DELETE
+	@Path(USER)
+	@Produces({ XML, JSON })
+	@Description("What do we know about a particular user?")
+	Response userdel(@PathParam("id") String username);
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(USERS)
+	@Description("What users are known to the server?")
+	Response optionsUsers();
+
+	/** What HTTP methods may we use? */
+	@OPTIONS
+	@Path(USER)
+	@Description("What do we know about a particular user?")
+	Response optionsUser(@PathParam("id") String username);
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+
+	/**
+	 * The description of what properties are supported by the administration
+	 * interface.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "description")
+	@XmlType(name = "Description")
+	public static class AdminDescription extends VersionedElement {
+		public Uri allowNew;
+		public Uri logWorkflows;
+		public Uri logFaults;
+		public Uri usageRecordDumpFile;
+		public Uri invokationCount;
+		public Uri runCount;
+		public Uri registryHost;
+		public Uri registryPort;
+		public Uri runLimit;
+		public Uri defaultLifetime;
+		public Uri currentRuns;
+		public Uri javaBinary;
+		public Uri extraArguments;
+		public Uri serverWorkerJar;
+		public Uri serverForkerJar;
+		public Uri executeWorkflowScript;
+		public Uri registrationWaitSeconds;
+		public Uri registrationPollMillis;
+		public Uri runasPasswordFile;
+		public Uri startupTime;
+		public Uri lastExitCode;
+		public Uri factoryProcessMapping;
+		public Uri usageRecords;
+		public Uri users;
+		public Uri operatingLimit;
+		public Uri operatingCount;
+		public Uri permittedWorkflowURIs;
+		public Uri generateProvenance;
+
+		public AdminDescription() {
+		}
+
+		public AdminDescription(UriInfo ui) {
+			allowNew = new Uri(ui, ALLOW_NEW);
+			logWorkflows = new Uri(ui, LOG_WFS);
+			logFaults = new Uri(ui, LOG_EXN);
+			usageRecordDumpFile = new Uri(ui, UR_FILE);
+			invokationCount = new Uri(ui, INVOKES);
+			runCount = new Uri(ui, TOTAL_RUNS);
+			registryHost = new Uri(ui, REG_HOST);
+			registryPort = new Uri(ui, REG_PORT);
+			runLimit = new Uri(ui, RUN_LIMIT);
+			defaultLifetime = new Uri(ui, LIFE);
+			currentRuns = new Uri(ui, RUNS);
+			javaBinary = new Uri(ui, JAVA);
+			extraArguments = new Uri(ui, ARGS);
+			serverWorkerJar = new Uri(ui, JAR_WORKER);
+			serverForkerJar = new Uri(ui, JAR_FORKER);
+			executeWorkflowScript = new Uri(ui, EXEC_WF);
+			registrationWaitSeconds = new Uri(ui, REG_WAIT);
+			registrationPollMillis = new Uri(ui, REG_POLL);
+			runasPasswordFile = new Uri(ui, PASSFILE);
+			startupTime = new Uri(ui, STARTUP);
+			lastExitCode = new Uri(ui, EXITCODE);
+			factoryProcessMapping = new Uri(ui, FACTORIES);
+			usageRecords = new Uri(ui, URS);
+			users = new Uri(ui, USERS);
+			operatingLimit = new Uri(ui, OP_LIMIT);
+			operatingCount = new Uri(ui, OPERATING);
+			permittedWorkflowURIs = new Uri(ui, PERM_WF);
+			generateProvenance = new Uri(ui, GEN_PROV);
+		}
+	}
+
+	/**
+	 * A list of strings, as XML.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "stringList")
+	@XmlType(name = "StringList")
+	public static class StringList {
+		@XmlElement
+		public List<String> string = new ArrayList<>();
+	}
+
+	/**
+	 * A list of users, as XML.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "userList")
+	@XmlType(name = "UserList")
+	public static class UserList {
+		@XmlElement
+		public List<URI> user = new ArrayList<>();
+	}
+
+	@XmlRootElement(name = "userDesc")
+	@XmlType(name = "UserDesc")
+	public static class UserDesc {
+		@XmlElement
+		public String username;
+		@XmlElement
+		public String password;
+		@XmlElement
+		public String localUserId;
+		@XmlElement
+		public Boolean enabled;
+		@XmlElement
+		public Boolean admin;
+	}
+
+	/**
+	 * A list of usage records, as XML.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "usageRecordList")
+	@XmlType(name = "UsageRecords")
+	public static class URList {
+		@XmlElement
+		public List<JobUsageRecord> usageRecord;
+	}
+}
+
+interface Paths {
+	static final String ROOT = "/";
+	static final String ALLOW_NEW = "allowNew";
+	static final String LOG_WFS = "logWorkflows";
+	static final String LOG_EXN = "logFaults";
+	static final String UR_FILE = "usageRecordDumpFile";
+	static final String INVOKES = "invokationCount";
+	static final String TOTAL_RUNS = "runCount";
+	static final String OPERATING = "operatingCount";
+	static final String REG_HOST = "registryHost";
+	static final String REG_PORT = "registryPort";
+	static final String REG_JAR = "registryJar";
+	static final String RUN_LIMIT = "runLimit";
+	static final String OP_LIMIT = "operatingLimit";
+	static final String LIFE = "defaultLifetime";
+	static final String RUNS = "currentRuns";
+	static final String JAVA = "javaBinary";
+	static final String ARGS = "extraArguments";
+	static final String JAR_WORKER = "serverWorkerJar";
+	static final String JAR_FORKER = "serverForkerJar";
+	static final String EXEC_WF = "executeWorkflowScript";
+	static final String REG_WAIT = "registrationWaitSeconds";
+	static final String REG_POLL = "registrationPollMillis";
+	static final String PASSFILE = "runasPasswordFile";
+	static final String STARTUP = "startupTime";
+	static final String EXITCODE = "lastExitCode";
+	static final String FACTORIES = "factoryProcessMapping";
+	static final String URS = "usageRecords";
+	static final String PERM_WF = "permittedWorkflowURIs";
+	static final String GEN_PROV = "generateProvenance";
+	static final String USERS = "users";
+	static final String USER = USERS + "/{id}";
+}
+
+interface Types {
+	static final String PLAIN = "text/plain";
+	static final String XML = "application/xml";
+	static final String JSON = "application/json";
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/admin/AdminBean.java b/server-webapp/src/main/java/org/taverna/server/master/admin/AdminBean.java
new file mode 100644
index 0000000..b83d88f
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/admin/AdminBean.java
@@ -0,0 +1,794 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.admin;
+
+import static java.util.Arrays.asList;
+import static java.util.UUID.randomUUID;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+import static org.taverna.server.master.common.Roles.ADMIN;
+import static org.taverna.server.master.common.Uri.secure;
+import static org.taverna.server.master.utils.RestUtils.opt;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.commons.io.IOUtils;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.ManagementModel;
+import org.taverna.server.master.exceptions.GeneralFailureException;
+import org.taverna.server.master.factories.ConfigurableRunFactory;
+import org.taverna.server.master.identity.User;
+import org.taverna.server.master.identity.UserStoreAPI;
+import org.taverna.server.master.usage.UsageRecordRecorder;
+import org.taverna.server.master.utils.InvocationCounter;
+import org.taverna.server.master.worker.RunDBSupport;
+import org.taverna.server.master.worker.WorkerModel;
+
+/**
+ * The administration interface to Taverna Server.
+ * 
+ * @author Donal Fellows
+ */
+public class AdminBean implements Admin {
+	@Required
+	public void setState(ManagementModel state) {
+		this.state = state;
+	}
+
+	@Required
+	public void setCounter(InvocationCounter counter) {
+		this.counter = counter;
+	}
+
+	@Required
+	public void setRunDB(RunDBSupport runDB) {
+		this.runDB = runDB;
+	}
+
+	@Required
+	public void setFactory(ConfigurableRunFactory factory) {
+		this.factory = factory;
+	}
+
+	@Required
+	public void setUsageRecords(UsageRecordRecorder usageRecords) {
+		this.usageRecords = usageRecords;
+	}
+
+	@Required
+	public void setUserStore(UserStoreAPI userStore) {
+		this.userStore = userStore;
+	}
+
+	@Required
+	public void setLocalWorkerModel(WorkerModel worker) {
+		localWorker = worker;
+	}
+
+	public void setAdminHtmlFile(String filename) {
+		this.adminHtmlFile = filename;
+	}
+
+	public void setResourceRoot(String root) {
+		this.resourceRoot = root;
+	}
+
+	protected byte[] getResource(String name) throws IOException {
+		if (AdminBean.class.getResource(name) == null)
+			throw new FileNotFoundException(name);
+		return IOUtils.toByteArray(AdminBean.class.getResourceAsStream(name));
+	}
+
+	private ManagementModel state;
+	private InvocationCounter counter;
+	private RunDBSupport runDB;
+	private ConfigurableRunFactory factory;
+	private UsageRecordRecorder usageRecords;
+	private UserStoreAPI userStore;
+	private WorkerModel localWorker;
+	private String adminHtmlFile = "/admin.html";
+	private String resourceRoot = "/static/";
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response getUserInterface() throws IOException {
+		return Response.ok(getResource(adminHtmlFile), "text/html").build();
+	}
+
+	@Override
+	@RolesAllowed(ADMIN)
+	public Response getStaticResource(String file) throws IOException {
+		if (file.matches("^[-_.a-zA-Z0-9]+$")) {
+			String type = "application/octet-stream";
+			if (file.endsWith(".html"))
+				type = "text/html";
+			else if (file.endsWith(".js"))
+				type = "text/javascript";
+			else if (file.endsWith(".jpg"))
+				type = "image/jpeg";
+			else if (file.endsWith(".gif"))
+				type = "image/gif";
+			else if (file.endsWith(".png"))
+				type = "image/png";
+			else if (file.endsWith(".svg"))
+				type = "image/svg+xml";
+			else if (file.endsWith(".css"))
+				type = "text/css";
+			try {
+				return Response.ok(getResource(resourceRoot + file), type)
+						.build();
+			} catch (IOException e) {
+				// ignore; we just treat as 404
+			}
+		}
+		return Response.status(NOT_FOUND).entity("no such resource").build();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public AdminDescription getDescription(UriInfo ui) {
+		return new AdminDescription(ui);
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsRoot() {
+		return opt();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public boolean getAllowNew() {
+		return state.getAllowNewWorkflowRuns();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public boolean setAllowNew(boolean newValue) {
+		state.setAllowNewWorkflowRuns(newValue);
+		return state.getAllowNewWorkflowRuns();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsAllowNew() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public boolean getLogWorkflows() {
+		return state.getLogIncomingWorkflows();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public boolean setLogWorkflows(boolean newValue) {
+		state.setLogIncomingWorkflows(newValue);
+		return state.getLogIncomingWorkflows();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsLogWorkflows() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public boolean getLogFaults() {
+		return state.getLogOutgoingExceptions();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public boolean setLogFaults(boolean newValue) {
+		state.setLogOutgoingExceptions(newValue);
+		return state.getLogOutgoingExceptions();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsLogFaults() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getURFile() {
+		return state.getUsageRecordLogFile();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setURFile(String newValue) {
+		state.setUsageRecordLogFile(newValue);
+		return state.getUsageRecordLogFile();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsURFile() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int invokeCount() {
+		return counter.getCount();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsInvokationCount() {
+		return opt();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int runCount() {
+		return runDB.countRuns();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsRunCount() {
+		return opt();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getRegistryHost() {
+		return factory.getRegistryHost();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setRegistryHost(String newValue) {
+		factory.setRegistryHost(newValue);
+		return factory.getRegistryHost();
+	}
+
+	@Override
+	public Response optionsRegistryHost() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int getRegistryPort() {
+		return factory.getRegistryPort();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int setRegistryPort(int newValue) {
+		factory.setRegistryPort(newValue);
+		return factory.getRegistryPort();
+	}
+
+	@Override
+	public Response optionsRegistryPort() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getRegistryJar() {
+		return factory.getRmiRegistryJar();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setRegistryJar(String registryJar) {
+		factory.setRmiRegistryJar(registryJar);
+		return factory.getRmiRegistryJar();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsRegistryJar() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int getRunLimit() {
+		return factory.getMaxRuns();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int setRunLimit(int newValue) {
+		factory.setMaxRuns(newValue);
+		return factory.getMaxRuns();
+	}
+
+	@Override
+	public Response optionsRunLimit() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int getDefaultLifetime() {
+		return factory.getDefaultLifetime();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int setDefaultLifetime(int newValue) {
+		factory.setDefaultLifetime(newValue);
+		return factory.getDefaultLifetime();
+	}
+
+	@Override
+	public Response optionsDefaultLifetime() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public StringList currentRuns() {
+		StringList result = new StringList();
+		result.string = runDB.listRunNames();
+		return result;
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsCurrentRuns() {
+		return opt();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getJavaBinary() {
+		return factory.getJavaBinary();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setJavaBinary(String newValue) {
+		factory.setJavaBinary(newValue);
+		return factory.getJavaBinary();
+	}
+
+	@Override
+	public Response optionsJavaBinary() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public StringList getExtraArguments() {
+		String[] xargs = factory.getExtraArguments();
+		StringList result = new StringList();
+		result.string = asList(xargs == null ? new String[0] : xargs);
+		return result;
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public StringList setExtraArguments(StringList newValue) {
+		if (newValue == null || newValue.string == null)
+			factory.setExtraArguments(new String[0]);
+		else
+			factory.setExtraArguments(newValue.string
+					.toArray(new String[newValue.string.size()]));
+		StringList result = new StringList();
+		result.string = asList(factory.getExtraArguments());
+		return result;
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsExtraArguments() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getServerWorkerJar() {
+		return factory.getServerWorkerJar();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setServerWorkerJar(String newValue) {
+		factory.setServerWorkerJar(newValue);
+		return factory.getServerWorkerJar();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsServerWorkerJar() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getExecuteWorkflowScript() {
+		return factory.getExecuteWorkflowScript();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setExecuteWorkflowScript(String newValue) {
+		factory.setExecuteWorkflowScript(newValue);
+		return factory.getExecuteWorkflowScript();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsExecuteWorkflowScript() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int getRegistrationWaitSeconds() {
+		return factory.getWaitSeconds();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int setRegistrationWaitSeconds(int newValue) {
+		factory.setWaitSeconds(newValue);
+		return factory.getWaitSeconds();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsRegistrationWaitSeconds() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int getRegistrationPollMillis() {
+		return factory.getSleepTime();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int setRegistrationPollMillis(int newValue) {
+		factory.setSleepTime(newValue);
+		return factory.getSleepTime();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsRegistrationPollMillis() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getRunasPasswordFile() {
+		return factory.getPasswordFile();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setRunasPasswordFile(String newValue) {
+		factory.setPasswordFile(newValue);
+		return factory.getPasswordFile();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsRunasPasswordFile() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getServerForkerJar() {
+		return factory.getServerForkerJar();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setServerForkerJar(String newValue) {
+		factory.setServerForkerJar(newValue);
+		return factory.getServerForkerJar();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsServerForkerJar() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int startupTime() {
+		return factory.getLastStartupCheckCount();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsStartupTime() {
+		return opt();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Integer lastExitCode() {
+		return factory.getLastExitCode();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsLastExitCode() {
+		return opt();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public StringList factoryProcessMapping() {
+		StringList result = new StringList();
+		result.string = asList(factory.getFactoryProcessMapping());
+		return result;
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsFactoryProcessMapping() {
+		return opt();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public URList usageRecords() {
+		URList result = new URList();
+		result.usageRecord = usageRecords.getUsageRecords();
+		return result;
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsUsageRecords() {
+		return opt();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public UserList users(UriInfo ui) {
+		UserList ul = new UserList();
+		UriBuilder ub = secure(ui).path("{id}");
+		for (String user : userStore.getUserNames())
+			ul.user.add(ub.build(user));
+		return ul;
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsUsers() {
+		return opt("POST");
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public UserDesc user(String username) {
+		UserDesc desc = new UserDesc();
+		User u = userStore.getUser(username);
+		desc.username = u.getUsername();
+		desc.localUserId = u.getLocalUsername();
+		desc.admin = u.isAdmin();
+		desc.enabled = u.isEnabled();
+		return desc;
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsUser(String username) {
+		return opt("PUT", "DELETE");
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response useradd(UserDesc userdesc, UriInfo ui) {
+		if (userdesc.username == null)
+			throw new IllegalArgumentException("no user name supplied");
+		if (userdesc.password == null)
+			userdesc.password = randomUUID().toString();
+		userStore.addUser(userdesc.username, userdesc.password, false);
+		if (userdesc.localUserId != null)
+			userStore.setUserLocalUser(userdesc.username, userdesc.localUserId);
+		if (userdesc.admin != null && userdesc.admin)
+			userStore.setUserAdmin(userdesc.username, true);
+		if (userdesc.enabled != null && userdesc.enabled)
+			userStore.setUserEnabled(userdesc.username, true);
+		return created(secure(ui).path("{id}").build(userdesc.username))
+				.build();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public UserDesc userset(String username, UserDesc userdesc) {
+		if (userdesc.password != null)
+			userStore.setUserPassword(username, userdesc.password);
+		if (userdesc.localUserId != null)
+			userStore.setUserLocalUser(username, userdesc.localUserId);
+		if (userdesc.admin != null)
+			userStore.setUserAdmin(username, userdesc.admin);
+		if (userdesc.enabled != null)
+			userStore.setUserEnabled(username, userdesc.enabled);
+		userdesc = null; // Stop reuse!
+
+		UserDesc desc = new UserDesc();
+		User u = userStore.getUser(username);
+		desc.username = u.getUsername();
+		desc.localUserId = u.getLocalUsername();
+		desc.admin = u.isAdmin();
+		desc.enabled = u.isEnabled();
+		return desc;
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response userdel(String username) {
+		userStore.deleteUser(username);
+		return noContent().build();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int operatingCount() {
+		try {
+			return factory.getOperatingCount();
+		} catch (Exception e) {
+			throw new GeneralFailureException(e);
+		}
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsOperatingCount() {
+		return opt();
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int getOperatingLimit() {
+		return factory.getOperatingLimit();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public int setOperatingLimit(int operatingLimit) {
+		factory.setOperatingLimit(operatingLimit);
+		return factory.getOperatingLimit();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsOperatingLimit() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+	@RolesAllowed(ADMIN)
+	@Override
+	public StringList getPermittedWorkflowURIs() {
+		StringList sl = new StringList();
+		List<URI> uris = localWorker.getPermittedWorkflowURIs();
+		if (uris != null)
+			for (URI uri : uris)
+				sl.string.add(uri.toString());
+		return sl;
+	}
+
+	private static final URI myExp = URI.create("http://www.myexperment.org/");
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public StringList setPermittedWorkflowURIs(StringList permitted) {
+		List<URI> uris = new ArrayList<>();
+		for (String uri : permitted.string)
+			try {
+				uris.add(myExp.resolve(uri));
+			} catch (Exception e) {
+				// Ignore
+			}
+		localWorker.setPermittedWorkflowURIs(uris);
+		return getPermittedWorkflowURIs();
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsPermittedWorkflowURIs() {
+		return opt("PUT");
+	}
+
+	// /////////////////////////////////////////////////////
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String getGenerateProvenance() {
+		return Boolean.toString(localWorker.getGenerateProvenance());
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public String setGenerateProvenance(String newValue) {
+		boolean b = Boolean.parseBoolean(newValue);
+		localWorker.setGenerateProvenance(b);
+		return Boolean.toString(localWorker.getGenerateProvenance());
+	}
+
+	@RolesAllowed(ADMIN)
+	@Override
+	public Response optionsGenerateProvenance() {
+		return opt("PUT");
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/admin/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/admin/package-info.java
new file mode 100644
index 0000000..930fd75
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/admin/package-info.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the RESTful administration interface to Taverna Server.
+ * @author Donal Fellows
+ */
+@XmlSchema(namespace = ADMIN, elementFormDefault = QUALIFIED, attributeFormDefault = QUALIFIED, xmlns = {
+		@XmlNs(prefix = "xlink", namespaceURI = XLINK),
+		@XmlNs(prefix = "ts", namespaceURI = SERVER),
+		@XmlNs(prefix = "ts-rest", namespaceURI = SERVER_REST),
+		@XmlNs(prefix = "ts-soap", namespaceURI = SERVER_SOAP),
+		@XmlNs(prefix = "feed", namespaceURI = FEED),
+		@XmlNs(prefix = "admin", namespaceURI = ADMIN),
+		@XmlNs(prefix = "ur", namespaceURI = UR),
+		@XmlNs(prefix = "ds", namespaceURI = XSIG) })
+package org.taverna.server.master.admin;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.master.common.Namespaces.ADMIN;
+import static org.taverna.server.master.common.Namespaces.FEED;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.SERVER_REST;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Namespaces.UR;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+import static org.taverna.server.master.common.Namespaces.XSIG;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/ContentTypes.java b/server-webapp/src/main/java/org/taverna/server/master/api/ContentTypes.java
new file mode 100644
index 0000000..1a89b8d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/ContentTypes.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.api;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
+import static javax.ws.rs.core.MediaType.APPLICATION_XML_TYPE;
+
+import java.util.List;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Variant;
+
+/**
+ * The content types supported at various points in the REST interface.
+ * 
+ * @author Donal Fellows
+ */
+public interface ContentTypes {
+	/** "application/zip" */
+	public static final MediaType APPLICATION_ZIP_TYPE = new MediaType(
+			"application", "zip");
+
+	/** "application/vnd.taverna.baclava+xml" */
+	public static final MediaType BACLAVA_MEDIA_TYPE = new MediaType(
+			"application", "vnd.taverna.baclava+xml");
+
+	/**
+	 * The media types that we are willing to serve up directories as. Note that
+	 * we <i>only</i> serve directories up as these.
+	 */
+	public static final List<Variant> DIRECTORY_VARIANTS = asList(new Variant(
+			APPLICATION_XML_TYPE, (String) null, "UTF-8"), new Variant(
+			APPLICATION_JSON_TYPE, (String) null, "UTF-8"), new Variant(
+			APPLICATION_ZIP_TYPE, (String) null, null));
+
+	/**
+	 * The baseline set of media types that we are willing to serve up files as.
+	 * Note that we <i>also</i> serve files up as their auto-detected media
+	 * type. In all cases, this means we just shovel the bytes (or characters,
+	 * in the case of <tt>text/*</tt> subtypes) back at the client.
+	 */
+	public static final List<Variant> INITIAL_FILE_VARIANTS = singletonList(new Variant(
+			APPLICATION_OCTET_STREAM_TYPE, (String) null, null));
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/DirectoryBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/DirectoryBean.java
new file mode 100644
index 0000000..8fa5c78
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/DirectoryBean.java
@@ -0,0 +1,16 @@
+package org.taverna.server.master.api;
+
+import org.taverna.server.master.rest.TavernaServerDirectoryREST;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.utils.FilenameUtils;
+
+/**
+ * Description of properties supported by {@link DirectoryREST}.
+ * 
+ * @author Donal Fellows
+ */
+public interface DirectoryBean extends SupportAware {
+	void setFileUtils(FilenameUtils fileUtils);
+
+	TavernaServerDirectoryREST connect(TavernaRun run);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/FeedBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/FeedBean.java
new file mode 100644
index 0000000..5ace6a3
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/FeedBean.java
@@ -0,0 +1,13 @@
+package org.taverna.server.master.api;
+
+import org.taverna.server.master.InteractionFeed;
+import org.taverna.server.master.interaction.InteractionFeedSupport;
+
+/**
+ * Description of properties supported by {@link InteractionFeed}.
+ * 
+ * @author Donal Fellows
+ */
+public interface FeedBean {
+	void setInteractionFeedSupport(InteractionFeedSupport feed);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/InputBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/InputBean.java
new file mode 100644
index 0000000..eecea44
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/InputBean.java
@@ -0,0 +1,21 @@
+package org.taverna.server.master.api;
+
+import javax.ws.rs.core.UriInfo;
+
+import org.taverna.server.master.ContentsDescriptorBuilder;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerInputREST;
+import org.taverna.server.master.utils.FilenameUtils;
+
+/**
+ * Description of properties supported by {@link InputREST}.
+ * 
+ * @author Donal Fellows
+ */
+public interface InputBean extends SupportAware {
+	TavernaServerInputREST connect(TavernaRun run, UriInfo ui);
+
+	void setCdBuilder(ContentsDescriptorBuilder cd);
+
+	void setFileUtils(FilenameUtils fn);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/ListenerPropertyBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/ListenerPropertyBean.java
new file mode 100644
index 0000000..5c480d4
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/ListenerPropertyBean.java
@@ -0,0 +1,15 @@
+package org.taverna.server.master.api;
+
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+
+/**
+ * Description of properties supported by {@link ListenerPropertyREST}.
+ * 
+ * @author Donal Fellows
+ */
+public interface ListenerPropertyBean extends SupportAware {
+	TavernaServerListenersREST.Property connect(Listener listen,
+			TavernaRun run, String propertyName);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/ListenersBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/ListenersBean.java
new file mode 100644
index 0000000..c02bf62
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/ListenersBean.java
@@ -0,0 +1,13 @@
+package org.taverna.server.master.api;
+
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerListenersREST;
+
+/**
+ * Description of properties supported by {@link ListenersREST}.
+ * 
+ * @author Donal Fellows
+ */
+public interface ListenersBean extends SupportAware {
+	TavernaServerListenersREST connect(TavernaRun run);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/ManagementModel.java b/server-webapp/src/main/java/org/taverna/server/master/api/ManagementModel.java
new file mode 100644
index 0000000..6bc6058
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/ManagementModel.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.api;
+
+/**
+ * The model of the webapp's state Java Bean.
+ * 
+ * @author Donal Fellows
+ */
+public interface ManagementModel {
+	/**
+	 * @return whether we allow the creation of new workflow runs.
+	 */
+	boolean getAllowNewWorkflowRuns();
+
+	/**
+	 * @return whether we should log all workflows sent to us.
+	 */
+	boolean getLogIncomingWorkflows();
+
+	/**
+	 * @return whether outgoing exceptions should be logged before being
+	 *         converted to responses.
+	 */
+	boolean getLogOutgoingExceptions();
+
+	/**
+	 * @return the file that all usage records should be appended to, or
+	 *         <tt>null</tt> if they should be just dropped.
+	 */
+	String getUsageRecordLogFile();
+
+	/**
+	 * @param logIncomingWorkflows
+	 *            whether we should log all workflows sent to us.
+	 */
+	void setLogIncomingWorkflows(boolean logIncomingWorkflows);
+
+	/**
+	 * @param allowNewWorkflowRuns
+	 *            whether we allow the creation of new workflow runs.
+	 */
+	void setAllowNewWorkflowRuns(boolean allowNewWorkflowRuns);
+
+	/**
+	 * @param logOutgoingExceptions
+	 *            whether outgoing exceptions should be logged before being
+	 *            converted to responses.
+	 */
+	void setLogOutgoingExceptions(boolean logOutgoingExceptions);
+
+	/**
+	 * @param usageRecordLogFile
+	 *            the file that all usage records should be appended to, or
+	 *            <tt>null</tt> if they should be just dropped.
+	 */
+	void setUsageRecordLogFile(String usageRecordLogFile);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/OneListenerBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/OneListenerBean.java
new file mode 100644
index 0000000..9f80a12
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/OneListenerBean.java
@@ -0,0 +1,14 @@
+package org.taverna.server.master.api;
+
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.rest.TavernaServerListenersREST.TavernaServerListenerREST;
+
+/**
+ * Description of properties supported by {@link InputREST}.
+ * 
+ * @author Donal Fellows
+ */
+public interface OneListenerBean {
+	TavernaServerListenerREST connect(Listener listen, TavernaRun run);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/RunBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/RunBean.java
new file mode 100644
index 0000000..4903ea9
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/RunBean.java
@@ -0,0 +1,17 @@
+package org.taverna.server.master.api;
+
+import org.taverna.server.master.ContentsDescriptorBuilder;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * Description of properties supported by {@link RunREST}.
+ * 
+ * @author Donal Fellows
+ */
+public interface RunBean extends SupportAware {
+	void setCdBuilder(ContentsDescriptorBuilder cdBuilder);
+
+	void setRun(TavernaRun run);
+
+	void setRunName(String runName);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/SecurityBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/SecurityBean.java
new file mode 100644
index 0000000..04fbd6e
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/SecurityBean.java
@@ -0,0 +1,14 @@
+package org.taverna.server.master.api;
+
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.rest.TavernaServerSecurityREST;
+
+/**
+ * Description of properties supported by {@link RunSecurityREST}.
+ * 
+ * @author Donal Fellows
+ */
+public interface SecurityBean extends SupportAware {
+	TavernaServerSecurityREST connect(TavernaSecurityContext context, TavernaRun run);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/SupportAware.java b/server-webapp/src/main/java/org/taverna/server/master/api/SupportAware.java
new file mode 100644
index 0000000..c632657
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/SupportAware.java
@@ -0,0 +1,21 @@
+package org.taverna.server.master.api;
+
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.TavernaServerSupport;
+
+/**
+ * Indicates that this is a class that wants to be told by Spring about the
+ * main support bean.
+ * 
+ * @author Donal Fellows
+ */
+public interface SupportAware {
+	/**
+	 * How to tell the bean about the support bean.
+	 * 
+	 * @param support
+	 *            Reference to the support bean.
+	 */
+	@Required
+	void setSupport(TavernaServerSupport support);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/TavernaServerBean.java b/server-webapp/src/main/java/org/taverna/server/master/api/TavernaServerBean.java
new file mode 100644
index 0000000..5a14fb6
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/TavernaServerBean.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.api;
+
+import javax.annotation.Nonnull;
+
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.ContentsDescriptorBuilder;
+import org.taverna.server.master.TavernaServerSupport;
+import org.taverna.server.master.interfaces.Policy;
+import org.taverna.server.master.interfaces.RunStore;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.notification.NotificationEngine;
+import org.taverna.server.master.notification.atom.EventDAO;
+import org.taverna.server.master.rest.TavernaServerREST;
+import org.taverna.server.master.soap.TavernaServerSOAP;
+import org.taverna.server.master.utils.FilenameUtils;
+
+/**
+ * The methods of the webapp that are accessed by beans other than itself or
+ * those which are told directly about it. This exists so that an AOP proxy can
+ * be installed around it.
+ * 
+ * @author Donal Fellows
+ */
+public interface TavernaServerBean extends TavernaServerSOAP, TavernaServerREST,
+		UriBuilderFactory {
+	/**
+	 * @param policy
+	 *            The policy being installed by Spring.
+	 */
+	@Required
+	void setPolicy(@Nonnull Policy policy);
+
+	/**
+	 * @param runStore
+	 *            The run store being installed by Spring.
+	 */
+	@Required
+	void setRunStore(@Nonnull RunStore runStore);
+
+	/**
+	 * @param converter
+	 *            The filename converter being installed by Spring.
+	 */
+	@Required
+	void setFileUtils(@Nonnull FilenameUtils converter);
+
+	/**
+	 * @param cdBuilder
+	 *            The contents descriptor builder being installed by Spring.
+	 */
+	@Required
+	void setContentsDescriptorBuilder(
+			@Nonnull ContentsDescriptorBuilder cdBuilder);
+
+	/**
+	 * @param notificationEngine
+	 *            The notification engine being installed by Spring.
+	 */
+	@Required
+	void setNotificationEngine(@Nonnull NotificationEngine notificationEngine);
+
+	/**
+	 * @param support
+	 *            The support bean being installed by Spring.
+	 */
+	@Required
+	void setSupport(@Nonnull TavernaServerSupport support);
+
+	/**
+	 * @param eventSource
+	 *            The event source bean being installed by Spring.
+	 */
+	@Required
+	void setEventSource(@Nonnull EventDAO eventSource);
+
+	/**
+	 * The nastier parts of security initialisation in SOAP calls, which we want
+	 * to go away.
+	 * 
+	 * @param context
+	 *            The context to configure.
+	 * @return True if we did <i>not</i> initialise things.
+	 */
+	boolean initObsoleteSOAPSecurity(@Nonnull TavernaSecurityContext context);
+
+	/**
+	 * The nastier parts of security initialisation in REST calls, which we want
+	 * to go away.
+	 * 
+	 * @param context
+	 *            The context to configure.
+	 * @return True if we did <i>not</i> initialise things.
+	 */
+	boolean initObsoleteRESTSecurity(@Nonnull TavernaSecurityContext context);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/api/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/api/package-info.java
new file mode 100644
index 0000000..fe76a09
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/api/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * API <tt>interface</tt>s for the main service classes.
+ * 
+ * @author Donal Fellows
+ */
+package org.taverna.server.master.api;
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Capability.java b/server-webapp/src/main/java/org/taverna/server/master/common/Capability.java
new file mode 100644
index 0000000..034d489
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Capability.java
@@ -0,0 +1,22 @@
+package org.taverna.server.master.common;
+
+import java.net.URI;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * Describes a single capability supported by Taverna Server's workflow
+ * execution core.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "Capability")
+public class Capability {
+	@XmlAttribute
+	@XmlSchemaType(name = "anyURI")
+	public URI capability;
+	@XmlAttribute
+	public String version;
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Credential.java b/server-webapp/src/main/java/org/taverna/server/master/common/Credential.java
new file mode 100644
index 0000000..5c16e79
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Credential.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2011-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import java.io.Serializable;
+import java.net.URI;
+import java.security.Key;
+import java.security.cert.Certificate;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A description of a private credential. This description is characterised by a
+ * file visible to the workflow run that contains a particular key-pair.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "CredentialDescriptor")
+@XmlSeeAlso({ Credential.KeyPair.class, Credential.Password.class })
+@SuppressWarnings("serial")
+public abstract class Credential implements Serializable {
+	/** The location of this descriptor in the REST world. */
+	@XmlAttribute(namespace = XLINK)
+	public String href;
+	/**
+	 * The location of this descriptor in the SOAP world. Must match corrected
+	 * with the {@link #href} field.
+	 */
+	@XmlTransient
+	public String id;
+	/**
+	 * The service URI to use this credential with. If omitted, this represents
+	 * the <i>default</i> credential to use.
+	 */
+	@XmlElement
+	@XmlSchemaType(name = "anyURI")
+	public URI serviceURI;
+	/** The key extracted from the keystore. */
+	public transient Key loadedKey;
+	/** The trust chain of the key extracted from the keystore. */
+	public transient Certificate[] loadedTrustChain;
+
+	@Override
+	public int hashCode() {
+		return id.hashCode();
+	}
+
+	@Override
+	public final boolean equals(Object o) {
+		if (o == null || !(o instanceof Credential))
+			return false;
+		return equals((Credential) o);
+	}
+
+	protected boolean equals(@Nonnull Credential c) {
+		return id.equals(c.id);
+	}
+
+	/**
+	 * A description of a credential that is a public/private key-pair in some
+	 * kind of key store.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "keypair")
+	@XmlType(name = "KeyPairCredential")
+	public static class KeyPair extends Credential {
+		/** The name of the credential within its store, i.e., it's alias. */
+		@XmlElement(required = true)
+		public String credentialName;
+		/**
+		 * The keystore file containing the credential. This is resolved with
+		 * respect to the workflow run working directory.
+		 */
+		@XmlElement
+		public String credentialFile;
+		/**
+		 * The type of keystore file. Defaults to <tt>JKS</tt> if unspecified.
+		 */
+		@XmlElement
+		public String fileType;
+		/**
+		 * The password used to unlock the keystore file. It is assumed that the
+		 * same password is used for unlocking the credential within, or that
+		 * the inner password is empty.
+		 */
+		@XmlElement
+		public String unlockPassword;
+		/**
+		 * The encoded serialized keystore containing the credential.
+		 */
+		@XmlElement
+		public byte[] credentialBytes;
+
+		@Override
+		public String toString() {
+			return "keypair(id=" + id + ")";
+		}
+	}
+
+	/**
+	 * A description of a credential that is a username and password.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "userpass")
+	@XmlType(name = "PasswordCredential")
+	public static class Password extends Credential {
+		@XmlElement(required = true)
+		public String username;
+		@XmlElement(required = true)
+		public String password;
+
+		@Override
+		public String toString() {
+			return "userpass(id=" + id + ")";
+		}
+	}
+
+	/**
+	 * A credential that is just used for deleting credentials by ID. Cannot be
+	 * marshalled as XML.
+	 * 
+	 * @author Donal Fellows
+	 */
+	public static class Dummy extends Credential {
+		public Dummy(String id) {
+			this.id = id;
+		}
+
+		@Override
+		public String toString() {
+			return "dummy(id=" + id + ")";
+		}
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/DirEntryReference.java b/server-webapp/src/main/java/org/taverna/server/master/common/DirEntryReference.java
new file mode 100644
index 0000000..318ae4f
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/DirEntryReference.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import java.net.URI;
+
+import javax.ws.rs.core.UriBuilder;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+
+/**
+ * A reference to something that is in a directory below the working directory
+ * of a workflow run, described using JAXB. Note that when creating an XML
+ * document containing one of these in a client, it is <i>not</i> necessary to
+ * supply any attribute.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "DirectoryEntry")
+@XmlSeeAlso( { DirEntryReference.DirectoryReference.class,
+		DirEntryReference.FileReference.class })
+public class DirEntryReference {
+	/** A link to the entry. Ignored on input. */
+	@XmlAttribute(name = "href", namespace = XLINK)
+	@XmlSchemaType(name = "anyURI")
+	public URI link;
+	/** The last, user-displayable part of the name. Ignored on input. */
+	@XmlAttribute
+	public String name;
+	/** The path of the entry. */
+	@XmlValue
+	public String path;
+
+	/**
+	 * Return the directory entry reference instance subclass suitable for the
+	 * given directory entry.
+	 * 
+	 * @param entry
+	 *            The entry to characterise.
+	 * @return An object that describes the directory entry.
+	 */
+	public static DirEntryReference newInstance(DirectoryEntry entry) {
+		return newInstance(null, entry);
+	}
+
+	/**
+	 * Return the directory entry reference instance subclass suitable for the
+	 * given directory entry.
+	 * 
+	 * @param ub
+	 *            Used for constructing URIs. The {@link #link} field is not
+	 *            filled in if this is <tt>null</tt>.
+	 * @param entry
+	 *            The entry to characterise.
+	 * @return An object that describes the directory entry.
+	 */
+	// Really returns a subclass, so cannot be constructor
+	public static DirEntryReference newInstance(UriBuilder ub,
+			DirectoryEntry entry) {
+		DirEntryReference de = (entry instanceof Directory) ? new DirectoryReference()
+				: new FileReference();
+		de.name = entry.getName();
+		String fullname = entry.getFullName();
+		de.path = fullname.startsWith("/") ? fullname.substring(1) : fullname;
+		if (ub != null)
+			de.link = ub.build(entry.getName());
+		return de;
+	}
+
+	/** A reference to a directory, done with JAXB. */
+	@XmlRootElement(name = "dir")
+	@XmlType(name = "DirectoryReference")
+	public static class DirectoryReference extends DirEntryReference {
+	}
+
+	/** A reference to a file, done with JAXB. */
+	@XmlRootElement(name = "file")
+	@XmlType(name = "FileReference")
+	public static class FileReference extends DirEntryReference {
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/InputDescription.java b/server-webapp/src/main/java/org/taverna/server/master/common/InputDescription.java
new file mode 100644
index 0000000..bb98d6d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/InputDescription.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * A description of the inputs to a workflow, described using JAXB.
+ * 
+ * @author Donal Fellows
+ */
+@XmlRootElement(name = "inputConfiguration")
+@XmlType(name = "InputConfigurationDescription")
+public class InputDescription extends VersionedElement {
+	/**
+	 * The Baclava file handling the description of the elements. May be
+	 * omitted/<tt>null</tt>.
+	 */
+	@XmlElement(required = false)
+	public String baclavaFile;
+	/**
+	 * The port/value assignment.
+	 */
+	@XmlElement(nillable = false)
+	public List<Port> port = new ArrayList<>();
+
+	/**
+	 * Make a blank input description.
+	 */
+	public InputDescription() {
+	}
+
+	/**
+	 * Make an input description suitable for the given workflow run.
+	 * 
+	 * @param run
+	 */
+	public InputDescription(TavernaRun run) {
+		super(true);
+		baclavaFile = run.getInputBaclavaFile();
+		if (baclavaFile == null)
+			for (Input i : run.getInputs())
+				port.add(new Port(i));
+	}
+
+	/**
+	 * The type of a single port description.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlType(name = "PortConfigurationDescription")
+	public static class Port {
+		/**
+		 * The name of this port.
+		 */
+		@XmlAttribute(name = "portName", required = true)
+		public String name;
+		/**
+		 * The file assigned to this port.
+		 */
+		@XmlAttribute(name = "portFile", required = false)
+		public String file;
+		/**
+		 * The file assigned to this port.
+		 */
+		@XmlAttribute(name = "listDelimiter", required = false)
+		public String delimiter;
+		/**
+		 * The value assigned to this port.
+		 */
+		@XmlValue
+		public String value;
+
+		/**
+		 * Make a blank port description.
+		 */
+		public Port() {
+		}
+
+		/**
+		 * Make a port description suitable for the given input.
+		 * 
+		 * @param input
+		 */
+		public Port(Input input) {
+			name = input.getName();
+			if (input.getFile() != null) {
+				file = input.getFile();
+				value = "";
+			} else {
+				file = null;
+				value = input.getValue();
+			}
+			delimiter = input.getDelimiter();
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Namespaces.java b/server-webapp/src/main/java/org/taverna/server/master/common/Namespaces.java
new file mode 100644
index 0000000..1311272
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Namespaces.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+/**
+ * A convenient place to keep the names of URIs so that they can be got right
+ * <i>once</i>.
+ * 
+ * @author Donal Fellows
+ */
+public interface Namespaces {
+	/**
+	 * The XLink specification's namespace name.
+	 */
+	public static final String XLINK = "http://www.w3.org/1999/xlink";
+	/**
+	 * The XML Digital Signature specification's namespace name.
+	 */
+	public static final String XSIG = "http://www.w3.org/2000/09/xmldsig#";
+	/**
+	 * The Usage Record specification's namespace name.
+	 */
+	public static final String UR = "http://schema.ogf.org/urf/2003/09/urf";
+	/**
+	 * The T2flow document format's namespace name.
+	 */
+	public static final String T2FLOW = "http://taverna.sf.net/2008/xml/t2flow";
+	/**
+	 * The namespace for the server.
+	 */
+	public static final String SERVER = "http://ns.taverna.org.uk/2010/xml/server/";
+	public static final String SERVER_REST = SERVER + "rest/";
+	public static final String SERVER_SOAP = SERVER + "soap/";
+	public static final String FEED = SERVER + "feed/";
+	public static final String ADMIN = SERVER + "admin/";
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Permission.java b/server-webapp/src/main/java/org/taverna/server/master/common/Permission.java
new file mode 100644
index 0000000..258bfd0
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Permission.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import javax.xml.bind.annotation.XmlEnum;
+import javax.xml.bind.annotation.XmlEnumValue;
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * Description of a permission to access a particular workflow run. Note that
+ * users always have full access to their own runs, as does any user with the "
+ * <tt>{@value org.taverna.server.master.common.Roles#ADMIN}</tt>" ability.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "Permission")
+@XmlEnum
+public enum Permission {
+	/** Indicates that a user cannot see the workflow run at all. */
+	@XmlEnumValue("none")
+	None,
+	/**
+	 * Indicates that a user can see the workflow run and its contents, but
+	 * can't modify anything.
+	 */
+	@XmlEnumValue("read")
+	Read,
+	/**
+	 * Indicates that a user can update most aspects of a workflow, but cannot
+	 * work with either its security features or its lifetime.
+	 */
+	@XmlEnumValue("update")
+	Update,
+	/**
+	 * Indicates that a user can update almost all aspects of a workflow, with
+	 * only its security features being shrouded.
+	 */
+	@XmlEnumValue("destroy")
+	Destroy
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/ProfileList.java b/server-webapp/src/main/java/org/taverna/server/master/common/ProfileList.java
new file mode 100644
index 0000000..69c3622
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/ProfileList.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE.txt" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+/**
+ * Description of the profiles that can apply to a workflow.
+ * 
+ * @author Donal K. Fellows
+ */
+@XmlRootElement(name = "profiles")
+@XmlType(name = "ProfileList")
+public class ProfileList {
+	public List<ProfileList.Info> profile = new ArrayList<ProfileList.Info>();
+
+	/**
+	 * Description of a single workflow profile.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "profile")
+	@XmlType(name = "Profile")
+	public static class Info {
+		@XmlValue
+		public String name;
+		/**
+		 * Whether this is the main profile.
+		 */
+		@XmlAttribute(name = "isMain")
+		public Boolean main;
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Roles.java b/server-webapp/src/main/java/org/taverna/server/master/common/Roles.java
new file mode 100644
index 0000000..8079b1c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Roles.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+/**
+ * The roles defined in this webapp.
+ * 
+ * @author Donal Fellows
+ */
+public interface Roles {
+	/** The role of a normal user. */
+	static final String USER = "ROLE_tavernauser";
+	/**
+	 * The role of an administrator. Administrators <i>should</i> have the
+	 * normal user role as well.
+	 */
+	static final String ADMIN = "ROLE_tavernasuperuser";
+	/**
+	 * The role of a workflow accessing itself. Do not give users this role.
+	 */
+	static final String SELF = "ROLE_tavernaworkflow";
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/RunReference.java b/server-webapp/src/main/java/org/taverna/server/master/common/RunReference.java
new file mode 100644
index 0000000..3486bf7
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/RunReference.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+import static org.taverna.server.master.common.VersionedElement.VERSION;
+
+import java.net.URI;
+
+import javax.ws.rs.core.UriBuilder;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+/**
+ * A reference to a single workflow run, described using JAXB.
+ * 
+ * @author Donal Fellows
+ * @see org.taverna.server.master.interfaces.TavernaRun TavernaRun
+ */
+@XmlRootElement
+@XmlType(name = "TavernaRun")
+@XmlSeeAlso( { Workflow.class, DirEntryReference.class })
+public class RunReference {
+	/**
+	 * Where to get information about the run. For REST.
+	 */
+	@XmlAttribute(name = "href", namespace = XLINK)
+	@XmlSchemaType(name = "anyURI")
+	public URI link;
+	/** What version of server produced this element? */
+	@XmlAttribute(namespace = SERVER)
+	public String serverVersion;
+	/**
+	 * The name of the run. For SOAP.
+	 */
+	@XmlValue
+	public String name;
+
+	/**
+	 * Make a blank run reference.
+	 */
+	public RunReference() {
+	}
+
+	/**
+	 * Make a reference to the given workflow run.
+	 * 
+	 * @param name
+	 *            The name of the run.
+	 * @param ub
+	 *            A factory for URIs, or <tt>null</tt> if none is to be made.
+	 */
+	public RunReference(String name, UriBuilder ub) {
+		this.serverVersion = VERSION;
+		this.name = name;
+		if (ub != null)
+			this.link = ub.build(name);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Status.java b/server-webapp/src/main/java/org/taverna/server/master/common/Status.java
new file mode 100644
index 0000000..8e360dc
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Status.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import javax.xml.bind.annotation.XmlEnum;
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * States of a workflow run. They are {@link #Initialized Initialized},
+ * {@link #Operating Operating}, {@link #Stopped Stopped}, and {@link #Finished
+ * Finished}. Conceptually, there is also a <tt>Destroyed</tt> state, but the
+ * workflow run does not exist (and hence can't have its state queried or set)
+ * in that case.
+ * 
+ * @author Donal Fellows
+ */
+@XmlEnum
+@XmlType(name = "Status")
+public enum Status {
+	/**
+	 * The workflow run has been created, but is not yet running. The run will
+	 * need to be manually moved to {@link #Operating Operating} when ready.
+	 */
+	Initialized,
+	/**
+	 * The workflow run is going, reading input, generating output, etc. Will
+	 * eventually either move automatically to {@link #Finished Finished} or can
+	 * be moved manually to {@link #Stopped Stopped} (where supported).
+	 */
+	Operating,
+	/**
+	 * The workflow run is paused, and will need to be moved back to
+	 * {@link #Operating Operating} manually.
+	 */
+	Stopped,
+	/**
+	 * The workflow run has ceased; data files will continue to exist until the
+	 * run is destroyed (which may be manual or automatic).
+	 */
+	Finished
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Trust.java b/server-webapp/src/main/java/org/taverna/server/master/common/Trust.java
new file mode 100644
index 0000000..6d77aa7
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Trust.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2011-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import java.io.Serializable;
+import java.security.cert.Certificate;
+import java.util.Collection;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+
+/**
+ * A description of a trusted identity or identities. This description is
+ * characterised by a file visible to the workflow run that contains one or more
+ * certificates.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "TrustDescriptor")
+@XmlRootElement(name = "trustedIdentity")
+@SuppressWarnings("serial")
+public final class Trust implements Serializable {
+	/** The location of this descriptor in the REST world. */
+	@XmlAttribute(namespace = XLINK)
+	public String href;
+	/**
+	 * The location of this descriptor in the SOAP world. Must match corrected
+	 * with the {@link #href} field.
+	 */
+	@XmlTransient
+	public String id;
+	/**
+	 * The file containing the certificate(s). This is resolved with respect to
+	 * the workflow run working directory.
+	 */
+	@XmlElement
+	public String certificateFile;
+	/**
+	 * The type of certificate file. Defaults to <tt>X.509</tt> if unspecified.
+	 */
+	@XmlElement
+	public String fileType;
+	/**
+	 * The encoded serialized keystore containing the certificate(s).
+	 */
+	@XmlElement
+	public byte[] certificateBytes;
+	/**
+	 * The names of the server(s) identified by this trust.
+	 */
+	@XmlElement
+	public List<String> serverName;
+	/**
+	 * The collection of certificates loaded from the specified file. This is
+	 * always <tt>null</tt> before validation.
+	 */
+	public transient Collection<? extends Certificate> loadedCertificates;
+
+	@Override
+	public int hashCode() {
+		return id.hashCode();
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (o == null || !(o instanceof Trust))
+			return false;
+		return id.equals(((Trust) o).id);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Uri.java b/server-webapp/src/main/java/org/taverna/server/master/common/Uri.java
new file mode 100644
index 0000000..d6d057c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Uri.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.util.Map;
+
+import javax.annotation.PreDestroy;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriBuilderException;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.web.PortMapper;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A class that makes it simpler to work with an element with a {@link URI} in
+ * an <tt>href</tt> attribute. Done with JAXB.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "Location")
+public class Uri {
+	static Log log = getLog("Taverna.Server.UriRewriter");
+	private static final String SECURE_SCHEME = "https";
+	/**
+	 * This type is characterised by an attribute that is the reference to some
+	 * other element.
+	 */
+	@XmlAttribute(name = "href", namespace = XLINK)
+	@XmlSchemaType(name = "anyURI")
+	public URI ref;
+
+	/** Make a reference that points nowhere. */
+	public Uri() {
+	}
+
+	/**
+	 * Make a reference to the given location.
+	 * 
+	 * @param ref
+	 *            Where to point to.
+	 */
+	public Uri(@Nonnull URI ref) {
+		this.ref = secure(ref);
+	}
+
+	/**
+	 * Make a reference from the factory with the given parameters.
+	 * 
+	 * @param ub
+	 *            The configured factory.
+	 * @param strings
+	 *            The parameters to the factory.
+	 */
+	public Uri(@Nonnull UriBuilder ub, String... strings) {
+		ref = secure(ub).build((Object[]) strings);
+	}
+
+	/**
+	 * Make a reference from the factory with the given parameters.
+	 * 
+	 * @param ui
+	 *            The factory factory.
+	 * @param path
+	 *            The path to configure the factory with.
+	 * @param strings
+	 *            The parameters to the factory.
+	 */
+	public Uri(@Nonnull UriInfo ui, @Nonnull String path, String... strings) {
+		this(ui, true, path, strings);
+	}
+
+	/**
+	 * Make a reference from the factory with the given parameters.
+	 * 
+	 * @param ui
+	 *            The factory factory.
+	 * @param secure
+	 *            Whether the URI should be required to use HTTPS.
+	 * @param path
+	 *            The path to configure the factory with.
+	 * @param strings
+	 *            The parameters to the factory.
+	 */
+	public Uri(@Nonnull UriInfo ui, boolean secure, @Nonnull String path,
+			String... strings) {
+		UriBuilder ub = ui.getAbsolutePathBuilder();
+		if (secure) {
+			ub = secure(ub);
+		}
+		ref = ub.path(path).build((Object[]) strings);
+	}
+
+	public static UriBuilder secure(UriBuilder ub) {
+		return Rewriter.getInstance().getSecuredUriBuilder(ub);
+	}
+
+	public static UriBuilder secure(UriInfo ui) {
+		return secure(ui.getAbsolutePathBuilder());
+	}
+
+	public static URI secure(URI uri) {
+		URI newURI = secure(fromUri(uri)).build();
+		if (log.isDebugEnabled())
+			log.debug("rewrote " + uri + " to " + newURI);
+		return newURI;
+	}
+
+	public static URI secure(URI base, String uri) {
+		URI newURI = secure(fromUri(base.resolve(uri))).build();
+		if (log.isDebugEnabled())
+			log.debug("rewrote " + uri + " to " + newURI);
+		return newURI;
+	}
+
+	/**
+	 * A bean that allows configuration of how to rewrite generated URIs to be
+	 * secure.
+	 * 
+	 * @author Donal Fellows
+	 */
+	public static class Rewriter {
+		private static Rewriter instance;
+		private PortMapper portMapper;
+		private boolean suppress;
+		private String rewriteRE = "://[^/]+/[^/]+";
+		private String rewriteTarget;
+
+		static Rewriter getInstance() {
+			if (instance == null)
+				new Rewriter();
+			return instance;
+		}
+
+		@Autowired
+		public void setPortMapper(PortMapper portMapper) {
+			this.portMapper = portMapper;
+		}
+
+		/**
+		 * Whether to suppress rewriting of URIs to be secure.
+		 * 
+		 * @param suppressSecurity
+		 *            True if no rewriting should be done.
+		 */
+		public void setSuppressSecurity(boolean suppressSecurity) {
+			suppress = suppressSecurity;
+		}
+
+		public void setRewriteRegexp(String rewriteRegexp) {
+			this.rewriteRE = rewriteRegexp;
+		}
+
+		/**
+		 * What to rewrite the host, port and web-app name to be.
+		 * 
+		 * @param rewriteTarget
+		 *            What to rewrite to, or "<tt>NONE</tt>" for no rewrite.
+		 */
+		public void setRewriteTarget(String rewriteTarget) {
+			if (rewriteTarget.isEmpty())
+				this.rewriteTarget = null;
+			else if (rewriteTarget.equals("NONE"))
+				this.rewriteTarget = null;
+			else if (rewriteTarget.startsWith("${"))
+				this.rewriteTarget = null;
+			else
+				this.rewriteTarget = "://" + rewriteTarget;
+		}
+
+		private Integer lookupHttpsPort(URI uri) {
+			if (portMapper != null)
+				return portMapper.lookupHttpsPort(uri.getPort());
+			return null;
+		}
+
+		public Rewriter() {
+			instance = this;
+		}
+
+		@PreDestroy
+		public void done() {
+			instance = null;
+			Uri.log = null;
+		}
+
+		@Nonnull
+		URI rewrite(@Nonnull String url) {
+			if (rewriteTarget != null)
+				url = url.replaceFirst(rewriteRE, rewriteTarget);
+			return URI.create(url);
+		}
+
+		@Nonnull
+		public UriBuilder getSecuredUriBuilder(@Nonnull UriBuilder uribuilder) {
+			if (suppress)
+				return uribuilder.clone();
+			UriBuilder ub = new RewritingUriBuilder(uribuilder);
+			Integer secPort = null;
+			try {
+				secPort = lookupHttpsPort(ub.build());
+			} catch (Exception e) {
+				/*
+				 * Do not log this; we know why it happens and don't actually
+				 * care to do anything about it. All it does is fill up the log
+				 * with pointless scariness!
+				 */
+
+				// log.debug("failed to extract current URI port", e);
+			}
+			if (secPort == null || secPort.intValue() == -1)
+				return ub.scheme(SECURE_SCHEME);
+			return ub.scheme(SECURE_SCHEME).port(secPort);
+		}
+
+		/**
+		 * {@link UriBuilder} that applies a rewrite rule to the URIs produced
+		 * by the wrapped builder.
+		 * 
+		 * @author Donal Fellows
+		 */
+		class RewritingUriBuilder extends UriBuilder {
+			private UriBuilder wrapped;
+
+			RewritingUriBuilder(UriBuilder builder) {
+				wrapped = builder.clone();
+			}
+
+			private URI rewrite(URI uri) {
+				return Rewriter.this.rewrite(uri.toString());
+			}
+
+			@Override
+			public UriBuilder clone() {
+				return new RewritingUriBuilder(wrapped);
+			}
+
+			@Override
+			public URI buildFromMap(Map<String, ?> values)
+					throws IllegalArgumentException, UriBuilderException {
+				return rewrite(wrapped.buildFromMap(values));
+			}
+
+			@Override
+			public URI buildFromEncodedMap(Map<String, ? extends Object> values)
+					throws IllegalArgumentException, UriBuilderException {
+				return rewrite(wrapped.buildFromEncodedMap(values));
+			}
+
+			@Override
+			public URI build(Object... values) throws IllegalArgumentException,
+					UriBuilderException {
+				return rewrite(wrapped.build(values));
+			}
+
+			@Override
+			public URI build(Object[] values, boolean encodeSlashInPath)
+					throws IllegalArgumentException, UriBuilderException {
+				return rewrite(wrapped.build(values, encodeSlashInPath));
+			}
+
+			@Override
+			public URI buildFromEncoded(Object... values)
+					throws IllegalArgumentException, UriBuilderException {
+				return rewrite(wrapped.buildFromEncoded(values));
+			}
+
+			@Override
+			public URI buildFromMap(Map<String, ?> values,
+					boolean encodeSlashInPath) throws IllegalArgumentException,
+					UriBuilderException {
+				return rewrite(wrapped.buildFromEncoded(values,
+						encodeSlashInPath));
+			}
+
+			@Override
+			public UriBuilder uri(URI uri) throws IllegalArgumentException {
+				wrapped.uri(uri);
+				return this;
+			}
+
+			@Override
+			public UriBuilder uri(String uriTemplate)
+					throws IllegalArgumentException {
+				wrapped.uri(uriTemplate);
+				return this;
+			}
+
+			@Override
+			public String toTemplate() {
+				return wrapped.toTemplate();
+			}
+
+			@Override
+			public UriBuilder scheme(String scheme)
+					throws IllegalArgumentException {
+				wrapped.scheme(scheme);
+				return this;
+			}
+
+			@Override
+			public UriBuilder schemeSpecificPart(String ssp)
+					throws IllegalArgumentException {
+				wrapped.schemeSpecificPart(ssp);
+				return this;
+			}
+
+			@Override
+			public UriBuilder userInfo(String ui) {
+				wrapped.userInfo(ui);
+				return this;
+			}
+
+			@Override
+			public UriBuilder host(String host) throws IllegalArgumentException {
+				wrapped.host(host);
+				return this;
+			}
+
+			@Override
+			public UriBuilder port(int port) throws IllegalArgumentException {
+				wrapped.port(port);
+				return this;
+			}
+
+			@Override
+			public UriBuilder replacePath(String path) {
+				wrapped.replacePath(path);
+				return this;
+			}
+
+			@Override
+			public UriBuilder path(String path) throws IllegalArgumentException {
+				wrapped.path(path);
+				return this;
+			}
+
+			@Override
+			public UriBuilder path(
+					@java.lang.SuppressWarnings("rawtypes") Class resource)
+					throws IllegalArgumentException {
+				wrapped.path(resource);
+				return this;
+			}
+
+			@Override
+			public UriBuilder path(
+					@java.lang.SuppressWarnings("rawtypes") Class resource,
+					String method) throws IllegalArgumentException {
+				wrapped.path(resource, method);
+				return this;
+			}
+
+			@Override
+			public UriBuilder path(Method method)
+					throws IllegalArgumentException {
+				wrapped.path(method);
+				return this;
+			}
+
+			@Override
+			public UriBuilder segment(String... segments)
+					throws IllegalArgumentException {
+				wrapped.segment(segments);
+				return this;
+			}
+
+			@Override
+			public UriBuilder replaceMatrix(String matrix)
+					throws IllegalArgumentException {
+				wrapped.replaceMatrix(matrix);
+				return this;
+			}
+
+			@Override
+			public UriBuilder matrixParam(String name, Object... values)
+					throws IllegalArgumentException {
+				wrapped.matrixParam(name, values);
+				return this;
+			}
+
+			@Override
+			public UriBuilder replaceMatrixParam(String name, Object... values)
+					throws IllegalArgumentException {
+				wrapped.replaceMatrixParam(name, values);
+				return this;
+			}
+
+			@Override
+			public UriBuilder replaceQuery(String query)
+					throws IllegalArgumentException {
+				wrapped.replaceQuery(query);
+				return this;
+			}
+
+			@Override
+			public UriBuilder queryParam(String name, Object... values)
+					throws IllegalArgumentException {
+				wrapped.queryParam(name, values);
+				return this;
+			}
+
+			@Override
+			public UriBuilder replaceQueryParam(String name, Object... values)
+					throws IllegalArgumentException {
+				wrapped.replaceQueryParam(name, values);
+				return this;
+			}
+
+			@Override
+			public UriBuilder fragment(String fragment) {
+				wrapped.fragment(fragment);
+				return this;
+			}
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/VersionedElement.java b/server-webapp/src/main/java/org/taverna/server/master/common/VersionedElement.java
new file mode 100644
index 0000000..6eddf2d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/VersionedElement.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlType;
+
+import org.apache.commons.logging.Log;
+
+/**
+ * The type of an element that declares the version of the server that produced
+ * it.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "VersionedElement", namespace = SERVER)
+public abstract class VersionedElement {
+	/** What version of server produced this element? */
+	@XmlAttribute(namespace = SERVER)
+	public String serverVersion;
+	/** What revision of server produced this element? Derived from SCM commit. */
+	@XmlAttribute(namespace = SERVER)
+	public String serverRevision;
+	/** When was the server built? */
+	@XmlAttribute(namespace = SERVER)
+	public String serverBuildTimestamp;
+	public static final String VERSION, REVISION, TIMESTAMP;
+	static {
+		Log log = getLog("Taverna.Server.Webapp");
+		Properties p = new Properties();
+		try {
+			try (InputStream is = VersionedElement.class
+					.getResourceAsStream("/version.properties")) {
+				p.load(is);
+			}
+		} catch (IOException e) {
+			log.warn("failed to read /version.properties", e);
+		}
+		VERSION = p.getProperty("tavernaserver.version", "unknownVersion");
+		REVISION = String.format("%s (tag: %s)",
+				p.getProperty("tavernaserver.branch", "unknownRevision"),
+				p.getProperty("tavernaserver.revision.describe", "unknownTag"));
+		TIMESTAMP = p
+				.getProperty("tavernaserver.timestamp", "unknownTimestamp");
+	}
+
+	public VersionedElement() {
+	}
+
+	protected VersionedElement(boolean ignored) {
+		serverVersion = VERSION;
+		serverRevision = REVISION;
+		serverBuildTimestamp = TIMESTAMP;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/Workflow.java b/server-webapp/src/main/java/org/taverna/server/master/common/Workflow.java
new file mode 100644
index 0000000..ba6a68c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/Workflow.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common;
+
+import static javax.xml.bind.Marshaller.JAXB_ENCODING;
+import static javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT;
+import static javax.xml.bind.annotation.XmlAccessType.NONE;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.rest.handler.Scufl2DocumentHandler.SCUFL2;
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW;
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW_NS;
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW_ROOTNAME;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.URL;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAnyElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.taverna.server.master.rest.handler.Scufl2DocumentHandler;
+import org.taverna.server.master.rest.handler.T2FlowDocumentHandler;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import uk.org.taverna.scufl2.api.common.NamedSet;
+import uk.org.taverna.scufl2.api.container.WorkflowBundle;
+import uk.org.taverna.scufl2.api.io.ReaderException;
+import uk.org.taverna.scufl2.api.io.WorkflowBundleIO;
+import uk.org.taverna.scufl2.api.io.WriterException;
+import uk.org.taverna.scufl2.api.profiles.Profile;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Encapsulation of a T2flow or Scufl2 document.
+ * 
+ * @author Donal K. Fellows
+ */
+@XmlRootElement(name = "workflow")
+@XmlType(name = "Workflow")
+@XmlAccessorType(NONE)
+public class Workflow implements Serializable, Externalizable {
+	/** Literal document, if present. */
+	@XmlAnyElement(lax = true)
+	private Element content;
+	/** SCUFL2 bundle, if present. */
+	@XmlTransient
+	private WorkflowBundle bundle;
+	/** Which came first, the bundle or the t2flow document. */
+	@XmlTransient
+	private boolean isBundleFirst;
+
+	private static Marshaller marshaller;
+	private static Unmarshaller unmarshaller;
+	private final static String ENCODING = "UTF-8";
+	private final static WorkflowBundleIO io;
+	static {
+		try {
+			JAXBContext context = JAXBContext.newInstance(Workflow.class);
+			marshaller = context.createMarshaller();
+			unmarshaller = context.createUnmarshaller();
+			marshaller.setProperty(JAXB_ENCODING, ENCODING);
+			marshaller.setProperty(JAXB_FORMATTED_OUTPUT, false);
+		} catch (JAXBException e) {
+			getLog("Taverna.Server.Webapp").fatal(
+					"failed to build JAXB context for working with "
+							+ Workflow.class, e);
+		}
+		io = new WorkflowBundleIO();
+	}
+
+	public enum ContentType {
+		T2FLOW(T2FlowDocumentHandler.T2FLOW), SCUFL2(
+				Scufl2DocumentHandler.SCUFL2);
+		private String type;
+
+		ContentType(String type) {
+			this.type = type;
+		}
+
+		public String getContentType() {
+			return type;
+		}
+	}
+
+	public Workflow() {
+	}
+
+	public Workflow(Element element) {
+		this.content = element;
+		this.isBundleFirst = false;
+	}
+
+	public Workflow(WorkflowBundle bundle) {
+		this.bundle = bundle;
+		this.isBundleFirst = true;
+	}
+
+	public Workflow(URL url) throws ReaderException, IOException {
+		this(io.readBundle(url, null));
+	}
+
+	/**
+	 * What content type would this workflow "prefer" to be?
+	 */
+	public ContentType getPreferredContentType() {
+		if (isBundleFirst)
+			return ContentType.SCUFL2;
+		else
+			return ContentType.T2FLOW;
+	}
+
+	/**
+	 * Retrieves the workflow as a SCUFL2 document, converting it if necessary.
+	 * 
+	 * @return The SCUFL2 document.
+	 * @throws IOException
+	 *             If anything goes wrong.
+	 */
+	public WorkflowBundle getScufl2Workflow() throws IOException {
+		try {
+			if (bundle == null)
+				bundle = io.readBundle(new ByteArrayInputStream(getAsT2Flow()),
+						T2FLOW);
+			return bundle;
+		} catch (IOException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new IOException("problem when converting to SCUFL2", e);
+		}
+	}
+
+	/**
+	 * Get the bytes of the serialized SCUFL2 workflow.
+	 * 
+	 * @return Array of bytes.
+	 * @throws IOException
+	 *             If serialization fails.
+	 * @throws WriterException
+	 *             If conversion fails.
+	 */
+	public byte[] getScufl2Bytes() throws IOException, WriterException {
+		ByteArrayOutputStream baos = new ByteArrayOutputStream();
+		io.writeBundle(getScufl2Workflow(), baos, SCUFL2);
+		return baos.toByteArray();
+	}
+
+	/**
+	 * Retrieves the workflow as a T2Flow document, converting it if necessary.
+	 * 
+	 * @return The T2Flow document.
+	 * @throws IOException
+	 *             If anything goes wrong.
+	 */
+	public Element getT2flowWorkflow() throws IOException {
+		try {
+			if (content != null)
+				return content;
+			ByteArrayOutputStream baos = new ByteArrayOutputStream();
+			io.writeBundle(bundle, baos, T2FLOW);
+			Document doc;
+			try {
+				DocumentBuilderFactory dbf = DocumentBuilderFactory
+						.newInstance();
+				dbf.setNamespaceAware(true);
+				doc = dbf.newDocumentBuilder().parse(
+						new ByteArrayInputStream(baos.toByteArray()));
+			} catch (SAXException e) {
+				throw new IOException("failed to convert to DOM tree", e);
+			}
+			Element e = doc.getDocumentElement();
+			if (e.getNamespaceURI().equals(T2FLOW_NS)
+					&& e.getNodeName().equals(T2FLOW_ROOTNAME))
+				return content = e;
+			throw new IOException(
+					"unexpected element when converting to T2Flow: {"
+							+ e.getNamespaceURI() + "}" + e.getNodeName());
+		} catch (IOException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new IOException("problem when converting to SCUFL2", e);
+		}
+	}
+
+	/**
+	 * @return The name of the main workflow profile, or <tt>null</tt> if there
+	 *         is none.
+	 */
+	public String getMainProfileName() {
+		try {
+			return getScufl2Workflow().getMainProfile().getName();
+		} catch (IOException e) {
+			return null;
+		}
+	}
+
+	/**
+	 * @return The set of profiles supported over this workflow.
+	 */
+	public NamedSet<Profile> getProfiles() {
+		try {
+			return getScufl2Workflow().getProfiles();
+		} catch (IOException e) {
+			return new NamedSet<Profile>();
+		}
+	}
+
+	/**
+	 * Convert from marshalled form.
+	 * 
+	 * @throws JAXBException
+	 *             If the conversion fails.
+	 */
+	public static Workflow unmarshal(String representation)
+			throws JAXBException {
+		StringReader sr = new StringReader(representation);
+		return (Workflow) unmarshaller.unmarshal(sr);
+	}
+
+	/**
+	 * Convert to marshalled form.
+	 */
+	public String marshal() throws JAXBException {
+		StringWriter sw = new StringWriter();
+		marshaller.marshal(this, sw);
+		return sw.toString();
+	}
+
+	@Override
+	public void readExternal(ObjectInput in) throws IOException,
+			ClassNotFoundException {
+		try {
+			ByteArrayInputStream bytes = readbytes(in);
+			if (bytes != null)
+				try (Reader r = new InputStreamReader(bytes, ENCODING)) {
+					content = ((Workflow) unmarshaller.unmarshal(r)).content;
+				}
+			bytes = readbytes(in);
+			if (bytes != null)
+				bundle = io.readBundle(bytes, SCUFL2);
+			isBundleFirst = in.readBoolean();
+			return;
+		} catch (JAXBException e) {
+			throw new IOException("failed to unmarshal", e);
+		} catch (ClassCastException e) {
+			throw new IOException("bizarre result of unmarshalling", e);
+		} catch (ReaderException e) {
+			throw new IOException("failed to unmarshal", e);
+		}
+	}
+
+	private byte[] getAsT2Flow() throws IOException, JAXBException {
+		ByteArrayOutputStream baos = new ByteArrayOutputStream();
+		OutputStreamWriter w = new OutputStreamWriter(baos, ENCODING);
+		marshaller.marshal(this, w);
+		w.close();
+		return baos.toByteArray();
+	}
+
+	private byte[] getAsScufl2() throws IOException, WriterException {
+		ByteArrayOutputStream baos = new ByteArrayOutputStream();
+		io.writeBundle(bundle, baos, SCUFL2);
+		baos.close();
+		return baos.toByteArray();
+	}
+
+	@Override
+	public void writeExternal(ObjectOutput out) throws IOException {
+		try {
+			writebytes(out, (content != null) ? getAsT2Flow() : null);
+		} catch (JAXBException e) {
+			throw new IOException("failed to marshal t2flow", e);
+		}
+		try {
+			writebytes(out, (bundle != null) ? getAsScufl2() : null);
+		} catch (WriterException e) {
+			throw new IOException("failed to marshal scufl2", e);
+		}
+		out.writeBoolean(isBundleFirst);
+	}
+
+	private ByteArrayInputStream readbytes(ObjectInput in) throws IOException {
+		int len = in.readInt();
+		if (len > 0) {
+			byte[] bytes = new byte[len];
+			in.readFully(bytes);
+			return new ByteArrayInputStream(bytes);
+		}
+		return null;
+	}
+
+	private void writebytes(ObjectOutput out, byte[] data) throws IOException {
+		out.writeInt(data == null ? 0 : data.length);
+		if (data != null && data.length > 0)
+			out.write(data);
+	}
+
+	/**
+	 * Make up for the lack of an integrated XPath engine.
+	 * 
+	 * @param name
+	 *            The element names to look up from the root of the contained
+	 *            document.
+	 * @return The looked up element, or <tt>null</tt> if it doesn't exist.
+	 */
+	private Element getEl(String... name) {
+		Element el = content;
+		boolean skip = true;
+		for (String n : name) {
+			if (skip) {
+				skip = false;
+				continue;
+			}
+			if (el == null)
+				return null;
+			NodeList nl = el.getElementsByTagNameNS(T2FLOW_NS, n);
+			if (nl.getLength() == 0)
+				return null;
+			Node node = nl.item(0);
+			if (node instanceof Element)
+				el = (Element) node;
+			else
+				return null;
+		}
+		return el;
+	}
+
+	/**
+	 * @return The content of the embedded
+	 *         <tt>&lt;workflow&gt;&lt;dataflow&gt;&lt;name&gt;</tt> element.
+	 */
+	@XmlTransient
+	public String getName() {
+		return getEl("workflow", "dataflow", "name").getTextContent();
+	}
+
+	/**
+	 * @return The embedded <tt>&lt;workflow&gt;</tt> element.
+	 */
+	@XmlTransient
+	public Element getWorkflowRoot() {
+		return getEl("workflow");
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/common/package-info.java
new file mode 100644
index 0000000..fd040de
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the common XML elements used throughout Taverna Server's various interfaces.
+ * @author Donal Fellows
+ */
+@XmlSchema(namespace = SERVER, elementFormDefault = QUALIFIED, attributeFormDefault = QUALIFIED, xmlns = {
+		@XmlNs(prefix = "xlink", namespaceURI = XLINK),
+		@XmlNs(prefix = "ts", namespaceURI = SERVER),
+		@XmlNs(prefix = "ts-rest", namespaceURI = SERVER_REST),
+		@XmlNs(prefix = "ts-soap", namespaceURI = SERVER_SOAP),
+		@XmlNs(prefix = "feed", namespaceURI = FEED),
+		@XmlNs(prefix = "admin", namespaceURI = ADMIN) })
+package org.taverna.server.master.common;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.master.common.Namespaces.ADMIN;
+import static org.taverna.server.master.common.Namespaces.FEED;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.SERVER_REST;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+
diff --git a/server-webapp/src/main/java/org/taverna/server/master/common/version/Version.java b/server-webapp/src/main/java/org/taverna/server/master/common/version/Version.java
new file mode 100644
index 0000000..e9c58a9
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/common/version/Version.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.common.version;
+
+import static org.taverna.server.master.common.version.Constants.PATCH;
+import static org.taverna.server.master.common.version.Constants.VERSION;
+
+/**
+ * Common location for describing the version of the server.
+ * 
+ * @author Donal Fellows
+ */
+public interface Version {
+	public static final String JAVA = VERSION + Constants.releaseChar + PATCH;
+	public static final String HTML = VERSION + Constants.releaseHEnt + PATCH;
+	public static final String XML = VERSION + Constants.releaseXEnt + PATCH;
+}
+
+/**
+ * The pieces of a version string.
+ * 
+ * @author Donal Fellows
+ */
+interface Constants {
+	static final String MAJOR = "2";
+	static final String MINOR = "5";
+	static final String PATCH = "4";
+
+	static final char alphaChar = '\u03b1';
+	static final char betaChar = '\u03b2';
+	static final char releaseChar = '.';
+	static final String alphaHEnt = "&alpha;";
+	static final String betaHEnt = "&beta;";
+	static final String releaseHEnt = ".";
+	static final String alphaXEnt = "&#x03b1;";
+	static final String betaXEnt = "&#x03b2;";
+	static final String releaseXEnt = ".";
+
+	static final String VERSION = MAJOR + "." + MINOR;
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/defaults/Default.java b/server-webapp/src/main/java/org/taverna/server/master/defaults/Default.java
new file mode 100644
index 0000000..c2a2bc9
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/defaults/Default.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.defaults;
+
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.localworker.LocalWorkerState;
+
+/**
+ * This defines a collection of default values, collecting them from various
+ * parts of the server.
+ * 
+ * @author Donal Fellows
+ */
+public interface Default {
+	/** The default value of the <tt>prefix</tt> property. */
+	static final String AUTHORITY_PREFIX = "LOCALUSER_";
+
+	/**
+	 * The name of the resource that is the implementation of the subprocess
+	 * that this class will fork off.
+	 */
+	static final String SERVER_WORKER_IMPLEMENTATION_JAR = "util/server.worker.jar";
+
+	/**
+	 * The name of the resource that is the implementation of the subprocess
+	 * that manages secure forking.
+	 */
+	static final String SECURE_FORK_IMPLEMENTATION_JAR = "util/secure.fork.jar";
+
+	/**
+	 * The name of the resource that is the implementation of the subprocess
+	 * that acts as the RMI registry.
+	 */
+	static final String REGISTRY_JAR = "util/rmi.daemon.jar";
+
+	/** Initial lifetime of runs, in minutes. */
+	static final int RUN_LIFE_MINUTES = 20;
+
+	/**
+	 * Maximum number of runs to exist at once. Note that this includes when
+	 * they are just existing for the purposes of file transfer (
+	 * {@link Status#Initialized}/{@link Status#Finished} states).
+	 */
+	static final int RUN_COUNT_MAX = 5;
+
+	/**
+	 * Prefix to use for RMI names.
+	 */
+	static final String RMI_PREFIX = "ForkRunFactory.";
+
+	/** Default value for {@link LocalWorkerState#passwordFile}. */
+	static final String PASSWORD_FILE = null;
+
+	/**
+	 * The extra arguments to pass to the subprocess.
+	 */
+	static final String[] EXTRA_ARGUMENTS = new String[0];
+
+	/**
+	 * How long to wait for subprocess startup, in seconds.
+	 */
+	static final int SUBPROCESS_START_WAIT = 40;
+
+	/**
+	 * Polling interval to use during startup, in milliseconds.
+	 */
+	static final int SUBPROCESS_START_POLL_SLEEP = 1000;
+
+	/**
+	 * Maximum number of {@link Status#Operating} runs at any time.
+	 */
+	static final int RUN_OPERATING_LIMIT = 10;
+
+	/**
+	 * What fields of a certificate we look at when understanding who it is
+	 * talking about, in the order that we look.
+	 */
+	static final String[] CERTIFICATE_FIELD_NAMES = { "CN", "COMMONNAME",
+			"COMMON NAME", "COMMON_NAME", "OU", "ORGANIZATIONALUNITNAME",
+			"ORGANIZATIONAL UNIT NAME", "O", "ORGANIZATIONNAME",
+			"ORGANIZATION NAME" };
+
+	/** The type of certificates that are processed if we don't say otherwise. */
+	static final String CERTIFICATE_TYPE = "X.509";
+
+	/** Max size of credential file, in kiB. */
+	static final int CREDENTIAL_FILE_SIZE_LIMIT = 20;
+
+	/**
+	 * The notification message format to use if none is configured.
+	 */
+	public static final String NOTIFY_MESSAGE_FORMAT = "Your job with ID={0} has finished with exit code {1,number,integer}.";
+
+	/** The address of the SMS gateway service used. */
+	public static final String SMS_GATEWAY_URL = "https://www.intellisoftware.co.uk/smsgateway/sendmsg.aspx";
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/defaults/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/defaults/package-info.java
new file mode 100644
index 0000000..239b260
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/defaults/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * This package contains information about the various default values supported by the server.
+ * @author Donal Fellows
+ */
+package org.taverna.server.master.defaults;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadInputPortNameException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadInputPortNameException.java
new file mode 100644
index 0000000..0b6247e
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadInputPortNameException.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Indicates that the port name was not recognized.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "BadInputPortNameFault")
+@SuppressWarnings("serial")
+public class BadInputPortNameException extends Exception {
+	public BadInputPortNameException(String msg) {
+		super(msg);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadPropertyValueException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadPropertyValueException.java
new file mode 100644
index 0000000..303b19b
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadPropertyValueException.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Indicates a bad property value.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "BadPropertyValueFault")
+public class BadPropertyValueException extends NoListenerException {
+	private static final long serialVersionUID = -8459491388504556875L;
+
+	public BadPropertyValueException(String msg) {
+		super(msg);
+	}
+
+	public BadPropertyValueException(String msg, Throwable e) {
+		super(msg, e);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadStateChangeException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadStateChangeException.java
new file mode 100644
index 0000000..4b843e2
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/BadStateChangeException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Exception that is thrown to indicate that the state change requested for a
+ * run is impossible.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "NoUpdateFault")
+public class BadStateChangeException extends NoUpdateException {
+	private static final long serialVersionUID = -4490826388447601775L;
+
+	public BadStateChangeException() {
+		super("cannot do that state change");
+	}
+
+	public BadStateChangeException(Throwable t) {
+		super("cannot do that state change", t);
+	}
+
+	public BadStateChangeException(String msg, Throwable t) {
+		super(msg, t);
+	}
+
+	public BadStateChangeException(String message) {
+		super(message);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/FilesystemAccessException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/FilesystemAccessException.java
new file mode 100644
index 0000000..0b6cf07
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/FilesystemAccessException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import java.rmi.RemoteException;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * An exception that happened when the underlying filesystem was accessed.
+ * @author Donal Fellows
+ */
+@WebFault(name = "FilesystemAccessFault")
+public class FilesystemAccessException extends Exception {
+	private static final long serialVersionUID = 8715937300989820318L;
+
+	public FilesystemAccessException(String msg) {
+		super(msg);
+	}
+
+	public FilesystemAccessException(String string, Throwable cause) {
+		super(string, getRealCause(cause));
+	}
+
+	private static Throwable getRealCause(Throwable t) {
+		if (t instanceof RemoteException) {
+			RemoteException remote = (RemoteException) t;
+			if (remote.detail != null)
+				return remote.detail;
+		}
+		if (t.getCause() != null)
+			return t.getCause();
+		return t;
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/GeneralFailureException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/GeneralFailureException.java
new file mode 100644
index 0000000..11d47c7
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/GeneralFailureException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Some sort of exception that occurred which we can't map any other way. This
+ * is generally indicative of a problem server-side.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "GeneralFailureFault", targetNamespace = SERVER_SOAP)
+@SuppressWarnings("serial")
+public class GeneralFailureException extends RuntimeException {
+	public GeneralFailureException(Throwable cause) {
+		super(cause.getMessage(), cause);
+	}
+
+	public GeneralFailureException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/InvalidCredentialException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/InvalidCredentialException.java
new file mode 100644
index 0000000..67ba551
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/InvalidCredentialException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+/**
+ * An exception that is thrown to indicate that a credential-descriptor or
+ * trust-descriptor supplied as part of a credential or trust management
+ * operation is invalid.
+ * 
+ * @author Donal Fellows
+ * 
+ */
+@SuppressWarnings("serial")
+public class InvalidCredentialException extends Exception {
+	private static final String MSG = "that credential is invalid";
+
+	public InvalidCredentialException() {
+		super(MSG);
+	}
+
+	public InvalidCredentialException(String reason) {
+		super(MSG + ": " + reason);
+	}
+
+	public InvalidCredentialException(String reason, Throwable cause) {
+		this(reason);
+		initCause(cause);
+	}
+
+	public InvalidCredentialException(Throwable cause) {
+		this(cause.getMessage(), cause);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoCreateException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoCreateException.java
new file mode 100644
index 0000000..e2ddf4c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoCreateException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+
+/**
+ * Exception that is thrown to indicate that the user is not permitted to
+ * create something.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "NoCreateFault")
+public class NoCreateException extends NoUpdateException {
+	private static final long serialVersionUID = 270413810410167235L;
+
+	public NoCreateException() {
+		super("not permitted to create");
+	}
+
+	public NoCreateException(String string) {
+		super(string);
+	}
+
+	public NoCreateException(String string, Throwable e) {
+		super(string, e);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoCredentialException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoCredentialException.java
new file mode 100644
index 0000000..7077fc4
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoCredentialException.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+/**
+ * Exception that indicates the absence of an expected credential.
+ * 
+ * @author Donal Fellows
+ */
+@SuppressWarnings("serial")
+public class NoCredentialException extends Exception {
+	public NoCredentialException() {
+		super("no such credential");
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoDestroyException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoDestroyException.java
new file mode 100644
index 0000000..d7b0d29
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoDestroyException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+
+/**
+ * Exception that is thrown to indicate that the user is not permitted to
+ * destroy something.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "NoDestroyFault")
+public class NoDestroyException extends NoUpdateException {
+	private static final long serialVersionUID = 6207448533265237933L;
+
+	public NoDestroyException() {
+		super("not permitted to destroy");
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoDirectoryEntryException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoDirectoryEntryException.java
new file mode 100644
index 0000000..0d6ca63
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoDirectoryEntryException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Indicates that the file or directory name was not recognized.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "NoDirectoryEntryFault")
+@SuppressWarnings("serial")
+public class NoDirectoryEntryException extends Exception {
+	public NoDirectoryEntryException(String msg) {
+		super(msg);
+	}
+	public NoDirectoryEntryException(String msg,Exception cause) {
+		super(msg, cause);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoListenerException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoListenerException.java
new file mode 100644
index 0000000..f4b8e81
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoListenerException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.ws.WebFault;
+
+/**
+ * Exception thrown to indicate that no listener by that name exists, or that
+ * some other problem with listeners has occurred.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "NoListenerFault")
+@XmlSeeAlso(BadPropertyValueException.class)
+public class NoListenerException extends Exception {
+	private static final long serialVersionUID = -2550897312787546547L;
+
+	public NoListenerException() {
+		super("no such listener");
+	}
+
+	public NoListenerException(String msg) {
+		super(msg);
+	}
+
+	public NoListenerException(String msg, Throwable t) {
+		super(msg, t);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoUpdateException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoUpdateException.java
new file mode 100644
index 0000000..c49d111
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NoUpdateException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.ws.WebFault;
+
+/**
+ * Exception that is thrown to indicate that the user is not permitted to update
+ * something.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "NoUpdateFault")
+@XmlSeeAlso( { NoCreateException.class, NoDestroyException.class, BadStateChangeException.class })
+public class NoUpdateException extends Exception {
+	private static final long serialVersionUID = 4230987102653846379L;
+
+	public NoUpdateException() {
+		super("not permitted to update");
+	}
+
+	public NoUpdateException(String msg) {
+		super(msg);
+	}
+
+	public NoUpdateException(String string, Throwable e) {
+		super(string, e);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/NotOwnerException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NotOwnerException.java
new file mode 100644
index 0000000..6e1f792
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/NotOwnerException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * An exception thrown when an operation is attempted which only the owner is
+ * permitted to do. Notably, permissions may <i>only</i> be manipulated by the
+ * owner.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "NotOwnerFault")
+@SuppressWarnings("serial")
+public class NotOwnerException extends Exception {
+	public NotOwnerException() {
+		super("not permitted; not the owner");
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/OverloadedException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/OverloadedException.java
new file mode 100644
index 0000000..dda371e
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/OverloadedException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Exception that is thrown to indicate that the state change requested for a
+ * run is currently impossible due to excessive server load.
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "OverloadedFault")
+public class OverloadedException extends BadStateChangeException {
+	private static final long serialVersionUID = 490826388447601776L;
+
+	public OverloadedException() {
+		super("server too busy; try later please");
+	}
+
+	public OverloadedException(Throwable t) {
+		super("server too busy; try later please", t);
+	}
+
+	public OverloadedException(String msg, Throwable t) {
+		super(msg, t);
+	}
+
+	public OverloadedException(String message) {
+		super(message);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/UnknownRunException.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/UnknownRunException.java
new file mode 100644
index 0000000..4632d4a
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/UnknownRunException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.exceptions;
+
+import javax.xml.ws.WebFault;
+
+/**
+ * Exception thrown to indicate that the handle of the run is unknown (or
+ * unacceptable to the current user).
+ * 
+ * @author Donal Fellows
+ */
+@WebFault(name = "UnknownRunFault")
+public class UnknownRunException extends Exception {
+	private static final long serialVersionUID = -3028749401786242841L;
+
+	public UnknownRunException() {
+		super("unknown run UUID");
+	}
+
+	public UnknownRunException(Throwable t) {
+		super("implementation problems", t);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/exceptions/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/exceptions/package-info.java
new file mode 100644
index 0000000..29ccacd
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/exceptions/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the exceptions/faults thrown by Taverna Server.
+ * @author Donal Fellows
+ */
+@XmlSchema(namespace = SERVER, elementFormDefault = QUALIFIED, attributeFormDefault = QUALIFIED, xmlns = {
+		@XmlNs(prefix = "xlink", namespaceURI = XLINK),
+		@XmlNs(prefix = "ts", namespaceURI = SERVER),
+		@XmlNs(prefix = "ts-rest", namespaceURI = SERVER_REST),
+		@XmlNs(prefix = "ts-soap", namespaceURI = SERVER_SOAP),
+		@XmlNs(prefix = "feed", namespaceURI = FEED),
+		@XmlNs(prefix = "admin", namespaceURI = ADMIN) })
+package org.taverna.server.master.exceptions;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.master.common.Namespaces.ADMIN;
+import static org.taverna.server.master.common.Namespaces.FEED;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.SERVER_REST;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+
diff --git a/server-webapp/src/main/java/org/taverna/server/master/facade/Facade.java b/server-webapp/src/main/java/org/taverna/server/master/facade/Facade.java
new file mode 100644
index 0000000..0015754
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/facade/Facade.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.facade;
+
+import static javax.ws.rs.core.MediaType.TEXT_HTML_TYPE;
+import static javax.ws.rs.core.Response.ok;
+
+import java.io.IOException;
+import java.net.URL;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.utils.Contextualizer;
+
+/**
+ * This is a simple class that is used to serve up a file (with a simple
+ * substitution applied) as the root of the T2Server webapp.
+ * 
+ * @author Donal Fellows
+ */
+@Path("/")
+public class Facade {
+	private Log log = LogFactory.getLog("Taverna.Server.Utils");
+	private String welcome;
+	private Contextualizer contextualizer;
+
+	/**
+	 * Set what resource file to use as the template for the response.
+	 * 
+	 * @param file
+	 *            The file from which to load the data (presumed HTML) to serve
+	 *            up as the root content.
+	 * @throws IOException
+	 *             If the file doesn't exist.
+	 */
+	public void setFile(String file) throws IOException {
+		URL full = Facade.class.getResource(file);
+		log.info("setting " + full + " as source of root page");
+		this.welcome = IOUtils.toString(full);
+	}
+
+	@Required
+	public void setContextualizer(Contextualizer contextualizer) {
+		this.contextualizer = contextualizer;
+	}
+
+	/**
+	 * Serve up some HTML as the root of the service.
+	 * 
+	 * @param ui
+	 *            A reference to how we were accessed by the service.
+	 * @return The response, containing the HTML.
+	 */
+	@GET
+	@Path("{dummy:.*}")
+	@Produces("text/html")
+	public Response get(@Context UriInfo ui) {
+		return ok(contextualizer.contextualize(ui, welcome), TEXT_HTML_TYPE)
+				.build();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/facade/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/facade/package-info.java
new file mode 100644
index 0000000..73c884f
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/facade/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Simple facade used at the top level of the Taverna Server in order to
+ * provide an entry splash page.
+ */
+package org.taverna.server.master.facade;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/factories/ConfigurableRunFactory.java b/server-webapp/src/main/java/org/taverna/server/master/factories/ConfigurableRunFactory.java
new file mode 100644
index 0000000..954b6f1
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/factories/ConfigurableRunFactory.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.factories;
+
+/**
+ * Interface to run factories for the purpose of configuration.
+ * 
+ * @author Donal Fellows
+ */
+public interface ConfigurableRunFactory extends RunFactory {
+	/** Where is the registry? Getter */
+	String getRegistryHost();
+
+	/** Where is the registry? Setter */
+	void setRegistryHost(String host);
+
+	/** Where is the registry? Getter */
+	int getRegistryPort();
+
+	/** Where is the registry? Setter */
+	void setRegistryPort(int port);
+
+	/** How much can be done at once? Getter */
+	int getMaxRuns();
+
+	/** How much can be done at once? Setter */
+	void setMaxRuns(int maxRuns);
+
+	/** How long will things live? Getter */
+	int getDefaultLifetime();
+
+	/** How long will things live? Setter */
+	void setDefaultLifetime(int defaultLifetime);
+
+	/** How often do we probe for info? Getter */
+	int getSleepTime();
+
+	/** How often do we probe for info? Setter */
+	void setSleepTime(int sleepTime);
+
+	/** How long do we allow for actions? Getter */
+	int getWaitSeconds();
+
+	/** How long do we allow for actions? Setter */
+	void setWaitSeconds(int seconds);
+
+	/** How do we start the workflow engine? Getter */
+	String getExecuteWorkflowScript();
+
+	/** How do we start the workflow engine? Setter */
+	void setExecuteWorkflowScript(String executeWorkflowScript);
+
+	/** How do we start the file system access process? Getter */
+	String getServerWorkerJar();
+
+	/** How do we start the file system access process? Setter */
+	void setServerWorkerJar(String serverWorkerJar);
+
+	/**
+	 * How do we start the file system access process? Extra arguments to pass.
+	 * Getter
+	 */
+	String[] getExtraArguments();
+
+	/**
+	 * How do we start the file system access process? Extra arguments to pass.
+	 * Setter
+	 */
+	void setExtraArguments(String[] firstArguments);
+
+	/** Where is Java? Getter */
+	String getJavaBinary();
+
+	/** Where is Java? Setter */
+	void setJavaBinary(String javaBinary);
+
+	/** Where do we get passwords from? Getter */
+	String getPasswordFile();
+
+	/** Where do we get passwords from? Setter */
+	void setPasswordFile(String newValue);
+
+	/** How do we switch users? Getter */
+	String getServerForkerJar();
+
+	/** How do we switch users? Setter */
+	void setServerForkerJar(String newValue);
+
+	/** How many runs have there been? */
+	int getTotalRuns();
+
+	/** How long did the last subprocess startup take? */
+	int getLastStartupCheckCount();
+
+	/** What are the current runs? */
+	String[] getCurrentRunNames();
+
+	/** What is the RMI ID of the factory process? */
+	String getFactoryProcessName();
+
+	/** What was the last observed exit code? */
+	Integer getLastExitCode();
+
+	/** What factory process to use for a particular user? */
+	String[] getFactoryProcessMapping();
+
+	/** How many runs can be operating at once? Setter */
+	void setOperatingLimit(int operatingLimit);
+
+	/** How many runs can be operating at once? Getter */
+	int getOperatingLimit();
+
+	/**
+	 * How many runs are actually operating?
+	 * 
+	 * @throws Exception
+	 *             if anything goes wrong
+	 */
+	int getOperatingCount() throws Exception;
+
+	/** How do we start the RMI registry process? Getter */
+	String getRmiRegistryJar();
+
+	/** How do we start the RMI registry process? Setter */
+	void setRmiRegistryJar(String rmiRegistryJar);
+
+	boolean getGenerateProvenance();
+
+	void setGenerateProvenance(boolean generateProvenance);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/factories/ListenerFactory.java b/server-webapp/src/main/java/org/taverna/server/master/factories/ListenerFactory.java
new file mode 100644
index 0000000..ecc13cd
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/factories/ListenerFactory.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.factories;
+
+import java.util.List;
+
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * How to make event listeners of various types that are attached to a workflow
+ * instance.
+ * 
+ * @author Donal Fellows
+ */
+public interface ListenerFactory {
+	/**
+	 * Make an event listener.
+	 * 
+	 * @param run
+	 *            The workflow instance to attach the event listener to.
+	 * @param listenerType
+	 *            The type of event listener to create. Must be one of the
+	 *            strings returned by {@link #getSupportedListenerTypes()}.
+	 * @param configuration
+	 *            A configuration document to pass to the listener.
+	 * @return The event listener that was created.
+	 * @throws NoListenerException
+	 *             If the <b>listenerType</b> is unrecognized or the
+	 *             <b>configuration</b> is bad in some way.
+	 */
+	public Listener makeListener(TavernaRun run, String listenerType,
+			String configuration) throws NoListenerException;
+
+	/**
+	 * What types of listener are supported? Note that we assume that the list
+	 * of types is the same for all users and all workflow instances.
+	 * 
+	 * @return A list of supported listener types.
+	 */
+	public List<String> getSupportedListenerTypes();
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/factories/RunFactory.java b/server-webapp/src/main/java/org/taverna/server/master/factories/RunFactory.java
new file mode 100644
index 0000000..eeacad7
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/factories/RunFactory.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.factories;
+
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * How to construct a Taverna Server Workflow Run.
+ * 
+ * @author Donal Fellows
+ */
+public interface RunFactory {
+	/**
+	 * Make a Taverna Server workflow run that is bound to a particular user
+	 * (the "creator") and able to run a particular workflow.
+	 * 
+	 * @param creator
+	 *            The user creating the workflow instance.
+	 * @param workflow
+	 *            The workflow to instantiate
+	 * @return An object representing the run.
+	 * @throws NoCreateException
+	 *             On failure.
+	 */
+	TavernaRun create(UsernamePrincipal creator, Workflow workflow)
+			throws NoCreateException;
+
+	/**
+	 * Check whether the factory is permitting runs to actually start operating.
+	 * 
+	 * @return Whether a run should start.
+	 */
+	boolean isAllowingRunsToStart();
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/factories/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/factories/package-info.java
new file mode 100644
index 0000000..39b9ac6
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/factories/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * These interfaces define the principal way for the <i>factories</i> of
+ * worker classes to be invoked.
+ */
+package org.taverna.server.master.factories;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/AuthorityDerivedIDMapper.java b/server-webapp/src/main/java/org/taverna/server/master/identity/AuthorityDerivedIDMapper.java
new file mode 100644
index 0000000..63402ef
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/AuthorityDerivedIDMapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static org.taverna.server.master.defaults.Default.AUTHORITY_PREFIX;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Extracts the local user id from the set of Spring Security authorities
+ * granted to the current user. This is done by scanning the set of authorities
+ * to see if any of them start with the substring listed in the <tt>prefix</tt>
+ * property; the username is the rest of the authority string in that case.
+ * 
+ * @author Donal Fellows
+ */
+public class AuthorityDerivedIDMapper implements LocalIdentityMapper {
+	private String prefix = AUTHORITY_PREFIX;
+
+	public String getPrefix() {
+		return prefix;
+	}
+
+	public void setPrefix(String prefix) {
+		this.prefix = prefix;
+	}
+
+	@Override
+	public String getUsernameForPrincipal(UsernamePrincipal user) {
+		Authentication auth = SecurityContextHolder.getContext()
+				.getAuthentication();
+		if (auth == null || !auth.isAuthenticated())
+			return null;
+		for (GrantedAuthority authority : auth.getAuthorities()) {
+			String token = authority.getAuthority();
+			if (token == null)
+				continue;
+			if (token.startsWith(prefix))
+				return token.substring(prefix.length());
+		}
+		return null;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/CompositeIDMapper.java b/server-webapp/src/main/java/org/taverna/server/master/identity/CompositeIDMapper.java
new file mode 100644
index 0000000..b5fcd5a
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/CompositeIDMapper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.util.List;
+import java.util.Map.Entry;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * An identity mapper that composes the results from other mappers, using the
+ * identity mappers in order until one can provide a non-<tt>null</tt> answer.
+ * 
+ * @author Donal Fellows.
+ */
+public class CompositeIDMapper implements LocalIdentityMapper,
+		ApplicationContextAware {
+	private Log log = getLog("Taverna.Server.IdentityMapper");
+	private List<LocalIdentityMapper> mappers;
+	private ApplicationContext context;
+
+	/**
+	 * @param mappers
+	 *            The list of mappers to delegate to. Order is significant.
+	 */
+	public void setIdentityMappers(List<LocalIdentityMapper> mappers) {
+		this.mappers = mappers;
+	}
+
+	@Override
+	public void setApplicationContext(ApplicationContext applicationContext)
+			throws BeansException {
+		context = applicationContext;
+	}
+
+	@Override
+	public String getUsernameForPrincipal(UsernamePrincipal user) {
+		if (mappers == null)
+			return null;
+		for (LocalIdentityMapper m : mappers) {
+			String u = m.getUsernameForPrincipal(user);
+			if (u == null)
+				continue;
+			for (Entry<String, ? extends LocalIdentityMapper> entry : context
+					.getBeansOfType(m.getClass()).entrySet())
+				if (m == entry.getValue()) {
+					log.info("used " + entry.getKey() + " LIM to map " + user
+							+ " to " + u);
+					break;
+				}
+			return u;
+		}
+		return null;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/ConstantIDMapper.java b/server-webapp/src/main/java/org/taverna/server/master/identity/ConstantIDMapper.java
new file mode 100644
index 0000000..e81f926
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/ConstantIDMapper.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * A trivial principal to user mapper that always uses the same ID.
+ * @author Donal Fellows
+ */
+public class ConstantIDMapper implements LocalIdentityMapper {
+	private String id;
+
+	/**
+	 * Sets what local user ID all users should be mapped to.
+	 * 
+	 * @param id
+	 *            The local user ID.
+	 */
+	public void setConstantId(String id) {
+		this.id = id;
+	}
+
+	@Override
+	public String getUsernameForPrincipal(UsernamePrincipal user) {
+		return id;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/NameIDMapper.java b/server-webapp/src/main/java/org/taverna/server/master/identity/NameIDMapper.java
new file mode 100644
index 0000000..42a3874
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/NameIDMapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static java.util.regex.Pattern.compile;
+
+import java.security.Principal;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * A trivial identity mapper that just uses the name out of the
+ * {@link Principal}, or uses a regular expression to extract it from the string
+ * representation of the principal.
+ * 
+ * @author Donal Fellows
+ */
+public class NameIDMapper implements LocalIdentityMapper {
+	private Pattern pat;
+
+	/**
+	 * @param regexp
+	 *            The regular expression to use. The first capturing group
+	 *            within the RE will be the result of the extraction.
+	 * @throws PatternSyntaxException
+	 *             If the pattern is invalid.
+	 */
+	public void setRegexp(String regexp) throws PatternSyntaxException {
+		pat = compile(regexp);
+	}
+
+	@Override
+	public String getUsernameForPrincipal(UsernamePrincipal user) {
+		if (pat != null) {
+			Matcher m = pat.matcher(user.toString());
+			if (m.find() && m.groupCount() > 0) {
+				return m.group(1);
+			}
+			return null;
+		}
+		return user.getName();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java b/server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java
new file mode 100644
index 0000000..06202dc
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/StrippedDownAuthProvider.java
@@ -0,0 +1,278 @@
+package org.taverna.server.master.identity;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.PreDestroy;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.security.authentication.AccountExpiredException;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.authentication.DisabledException;
+import org.springframework.security.authentication.LockedException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+
+/**
+ * A stripped down version of a
+ * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
+ * DaoAuthenticationProvider}/
+ * {@link org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider
+ * AbstractUserDetailsAuthenticationProvider} that avoids much of the overhead
+ * associated with that class.
+ */
+public class StrippedDownAuthProvider implements AuthenticationProvider {
+	/**
+	 * The plaintext password used to perform
+	 * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when
+	 * the user is not found to avoid SEC-2056.
+	 */
+	private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
+
+	/**
+	 * The password used to perform
+	 * {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when
+	 * the user is not found to avoid SEC-2056. This is necessary, because some
+	 * {@link PasswordEncoder} implementations will short circuit if the
+	 * password is not in a valid format.
+	 */
+	private String userNotFoundEncodedPassword;
+	private UserDetailsService userDetailsService;
+	private PasswordEncoder passwordEncoder;
+	private Map<String, AuthCacheEntry> authCache = new HashMap<>();
+	protected final Log logger = LogFactory.getLog(getClass());
+
+	private static class AuthCacheEntry {
+		private String creds;
+		private long timestamp;
+		private static final long VALIDITY = 1000 * 60 * 20;
+		AuthCacheEntry(String credentials) {
+			creds = credentials;
+			timestamp = System.currentTimeMillis();
+		}
+		boolean valid(String password) {
+			return creds.equals(password) && timestamp+VALIDITY > System.currentTimeMillis();
+		}
+	}
+
+	@PerfLogged
+	@Override
+	public Authentication authenticate(Authentication authentication)
+			throws AuthenticationException {
+
+		if (!(authentication instanceof UsernamePasswordAuthenticationToken))
+			throw new IllegalArgumentException(
+					"can only authenticate against username+password");
+		UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
+
+		// Determine username
+		String username = (auth.getPrincipal() == null) ? "NONE_PROVIDED"
+				: auth.getName();
+
+		UserDetails user;
+
+		try {
+			user = retrieveUser(username, auth);
+			if (user == null)
+				throw new IllegalStateException(
+						"retrieveUser returned null - a violation of the interface contract");
+		} catch (UsernameNotFoundException notFound) {
+			if (logger.isDebugEnabled())
+				logger.debug("User '" + username + "' not found", notFound);
+			throw new BadCredentialsException("Bad credentials");
+		}
+
+		// Pre-auth
+		if (!user.isAccountNonLocked())
+			throw new LockedException("User account is locked");
+		if (!user.isEnabled())
+			throw new DisabledException("User account is disabled");
+		if (!user.isAccountNonExpired())
+			throw new AccountExpiredException("User account has expired");
+		Object credentials = auth.getCredentials();
+		if (credentials == null) {
+			logger.debug("Authentication failed: no credentials provided");
+
+			throw new BadCredentialsException("Bad credentials");
+		}
+
+		String providedPassword = credentials.toString();
+		boolean matched = false;
+		synchronized (authCache) {
+			AuthCacheEntry pw = authCache.get(username);
+			if (pw != null && providedPassword != null) {
+				if (pw.valid(providedPassword))
+					matched = true;
+				else
+					authCache.remove(username);
+			}
+		}
+		// Auth
+		if (!matched) {
+			if (!passwordEncoder.matches(providedPassword, user.getPassword())) {
+				logger.debug("Authentication failed: password does not match stored value");
+
+				throw new BadCredentialsException("Bad credentials");
+			}
+			if (providedPassword != null)
+				synchronized (authCache) {
+					authCache.put(username, new AuthCacheEntry(providedPassword));
+				}
+		}
+
+		// Post-auth
+		if (!user.isCredentialsNonExpired())
+			throw new CredentialsExpiredException(
+					"User credentials have expired");
+
+		return createSuccessAuthentication(user, auth, user);
+	}
+
+	@PreDestroy
+	void clearCache() {
+		authCache.clear();
+	}
+
+	/**
+	 * Creates a successful {@link Authentication} object.
+	 * <p>
+	 * Protected so subclasses can override.
+	 * </p>
+	 * <p>
+	 * Subclasses will usually store the original credentials the user supplied
+	 * (not salted or encoded passwords) in the returned
+	 * <code>Authentication</code> object.
+	 * </p>
+	 * 
+	 * @param principal
+	 *            that should be the principal in the returned object (defined
+	 *            by the {@link #isForcePrincipalAsString()} method)
+	 * @param authentication
+	 *            that was presented to the provider for validation
+	 * @param user
+	 *            that was loaded by the implementation
+	 * 
+	 * @return the successful authentication token
+	 */
+	private Authentication createSuccessAuthentication(Object principal,
+			Authentication authentication, UserDetails user) {
+		/*
+		 * Ensure we return the original credentials the user supplied, so
+		 * subsequent attempts are successful even with encoded passwords. Also
+		 * ensure we return the original getDetails(), so that future
+		 * authentication events after cache expiry contain the details
+		 */
+		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
+				principal, authentication.getCredentials(),
+				user.getAuthorities());
+		result.setDetails(authentication.getDetails());
+
+		return result;
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return UsernamePasswordAuthenticationToken.class
+				.isAssignableFrom(authentication);
+	}
+
+	/**
+	 * Allows subclasses to actually retrieve the <code>UserDetails</code> from
+	 * an implementation-specific location, with the option of throwing an
+	 * <code>AuthenticationException</code> immediately if the presented
+	 * credentials are incorrect (this is especially useful if it is necessary
+	 * to bind to a resource as the user in order to obtain or generate a
+	 * <code>UserDetails</code>).
+	 * <p>
+	 * Subclasses are not required to perform any caching, as the
+	 * <code>AbstractUserDetailsAuthenticationProvider</code> will by default
+	 * cache the <code>UserDetails</code>. The caching of
+	 * <code>UserDetails</code> does present additional complexity as this means
+	 * subsequent requests that rely on the cache will need to still have their
+	 * credentials validated, even if the correctness of credentials was assured
+	 * by subclasses adopting a binding-based strategy in this method.
+	 * Accordingly it is important that subclasses either disable caching (if
+	 * they want to ensure that this method is the only method that is capable
+	 * of authenticating a request, as no <code>UserDetails</code> will ever be
+	 * cached) or ensure subclasses implement
+	 * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
+	 * to compare the credentials of a cached <code>UserDetails</code> with
+	 * subsequent authentication requests.
+	 * </p>
+	 * <p>
+	 * Most of the time subclasses will not perform credentials inspection in
+	 * this method, instead performing it in
+	 * {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
+	 * so that code related to credentials validation need not be duplicated
+	 * across two methods.
+	 * </p>
+	 * 
+	 * @param username
+	 *            The username to retrieve
+	 * @param authentication
+	 *            The authentication request, which subclasses <em>may</em> need
+	 *            to perform a binding-based retrieval of the
+	 *            <code>UserDetails</code>
+	 * 
+	 * @return the user information (never <code>null</code> - instead an
+	 *         exception should the thrown)
+	 * 
+	 * @throws AuthenticationException
+	 *             if the credentials could not be validated (generally a
+	 *             <code>BadCredentialsException</code>, an
+	 *             <code>AuthenticationServiceException</code> or
+	 *             <code>UsernameNotFoundException</code>)
+	 */
+	private UserDetails retrieveUser(String username,
+			UsernamePasswordAuthenticationToken authentication)
+			throws AuthenticationException {
+		try {
+			return userDetailsService.loadUserByUsername(username);
+		} catch (UsernameNotFoundException notFound) {
+			if (authentication.getCredentials() != null) {
+				String presentedPassword = authentication.getCredentials()
+						.toString();
+				passwordEncoder.matches(presentedPassword,
+						userNotFoundEncodedPassword);
+			}
+			throw notFound;
+		} catch (AuthenticationException e) {
+			throw e;
+		} catch (Exception repositoryProblem) {
+			throw new AuthenticationServiceException(
+					repositoryProblem.getMessage(), repositoryProblem);
+		}
+	}
+
+	/**
+	 * Sets the PasswordEncoder instance to be used to encode and validate
+	 * passwords.
+	 */
+	@Required
+	public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
+		if (passwordEncoder == null)
+			throw new IllegalArgumentException("passwordEncoder cannot be null");
+
+		this.passwordEncoder = passwordEncoder;
+		this.userNotFoundEncodedPassword = passwordEncoder
+				.encode(USER_NOT_FOUND_PASSWORD);
+	}
+
+	@Required
+	public void setUserDetailsService(UserDetailsService userDetailsService) {
+		if (userDetailsService == null)
+			throw new IllegalStateException("A UserDetailsService must be set");
+		this.userDetailsService = userDetailsService;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/User.java b/server-webapp/src/main/java/org/taverna/server/master/identity/User.java
new file mode 100644
index 0000000..bdb6e40
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/User.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2011-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static org.taverna.server.master.common.Roles.ADMIN;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.defaults.Default.AUTHORITY_PREFIX;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.Query;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/**
+ * The representation of a user in the database.
+ * <p>
+ * A user consists logically of a (non-ordered) tuple of items:
+ * <ul>
+ * <li>The {@linkplain #getUsername() user name},
+ * <li>The {@linkplain #getPassword() user's password} (salted, encoded),
+ * <li>Whether the user is {@linkplain #isEnabled() enabled} (i.e., able to log
+ * in),
+ * <li>Whether the user has {@linkplain #isAdmin() administrative privileges}, and
+ * <li>What {@linkplain #getLocalUsername() system (Unix) account} the user's
+ * workflows will run as; separation between different users that are mapped to
+ * the same system account is nothing like as strongly enforced.
+ * </ul>
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceCapable(schema = "USERS", table = "LIST")
+@Query(name = "users", language = "SQL", value = "SELECT id FROM USERS.LIST ORDER BY id", resultClass = String.class)
+@XmlRootElement
+@XmlType(name = "User", propOrder = {})
+@SuppressWarnings("serial")
+public class User implements UserDetails {
+	@XmlElement
+	@Persistent
+	private boolean disabled;
+	@XmlElement(name = "username", required = true)
+	@Persistent(primaryKey = "true")
+	private String id;
+	@XmlElement(name = "password", required = true)
+	@Persistent(column = "password")
+	private String encodedPassword;
+	@XmlElement
+	@Persistent
+	private boolean admin;
+	@XmlElement
+	@Persistent
+	private String localUsername;
+
+	@Override
+	public Collection<GrantedAuthority> getAuthorities() {
+		List<GrantedAuthority> auths = new ArrayList<>();
+		auths.add(new LiteralGrantedAuthority(USER));
+		if (admin)
+			auths.add(new LiteralGrantedAuthority(ADMIN));
+		if (localUsername != null)
+			auths.add(new LiteralGrantedAuthority(AUTHORITY_PREFIX
+					+ localUsername));
+		return auths;
+	}
+
+	@Override
+	public String getPassword() {
+		return encodedPassword;
+	}
+
+	@Override
+	public String getUsername() {
+		return id;
+	}
+
+	@Override
+	public boolean isAccountNonExpired() {
+		return true;
+	}
+
+	@Override
+	public boolean isAccountNonLocked() {
+		return true;
+	}
+
+	@Override
+	public boolean isCredentialsNonExpired() {
+		return true;
+	}
+
+	@Override
+	public boolean isEnabled() {
+		return !disabled;
+	}
+
+	void setDisabled(boolean disabled) {
+		this.disabled = disabled;
+	}
+
+	void setUsername(String username) {
+		this.id = username;
+	}
+
+	void setEncodedPassword(String password) {
+		this.encodedPassword = password;
+	}
+
+	void setAdmin(boolean admin) {
+		this.admin = admin;
+	}
+
+	public boolean isAdmin() {
+		return admin;
+	}
+
+	void setLocalUsername(String localUsername) {
+		this.localUsername = localUsername;
+	}
+
+	public String getLocalUsername() {
+		return localUsername;
+	}
+}
+
+@SuppressWarnings("serial")
+class LiteralGrantedAuthority implements GrantedAuthority {
+	private String auth;
+
+	LiteralGrantedAuthority(String auth) {
+		this.auth = auth;
+	}
+
+	@Override
+	public String getAuthority() {
+		return auth;
+	}
+
+	@Override
+	public String toString() {
+		return "AUTHORITY(" + auth + ")";
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java b/server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java
new file mode 100644
index 0000000..054d932
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/UserStore.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright (C) 2011-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+import static org.taverna.server.master.common.Roles.ADMIN;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.defaults.Default.AUTHORITY_PREFIX;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.jdo.annotations.PersistenceAware;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.dao.DataAccessException;
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedOperation;
+import org.springframework.jmx.export.annotation.ManagedOperationParameter;
+import org.springframework.jmx.export.annotation.ManagedOperationParameters;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.core.userdetails.memory.UserAttribute;
+import org.springframework.security.core.userdetails.memory.UserAttributeEditor;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.JDOSupport;
+
+/**
+ * The bean class that is responsible for managing the users in the database.
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceAware
+@ManagedResource(objectName = JMX_ROOT + "Users", description = "The user database.")
+public class UserStore extends JDOSupport<User> implements UserDetailsService,
+		UserStoreAPI {
+	/** The logger for the user store. */
+	private transient Log log = getLog("Taverna.Server.UserDB");
+
+	public UserStore() {
+		super(User.class);
+	}
+
+	@PreDestroy
+	void closeLog() {
+		log = null;
+	}
+
+	private Map<String, BootstrapUserInfo> base = new HashMap<>();
+	private String defLocalUser;
+	private PasswordEncoder encoder;
+	private volatile int epoch;
+
+	/**
+	 * Install the encoder that will be used to turn a plaintext password into
+	 * something that it is safe to store in the database.
+	 * 
+	 * @param encoder
+	 *            The password encoder bean to install.
+	 */
+	public void setEncoder(PasswordEncoder encoder) {
+		this.encoder = encoder;
+	}
+
+	public void setBaselineUserProperties(Properties props) {
+		UserAttributeEditor parser = new UserAttributeEditor();
+
+		for (Object name : props.keySet()) {
+			String username = (String) name;
+			String value = props.getProperty(username);
+
+			// Convert value to a password, enabled setting, and list of granted
+			// authorities
+			parser.setAsText(value);
+
+			UserAttribute attr = (UserAttribute) parser.getValue();
+			if (attr != null && attr.isEnabled())
+				base.put(username, new BootstrapUserInfo(username, attr));
+		}
+	}
+
+	private void installPassword(User u, String password) {
+		u.setEncodedPassword(encoder.encode(password));
+	}
+
+	public void setDefaultLocalUser(String defLocalUser) {
+		this.defLocalUser = defLocalUser;
+	}
+
+	@SuppressWarnings("unchecked")
+	private List<String> getUsers() {
+		return (List<String>) namedQuery("users").execute();
+	}
+
+	@WithinSingleTransaction
+	@PostConstruct
+	void initDB() {
+		if (base == null || base.isEmpty())
+			log.warn("no baseline user collection");
+		else if (!getUsers().isEmpty())
+			log.info("using existing users from database");
+		else
+			for (String username : base.keySet()) {
+				BootstrapUserInfo ud = base.get(username);
+				if (ud == null)
+					continue;
+				User u = ud.get(encoder);
+				if (u == null)
+					continue;
+				log.info("bootstrapping user " + username + " in the database");
+				persist(u);
+			}
+		base = null;
+		epoch++;
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	@ManagedAttribute(description = "The list of server accounts known about.", currencyTimeLimit = 30)
+	public List<String> getUserNames() {
+		return getUsers();
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	public User getUser(String userName) {
+		return detach(getById(userName));
+	}
+
+	/**
+	 * Get information about a server account.
+	 * 
+	 * @param userName
+	 *            The username to look up.
+	 * @return A description map intended for use by a server admin over JMX.
+	 */
+	@PerfLogged
+	@WithinSingleTransaction
+	@ManagedOperation(description = "Get information about a server account.")
+	@ManagedOperationParameters(@ManagedOperationParameter(name = "userName", description = "The username to look up."))
+	public Map<String, String> getUserInfo(String userName) {
+		User u = getById(userName);
+		Map<String, String> info = new HashMap<>();
+		info.put("name", u.getUsername());
+		info.put("admin", u.isAdmin() ? "yes" : "no");
+		info.put("enabled", u.isEnabled() ? "yes" : "no");
+		info.put("localID", u.getLocalUsername());
+		return info;
+	}
+
+	/**
+	 * Get a list of all the users in the database.
+	 * 
+	 * @return A list of user details, <i>copied</i> out of the database.
+	 */
+	@PerfLogged
+	@WithinSingleTransaction
+	public List<UserDetails> listUsers() {
+		ArrayList<UserDetails> result = new ArrayList<>();
+		for (String id : getUsers())
+			result.add(detach(getById(id)));
+		return result;
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	@ManagedOperation(description = "Create a new user account; the account will be disabled and "
+			+ "non-administrative by default. Does not create any underlying system account.")
+	@ManagedOperationParameters({
+			@ManagedOperationParameter(name = "username", description = "The username to create."),
+			@ManagedOperationParameter(name = "password", description = "The password to use."),
+			@ManagedOperationParameter(name = "coupleLocalUsername", description = "Whether to set the local user name to the 'main' one.") })
+	public void addUser(String username, String password,
+			boolean coupleLocalUsername) {
+		if (username.matches(".*[^a-zA-Z0-9].*"))
+			throw new IllegalArgumentException(
+					"bad user name; must be pure alphanumeric");
+		if (getById(username) != null)
+			throw new IllegalArgumentException("user name already exists");
+		User u = new User();
+		u.setDisabled(true);
+		u.setAdmin(false);
+		u.setUsername(username);
+		installPassword(u, password);
+		if (coupleLocalUsername)
+			u.setLocalUsername(username);
+		else
+			u.setLocalUsername(defLocalUser);
+		log.info("creating user for " + username);
+		persist(u);
+		epoch++;
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	@ManagedOperation(description = "Set or clear whether this account is enabled. "
+			+ "Disabled accounts cannot be used to log in.")
+	@ManagedOperationParameters({
+			@ManagedOperationParameter(name = "username", description = "The username to adjust."),
+			@ManagedOperationParameter(name = "enabled", description = "Whether to enable the account.") })
+	public void setUserEnabled(String username, boolean enabled) {
+		User u = getById(username);
+		if (u != null) {
+			u.setDisabled(!enabled);
+			log.info((enabled ? "enabling" : "disabling") + " user " + username);
+			epoch++;
+		}
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	@ManagedOperation(description = "Set or clear the mark on an account that indicates "
+			+ "that it has administrative privileges.")
+	@ManagedOperationParameters({
+			@ManagedOperationParameter(name = "username", description = "The username to adjust."),
+			@ManagedOperationParameter(name = "admin", description = "Whether the account has admin privileges.") })
+	public void setUserAdmin(String username, boolean admin) {
+		User u = getById(username);
+		if (u != null) {
+			u.setAdmin(admin);
+			log.info((admin ? "enabling" : "disabling") + " user " + username
+					+ " admin status");
+			epoch++;
+		}
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	@ManagedOperation(description = "Change the password for an account.")
+	@ManagedOperationParameters({
+			@ManagedOperationParameter(name = "username", description = "The username to adjust."),
+			@ManagedOperationParameter(name = "password", description = "The new password to use.") })
+	public void setUserPassword(String username, String password) {
+		User u = getById(username);
+		if (u != null) {
+			installPassword(u, password);
+			log.info("changing password for user " + username);
+			epoch++;
+		}
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	@ManagedOperation(description = "Change what local system account to use for a server account.")
+	@ManagedOperationParameters({
+			@ManagedOperationParameter(name = "username", description = "The username to adjust."),
+			@ManagedOperationParameter(name = "password", description = "The new local user account use.") })
+	public void setUserLocalUser(String username, String localUsername) {
+		User u = getById(username);
+		if (u != null) {
+			u.setLocalUsername(localUsername);
+			log.info("mapping user " + username + " to local account "
+					+ localUsername);
+			epoch++;
+		}
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	@ManagedOperation(description = "Delete a server account. The underlying "
+			+ "system account is not modified.")
+	@ManagedOperationParameters(@ManagedOperationParameter(name = "username", description = "The username to delete."))
+	public void deleteUser(String username) {
+		delete(getById(username));
+		log.info("deleting user " + username);
+		epoch++;
+	}
+
+	@Override
+	@PerfLogged
+	@WithinSingleTransaction
+	public UserDetails loadUserByUsername(String username)
+			throws UsernameNotFoundException, DataAccessException {
+		User u;
+		if (base != null) {
+			log.warn("bootstrap user store still installed!");
+			BootstrapUserInfo ud = base.get(username);
+			if (ud != null) {
+				log.warn("retrieved production credentials for " + username
+						+ " from bootstrap store");
+				u = ud.get(encoder);
+				if (u != null)
+					return u;
+			}
+		}
+		try {
+			u = detach(getById(username));
+		} catch (NullPointerException npe) {
+			throw new UsernameNotFoundException("who are you?");
+		} catch (Exception ex) {
+			throw new UsernameNotFoundException("who are you?", ex);
+		}
+		if (u != null)
+			return u;
+		throw new UsernameNotFoundException("who are you?");
+	}
+
+	int getEpoch() {
+		return epoch;
+	}
+
+	public static class CachedUserStore implements UserDetailsService {
+		private int epoch;
+		private Map<String, UserDetails> cache = new HashMap<>();
+		private UserStore realStore;
+
+		@Required
+		public void setRealStore(UserStore store) {
+			this.realStore = store;
+		}
+
+		@Override
+		@PerfLogged
+		public UserDetails loadUserByUsername(String username) {
+			int epoch = realStore.getEpoch();
+			UserDetails details;
+			synchronized (cache) {
+				if (epoch != this.epoch) {
+					cache.clear();
+					this.epoch = epoch;
+					details = null;
+				} else
+					details = cache.get(username);
+			}
+			if (details == null) {
+				details = realStore.loadUserByUsername(username);
+				synchronized (cache) {
+					cache.put(username, details);
+				}
+			}
+			return details;
+		}
+	}
+
+	private static class BootstrapUserInfo {
+		private String user;
+		private String pass;
+		private Collection<GrantedAuthority> auth;
+
+		BootstrapUserInfo(String username, UserAttribute attr) {
+			user = username;
+			pass = attr.getPassword();
+			auth = attr.getAuthorities();
+		}
+
+		User get(PasswordEncoder encoder) {
+			User u = new User();
+			boolean realUser = false;
+			for (GrantedAuthority ga : auth) {
+				String a = ga.getAuthority();
+				if (a.startsWith(AUTHORITY_PREFIX))
+					u.setLocalUsername(a.substring(AUTHORITY_PREFIX.length()));
+				else if (a.equals(USER))
+					realUser = true;
+				else if (a.equals(ADMIN))
+					u.setAdmin(true);
+			}
+			if (!realUser)
+				return null;
+			u.setUsername(user);
+			u.setEncodedPassword(encoder.encode(pass));
+			u.setDisabled(false);
+			return u;
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java b/server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java
new file mode 100644
index 0000000..a048da9
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/UserStoreAPI.java
@@ -0,0 +1,91 @@
+package org.taverna.server.master.identity;
+
+import java.util.List;
+
+/**
+ * The API that is exposed by the DAO that exposes user management.
+ * 
+ * @author Donal Fellows
+ * @see User
+ */
+public interface UserStoreAPI {
+	/**
+	 * List the currently-known account names.
+	 * 
+	 * @return A list of users in the database. Note that this is a snapshot.
+	 */
+	List<String> getUserNames();
+
+	/**
+	 * Get a particular user's description.
+	 * 
+	 * @param userName
+	 *            The username to look up.
+	 * @return A <i>copy</i> of the user description.
+	 */
+	User getUser(String userName);
+
+	/**
+	 * Create a new user account; the account will be disabled and
+	 * non-administrative by default. Does not create any underlying system
+	 * account.
+	 * 
+	 * @param username
+	 *            The username to create.
+	 * @param password
+	 *            The password to use.
+	 * @param coupleLocalUsername
+	 *            Whether to set the local user name to the 'main' one.
+	 */
+	void addUser(String username, String password, boolean coupleLocalUsername);
+
+	/**
+	 * Set or clear whether this account is enabled. Disabled accounts cannot be
+	 * used to log in.
+	 * 
+	 * @param username
+	 *            The username to adjust.
+	 * @param enabled
+	 *            Whether to enable the account.
+	 */
+	void setUserEnabled(String username, boolean enabled);
+
+	/**
+	 * Set or clear the mark on an account that indicates that it has
+	 * administrative privileges.
+	 * 
+	 * @param username
+	 *            The username to adjust.
+	 * @param admin
+	 *            Whether the account has admin privileges.
+	 */
+	void setUserAdmin(String username, boolean admin);
+
+	/**
+	 * Change the password for an account.
+	 * 
+	 * @param username
+	 *            The username to adjust.
+	 * @param password
+	 *            The new password to use.
+	 */
+	void setUserPassword(String username, String password);
+
+	/**
+	 * Change what local system account to use for a server account.
+	 * 
+	 * @param username
+	 *            The username to adjust.
+	 * @param localUsername
+	 *            The new local user account use.
+	 */
+	void setUserLocalUser(String username, String localUsername);
+
+	/**
+	 * Delete a server account. The underlying system account is not modified.
+	 * 
+	 * @param username
+	 *            The username to delete.
+	 */
+	void deleteUser(String username);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java b/server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java
new file mode 100644
index 0000000..9219a60
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/WorkflowInternalAuthProvider.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE.txt" for license terms.
+ */
+package org.taverna.server.master.identity;
+
+import static java.util.Collections.synchronizedMap;
+import static org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes;
+import static org.taverna.server.master.common.Roles.SELF;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.web.authentication.WebAuthenticationDetails;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.interfaces.RunStore;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.UsernamePrincipal;
+import org.taverna.server.master.worker.RunDatabaseDAO;
+
+/**
+ * A special authentication provider that allows a workflow to authenticate to
+ * itself. This is used to allow the workflow to publish to its own interaction
+ * feed.
+ * 
+ * @author Donal Fellows
+ */
+public class WorkflowInternalAuthProvider extends
+		AbstractUserDetailsAuthenticationProvider {
+	private Log log = LogFactory.getLog("Taverna.Server.UserDB");
+	private static final boolean logDecisions = true;
+	public static final String PREFIX = "wfrun_";
+	private RunDatabaseDAO dao;
+	private Map<String, String> cache;
+
+	@Required
+	public void setDao(RunDatabaseDAO dao) {
+		this.dao = dao;
+	}
+
+	@Required
+	@SuppressWarnings("serial")
+	public void setCacheBound(final int bound) {
+		cache = synchronizedMap(new LinkedHashMap<String, String>() {
+			@Override
+			protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
+				return size() > bound;
+			}
+		});
+	}
+
+	public void setAuthorizedAddresses(String[] addresses) {
+		authorizedAddresses = new HashSet<>(localAddresses);
+		for (String s : addresses)
+			authorizedAddresses.add(s);
+	}
+
+	@PostConstruct
+	public void logConfig() {
+		log.info("authorized addresses for automatic access: "
+				+ authorizedAddresses);
+	}
+
+	@PreDestroy
+	void closeLog() {
+		log = null;
+	}
+
+	private final Set<String> localAddresses = new HashSet<>();
+	private Set<String> authorizedAddresses;
+	{
+		localAddresses.add("127.0.0.1"); // IPv4
+		localAddresses.add("::1"); // IPv6
+		try {
+			InetAddress addr = InetAddress.getLocalHost();
+			if (!addr.isLoopbackAddress())
+				localAddresses.add(addr.getHostAddress());
+		} catch (UnknownHostException e) {
+			// Ignore the exception
+		}
+		authorizedAddresses = new HashSet<>(localAddresses);
+	}
+
+	/**
+	 * Check that the authentication request is actually valid for the given
+	 * user record.
+	 * 
+	 * @param userRecord
+	 *            as retrieved from the
+	 *            {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)}
+	 *            or <code>UserCache</code>
+	 * @param principal
+	 *            the principal that is trying to authenticate (and that we're
+	 *            trying to bind)
+	 * @param credentials
+	 *            the credentials (e.g., password) presented by the principal
+	 * 
+	 * @throws AuthenticationException
+	 *             AuthenticationException if the credentials could not be
+	 *             validated (generally a <code>BadCredentialsException</code>,
+	 *             an <code>AuthenticationServiceException</code>)
+	 * @throws Exception
+	 *             If something goes wrong. Will be logged and converted to a
+	 *             generic AuthenticationException.
+	 */
+	protected void additionalAuthenticationChecks(UserDetails userRecord,
+			@Nonnull Object principal, @Nonnull Object credentials)
+			throws Exception {
+		@Nonnull
+		HttpServletRequest req = ((ServletRequestAttributes) currentRequestAttributes())
+				.getRequest();
+
+		// Are we coming from a "local" address?
+		if (!req.getLocalAddr().equals(req.getRemoteAddr())
+				&& !authorizedAddresses.contains(req.getRemoteAddr())) {
+			if (logDecisions)
+				log.info("attempt to use workflow magic token from untrusted address:"
+						+ " token="
+						+ userRecord.getUsername()
+						+ ", address="
+						+ req.getRemoteAddr());
+			throw new BadCredentialsException("bad login token");
+		}
+
+		// Does the password match?
+		if (!credentials.equals(userRecord.getPassword())) {
+			if (logDecisions)
+				log.info("workflow magic token is untrusted due to password mismatch:"
+						+ " wanted="
+						+ userRecord.getPassword()
+						+ ", got="
+						+ credentials);
+			throw new BadCredentialsException("bad login token");
+		}
+
+		if (logDecisions)
+			log.info("granted role " + SELF + " to user "
+					+ userRecord.getUsername());
+	}
+
+	/**
+	 * Retrieve the <code>UserDetails</code> from the relevant store, with the
+	 * option of throwing an <code>AuthenticationException</code> immediately if
+	 * the presented credentials are incorrect (this is especially useful if it
+	 * is necessary to bind to a resource as the user in order to obtain or
+	 * generate a <code>UserDetails</code>).
+	 * 
+	 * @param username
+	 *            The username to retrieve
+	 * @param details
+	 *            The details from the authentication request.
+	 * @see #retrieveUser(String,UsernamePasswordAuthenticationToken)
+	 * @return the user information (never <code>null</code> - instead an
+	 *         exception should the thrown)
+	 * @throws AuthenticationException
+	 *             if the credentials could not be validated (generally a
+	 *             <code>BadCredentialsException</code>, an
+	 *             <code>AuthenticationServiceException</code> or
+	 *             <code>UsernameNotFoundException</code>)
+	 * @throws Exception
+	 *             If something goes wrong. It will be logged and converted into
+	 *             a general AuthenticationException.
+	 */
+	@Nonnull
+	protected UserDetails retrieveUser(String username, Object details)
+			throws Exception {
+		if (details == null || !(details instanceof WebAuthenticationDetails))
+			throw new UsernameNotFoundException("context unsupported");
+		if (!username.startsWith(PREFIX))
+			throw new UsernameNotFoundException(
+					"unsupported username for this provider");
+		if (logDecisions)
+			log.info("request for auth for user " + username);
+		String wfid = username.substring(PREFIX.length());
+		String securityToken;
+		try {
+			securityToken = cache.get(wfid);
+			if (securityToken == null) {
+				securityToken = dao.getSecurityToken(wfid);
+				if (securityToken == null)
+					throw new UsernameNotFoundException("no such user");
+				cache.put(wfid, securityToken);
+			}
+		} catch (NullPointerException npe) {
+			throw new UsernameNotFoundException("no such user");
+		}
+		return new User(username, securityToken, true, true, true, true,
+				Arrays.asList(new LiteralGrantedAuthority(SELF),
+						new WorkflowSelfAuthority(wfid)));
+	}
+
+	@Override
+	@PerfLogged
+	protected final void additionalAuthenticationChecks(UserDetails userRecord,
+			UsernamePasswordAuthenticationToken token) {
+		try {
+			additionalAuthenticationChecks(userRecord, token.getPrincipal(),
+					token.getCredentials());
+		} catch (AuthenticationException e) {
+			throw e;
+		} catch (Exception e) {
+			log.warn("unexpected failure in authentication", e);
+			throw new AuthenticationServiceException(
+					"unexpected failure in authentication", e);
+		}
+	}
+
+	@Override
+	@Nonnull
+	@PerfLogged
+	protected final UserDetails retrieveUser(String username,
+			UsernamePasswordAuthenticationToken token) {
+		try {
+			return retrieveUser(username, token.getDetails());
+		} catch (AuthenticationException e) {
+			throw e;
+		} catch (Exception e) {
+			log.warn("unexpected failure in authentication", e);
+			throw new AuthenticationServiceException(
+					"unexpected failure in authentication", e);
+		}
+	}
+
+	@SuppressWarnings("serial")
+	public static class WorkflowSelfAuthority extends LiteralGrantedAuthority {
+		public WorkflowSelfAuthority(String wfid) {
+			super(wfid);
+		}
+
+		public String getWorkflowID() {
+			return getAuthority();
+		}
+
+		@Override
+		public String toString() {
+			return "WORKFLOW(" + getAuthority() + ")";
+		}
+	}
+
+	public static class WorkflowSelfIDMapper implements LocalIdentityMapper {
+		private Log log = LogFactory.getLog("Taverna.Server.UserDB");
+		private RunStore runStore;
+
+		@PreDestroy
+		void closeLog() {
+			log = null;
+		}
+
+		@Required
+		public void setRunStore(RunStore runStore) {
+			this.runStore = runStore;
+		}
+
+		private String getUsernameForSelfAccess(WorkflowSelfAuthority authority)
+				throws UnknownRunException {
+			return runStore.getRun(authority.getWorkflowID())
+					.getSecurityContext().getOwner().getName();
+		}
+
+		@Override
+		@PerfLogged
+		public String getUsernameForPrincipal(UsernamePrincipal user) {
+			Authentication auth = SecurityContextHolder.getContext()
+					.getAuthentication();
+			if (auth == null || !auth.isAuthenticated())
+				return null;
+			try {
+				for (GrantedAuthority authority : auth.getAuthorities())
+					if (authority instanceof WorkflowSelfAuthority)
+						return getUsernameForSelfAccess((WorkflowSelfAuthority) authority);
+			} catch (UnknownRunException e) {
+				log.warn("workflow run disappeared during computation of workflow map identity");
+			}
+			return null;
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java
new file mode 100644
index 0000000..dd1500a
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/identity/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Implementations of beans that map global user identities to local
+ * usernames.
+ */
+package org.taverna.server.master.identity;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java b/server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java
new file mode 100644
index 0000000..99e1d99
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interaction/InteractionFeedSupport.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interaction;
+
+import static java.lang.management.ManagementFactory.getPlatformMBeanServer;
+import static java.util.Collections.reverse;
+import static javax.management.Query.attr;
+import static javax.management.Query.match;
+import static javax.management.Query.value;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.factory.Factory;
+import org.apache.abdera.i18n.iri.IRI;
+import org.apache.abdera.model.Document;
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.model.Feed;
+import org.apache.abdera.parser.Parser;
+import org.apache.abdera.writer.Writer;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.TavernaServerSupport;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.utils.FilenameUtils;
+
+/**
+ * Bean that supports interaction feeds. This glues together the Abdera
+ * serialization engine and the directory-based model used inside the server.
+ * 
+ * @author Donal Fellows
+ */
+public class InteractionFeedSupport {
+	/**
+	 * The name of the resource within the run resource that is the run's
+	 * interaction feed resource.
+	 */
+	public static final String FEED_URL_DIR = "interaction";
+	/**
+	 * The name of the directory below the run working directory that will
+	 * contain the entries of the interaction feed.
+	 */
+	public static final String FEED_DIR = "feed";
+	/**
+	 * Should the contents of the entry be stripped when describing the overall
+	 * feed? This makes sense if (and only if) large entries are being pushed
+	 * through the feed.
+	 */
+	private static final boolean STRIP_CONTENTS = false;
+	/** Maximum size of an entry before truncation. */
+	private static final long MAX_ENTRY_SIZE = 50 * 1024;
+	/** Extension for entry files. */
+	private static final String EXT = ".atom";
+
+	private TavernaServerSupport support;
+	private FilenameUtils utils;
+	private Writer writer;
+	private Parser parser;
+	private Factory factory;
+	private UriBuilderFactory uriBuilder;
+
+	private AtomicInteger counter = new AtomicInteger();
+
+	@Required
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	@Required
+	public void setUtils(FilenameUtils utils) {
+		this.utils = utils;
+	}
+
+	@Required
+	public void setAbdera(Abdera abdera) {
+		this.factory = abdera.getFactory();
+		this.parser = abdera.getParser();
+		this.writer = abdera.getWriterFactory().getWriter("prettyxml");
+	}
+
+	@Required
+	// webapp
+	public void setUriBuilder(UriBuilderFactory uriBuilder) {
+		this.uriBuilder = uriBuilder;
+	}
+
+	private final Map<String, URL> endPoints = new HashMap<>();
+
+	@PostConstruct
+	void determinePorts() {
+		try {
+			MBeanServer mbs = getPlatformMBeanServer();
+			for (ObjectName obj : mbs.queryNames(new ObjectName(
+					"*:type=Connector,*"),
+					match(attr("protocol"), value("HTTP/1.1")))) {
+				String scheme = mbs.getAttribute(obj, "scheme").toString();
+				String port = obj.getKeyProperty("port");
+				endPoints.put(scheme, new URL(scheme + "://localhost:" + port));
+			}
+			getLog(getClass()).info(
+					"installed feed port publication mapping for "
+							+ endPoints.keySet());
+		} catch (Exception e) {
+			getLog(getClass()).error(
+					"failure in determining local port mapping", e);
+		}
+	}
+	
+	/**
+	 * @param run
+	 *            The workflow run that defines which feed we are operating on.
+	 * @return The URI of the feed
+	 */
+	public URI getFeedURI(TavernaRun run) {
+		return uriBuilder.getRunUriBuilder(run).path(FEED_URL_DIR).build();
+	}
+
+	@Nullable
+	public URL getLocalFeedBase(URI feedURI) {
+		if (feedURI == null)
+			return null;
+		return endPoints.get(feedURI.getScheme());
+	}
+
+	/**
+	 * @param run
+	 *            The workflow run that defines which feed we are operating on.
+	 * @param id
+	 *            The ID of the entry.
+	 * @return The URI of the entry.
+	 */
+	public URI getEntryURI(TavernaRun run, String id) {
+		return uriBuilder.getRunUriBuilder(run)
+				.path(FEED_URL_DIR + "/{entryID}").build(id);
+	}
+
+	private Entry getEntryFromFile(File f) throws FilesystemAccessException {
+		long size = f.getSize();
+		if (size > MAX_ENTRY_SIZE)
+			throw new FilesystemAccessException("entry larger than 50kB");
+		byte[] contents = f.getContents(0, (int) size);
+		Document<Entry> doc = parser.parse(new ByteArrayInputStream(contents));
+		return doc.getRoot();
+	}
+
+	private void putEntryInFile(Directory dir, String name, Entry contents)
+			throws FilesystemAccessException, NoUpdateException {
+		ByteArrayOutputStream baos = new ByteArrayOutputStream();
+		try {
+			writer.writeTo(contents, baos);
+		} catch (IOException e) {
+			throw new NoUpdateException("failed to serialize the ATOM entry", e);
+		}
+		File f = dir.makeEmptyFile(support.getPrincipal(), name);
+		f.appendContents(baos.toByteArray());
+	}
+
+	private List<DirectoryEntry> listPossibleEntries(TavernaRun run)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		List<DirectoryEntry> entries = new ArrayList<>(utils.getDirectory(run,
+				FEED_DIR).getContentsByDate());
+		reverse(entries);
+		return entries;
+	}
+
+	private String getRunURL(TavernaRun run) {
+		return new IRI(uriBuilder.getRunUriBuilder(run).build()).toString();
+	}
+
+	/**
+	 * Get the interaction feed for a partciular run.
+	 * 
+	 * @param run
+	 *            The workflow run that defines which feed we are operating on.
+	 * @return The Abdera feed descriptor.
+	 * @throws FilesystemAccessException
+	 *             If the feed directory can't be read for some reason.
+	 * @throws NoDirectoryEntryException
+	 *             If the feed directory doesn't exist or an entry is
+	 *             unexpectedly removed.
+	 */
+	public Feed getRunFeed(TavernaRun run) throws FilesystemAccessException,
+			NoDirectoryEntryException {
+		URI feedURI = getFeedURI(run);
+		Feed feed = factory.newFeed();
+		feed.setTitle("Interactions for Taverna Run \"" + run.getName() + "\"");
+		feed.addLink(new IRI(feedURI).toString(), "self");
+		feed.addLink(getRunURL(run), "workflowrun");
+		boolean fetchedDate = false;
+		for (DirectoryEntry de : listPossibleEntries(run)) {
+			if (!(de instanceof File))
+				continue;
+			try {
+				Entry e = getEntryFromFile((File) de);
+				if (STRIP_CONTENTS)
+					e.setContentElement(null);
+				feed.addEntry(e);
+				if (fetchedDate)
+					continue;
+				Date last = e.getUpdated();
+				if (last == null)
+					last = e.getPublished();
+				if (last == null)
+					last = de.getModificationDate();
+				feed.setUpdated(last);
+				fetchedDate = true;
+			} catch (FilesystemAccessException e) {
+				// Can't do anything about it, so we'll just drop the entry.
+			}
+		}
+		return feed;
+	}
+
+	/**
+	 * Gets the contents of a particular feed entry.
+	 * 
+	 * @param run
+	 *            The workflow run that defines which feed we are operating on.
+	 * @param entryID
+	 *            The identifier (from the path) of the entry to read.
+	 * @return The description of the entry.
+	 * @throws FilesystemAccessException
+	 *             If the entry can't be read or is too large.
+	 * @throws NoDirectoryEntryException
+	 *             If the entry can't be found.
+	 */
+	public Entry getRunFeedEntry(TavernaRun run, String entryID)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		File entryFile = utils.getFile(run, FEED_DIR + "/" + entryID + EXT);
+		return getEntryFromFile(entryFile);
+	}
+
+	/**
+	 * Given a partial feed entry, store a complete feed entry in the filesystem
+	 * for a particular run. Note that this does not permit update of an
+	 * existing entry; the entry is always created new.
+	 * 
+	 * @param run
+	 *            The workflow run that defines which feed we are operating on.
+	 * @param entry
+	 *            The partial entry to store
+	 * @return A link to the entry.
+	 * @throws FilesystemAccessException
+	 *             If the entry can't be stored.
+	 * @throws NoDirectoryEntryException
+	 *             If the run is improperly configured.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to do the write.
+	 * @throws MalformedURLException
+	 *             If a generated URL is illegal (shouldn't happen).
+	 */
+	public Entry addRunFeedEntry(TavernaRun run, Entry entry)
+			throws FilesystemAccessException, NoDirectoryEntryException,
+			NoUpdateException {
+		support.permitUpdate(run);
+		Date now = new Date();
+		entry.newId();
+		String localId = "entry_" + counter.incrementAndGet();
+		IRI selfLink = new IRI(getEntryURI(run, localId));
+		entry.addLink(selfLink.toString(), "self");
+		entry.addLink(getRunURL(run), "workflowrun");
+		entry.setUpdated(now);
+		entry.setPublished(now);
+		putEntryInFile(utils.getDirectory(run, FEED_DIR), localId + EXT, entry);
+		return getEntryFromFile(utils.getFile(run, FEED_DIR + "/" + localId
+				+ EXT));
+	}
+
+	/**
+	 * Deletes an entry from a feed.
+	 * 
+	 * @param run
+	 *            The workflow run that defines which feed we are operating on.
+	 * @param entryID
+	 *            The ID of the entry to delete.
+	 * @throws FilesystemAccessException
+	 *             If the entry can't be deleted
+	 * @throws NoDirectoryEntryException
+	 *             If the entry can't be found.
+	 * @throws NoUpdateException
+	 *             If the current user is not permitted to modify the run's
+	 *             characteristics.
+	 */
+	public void removeRunFeedEntry(TavernaRun run, String entryID)
+			throws FilesystemAccessException, NoDirectoryEntryException,
+			NoUpdateException {
+		support.permitUpdate(run);
+		utils.getFile(run, FEED_DIR + "/" + entryID + EXT).destroy();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java
new file mode 100644
index 0000000..9efc30d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interaction/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the Atom feed implementation for interactions for a particular workflow run.
+ * @author Donal Fellows
+ */
+package org.taverna.server.master.interaction;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java
new file mode 100644
index 0000000..9a0a84e
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/Directory.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.io.PipedInputStream;
+import java.security.Principal;
+import java.util.Collection;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+/**
+ * Represents a directory that is the working directory of a workflow run, or a
+ * sub-directory of it.
+ * 
+ * @author Donal Fellows
+ * @see File
+ */
+public interface Directory extends DirectoryEntry {
+	/**
+	 * @return A list of the contents of the directory.
+	 * @throws FilesystemAccessException
+	 *             If things go wrong.
+	 */
+	Collection<DirectoryEntry> getContents() throws FilesystemAccessException;
+
+	/**
+	 * @return A list of the contents of the directory, in guaranteed date
+	 *         order.
+	 * @throws FilesystemAccessException
+	 *             If things go wrong.
+	 */
+	Collection<DirectoryEntry> getContentsByDate()
+			throws FilesystemAccessException;
+
+	/**
+	 * @return The contents of the directory (and its sub-directories) as a zip.
+	 * @throws FilesystemAccessException
+	 *             If things go wrong.
+	 */
+	ZipStream getContentsAsZip() throws FilesystemAccessException;
+
+	/**
+	 * Creates a sub-directory of this directory.
+	 * 
+	 * @param actor
+	 *            Who this is being created by.
+	 * @param name
+	 *            The name of the sub-directory.
+	 * @return A handle to the newly-created directory.
+	 * @throws FilesystemAccessException
+	 *             If the name is the same as some existing entry in the
+	 *             directory, or if something else goes wrong during creation.
+	 */
+	Directory makeSubdirectory(Principal actor, String name)
+			throws FilesystemAccessException;
+
+	/**
+	 * Creates an empty file in this directory.
+	 * 
+	 * @param actor
+	 *            Who this is being created by.
+	 * @param name
+	 *            The name of the file to create.
+	 * @return A handle to the newly-created file.
+	 * @throws FilesystemAccessException
+	 *             If the name is the same as some existing entry in the
+	 *             directory, or if something else goes wrong during creation.
+	 */
+	File makeEmptyFile(Principal actor, String name)
+			throws FilesystemAccessException;
+
+	/**
+	 * A simple pipe that produces the zipped contents of a directory.
+	 * 
+	 * @author Donal Fellows
+	 */
+	public static class ZipStream extends PipedInputStream {
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java
new file mode 100644
index 0000000..b098152
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/DirectoryEntry.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.util.Date;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+/**
+ * An entry in a {@link Directory} representing a file or sub-directory.
+ * 
+ * @author Donal Fellows
+ * @see Directory
+ * @see File
+ */
+public interface DirectoryEntry extends Comparable<DirectoryEntry> {
+	/**
+	 * @return The "local" name of the entry. This will never be "<tt>..</tt>"
+	 *         or contain the character "<tt>/</tt>".
+	 */
+	public String getName();
+
+	/**
+	 * @return The "full" name of the entry. This is computed relative to the
+	 *         workflow run's working directory. It may contain the "<tt>/</tt>"
+	 *         character.
+	 */
+	public String getFullName();
+
+	/**
+	 * @return The time that the entry was last modified.
+	 */
+	public Date getModificationDate();
+
+	/**
+	 * Destroy this directory entry, deleting the file or sub-directory. The
+	 * workflow run's working directory can never be manually destroyed.
+	 * 
+	 * @throws FilesystemAccessException
+	 *             If the destroy fails for some reason.
+	 */
+	public void destroy() throws FilesystemAccessException;
+	// TODO: Permissions (or decide not to do anything about them)
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java
new file mode 100644
index 0000000..e4e6590
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/File.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+/**
+ * Represents a file in the working directory of a workflow instance run, or in
+ * some sub-directory of it.
+ * 
+ * @author Donal Fellows
+ * @see Directory
+ */
+public interface File extends DirectoryEntry {
+	/**
+	 * @param offset
+	 *            Where in the file to start reading.
+	 * @param length
+	 *            The length of file to read, or -1 to read to the end of the
+	 *            file.
+	 * @return The literal byte contents of the section of the file, or null if
+	 *         the section doesn't exist.
+	 * @throws FilesystemAccessException
+	 *             If the read of the file goes wrong.
+	 */
+	public byte[] getContents(int offset, int length)
+			throws FilesystemAccessException;
+
+	/**
+	 * Write the data to the file, totally replacing what was there before.
+	 * 
+	 * @param data
+	 *            The literal bytes that will form the new contents of the file.
+	 * @throws FilesystemAccessException
+	 *             If the write to the file goes wrong.
+	 */
+	public void setContents(byte[] data) throws FilesystemAccessException;
+
+	/**
+	 * Append the data to the file.
+	 * 
+	 * @param data
+	 *            The literal bytes that will be added on to the end of the
+	 *            file.
+	 * @throws FilesystemAccessException
+	 *             If the write to the file goes wrong.
+	 */
+	public void appendContents(byte[] data) throws FilesystemAccessException;
+
+	/**
+	 * @return The length of the file, in bytes.
+	 * @throws FilesystemAccessException
+	 *             If the read of the file size goes wrong.
+	 */
+	public long getSize() throws FilesystemAccessException;
+
+	/**
+	 * Asks for the argument file to be copied to this one.
+	 * 
+	 * @param from
+	 *            The source file.
+	 * @throws FilesystemAccessException
+	 *             If anything goes wrong.
+	 */
+	public void copy(File from) throws FilesystemAccessException;
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java
new file mode 100644
index 0000000..31cb7cb
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/Input.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+/**
+ * This represents the assignment of inputs to input ports of the workflow. Note
+ * that the <tt>file</tt> and <tt>value</tt> properties are never set at the
+ * same time.
+ * 
+ * @author Donal Fellows
+ */
+public interface Input {
+	/**
+	 * @return The file currently assigned to this input port, or <tt>null</tt>
+	 *         if no file is assigned.
+	 */
+	@Nullable
+	public String getFile();
+
+	/**
+	 * @return The name of this input port. This may not be changed.
+	 */
+	@Nonnull
+	public String getName();
+
+	/**
+	 * @return The value currently assigned to this input port, or <tt>null</tt>
+	 *         if no value is assigned.
+	 */
+	@Nullable
+	public String getValue();
+
+	/**
+	 * @return The delimiter for the input port, or <tt>null</tt> if the value
+	 *         is not to be split.
+	 */
+	@Nullable
+	public String getDelimiter();
+
+	/**
+	 * Sets the file to use for this input. This overrides the use of the
+	 * previous file and any set value.
+	 * 
+	 * @param file
+	 *            The filename to use. Must not start with a <tt>/</tt> or
+	 *            contain any <tt>..</tt> segments. Will be interpreted relative
+	 *            to the run's working directory.
+	 * @throws FilesystemAccessException
+	 *             If the filename is invalid.
+	 * @throws BadStateChangeException
+	 *             If the run isn't in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	public void setFile(String file) throws FilesystemAccessException,
+			BadStateChangeException;
+
+	/**
+	 * Sets the value to use for this input. This overrides the use of the
+	 * previous value and any set file.
+	 * 
+	 * @param value
+	 *            The value to use.
+	 * @throws BadStateChangeException
+	 *             If the run isn't in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	public void setValue(String value) throws BadStateChangeException;
+
+	/**
+	 * Sets (or clears) the delimiter for the input port.
+	 * 
+	 * @param delimiter
+	 *            The delimiter character, or <tt>null</tt> if the value is not
+	 *            to be split.
+	 * @throws BadStateChangeException
+	 *             If the run isn't in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	@Nullable
+	public void setDelimiter(String delimiter) throws BadStateChangeException;
+
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java
new file mode 100644
index 0000000..d7998bc
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/Listener.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+import org.taverna.server.master.exceptions.NoListenerException;
+
+/**
+ * An event listener that can be attached to a {@link TavernaRun}.
+ * 
+ * @author Donal Fellows
+ */
+public interface Listener {
+	/**
+	 * @return The name of the listener.
+	 */
+	public String getName();
+
+	/**
+	 * @return The type of the listener.
+	 */
+	public String getType();
+
+	/**
+	 * @return The configuration document for the listener.
+	 */
+	public String getConfiguration();
+
+	/**
+	 * @return The supported properties of the listener.
+	 */
+	public String[] listProperties();
+
+	/**
+	 * Get the value of a particular property, which should be listed in the
+	 * {@link #listProperties()} method.
+	 * 
+	 * @param propName
+	 *            The name of the property to read.
+	 * @return The value of the property.
+	 * @throws NoListenerException
+	 *             If no property with that name exists.
+	 */
+	public String getProperty(String propName) throws NoListenerException;
+
+	/**
+	 * Set the value of a particular property, which should be listed in the
+	 * {@link #listProperties()} method.
+	 * 
+	 * @param propName
+	 *            The name of the property to write.
+	 * @param value
+	 *            The value to set the property to.
+	 * @throws NoListenerException
+	 *             If no property with that name exists.
+	 * @throws BadPropertyValueException
+	 *             If the value of the property is bad (e.g., wrong syntax).
+	 */
+	public void setProperty(String propName, String value)
+			throws NoListenerException, BadPropertyValueException;
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java
new file mode 100644
index 0000000..37b104e
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/LocalIdentityMapper.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * This interface describes how to map from the identity understood by the
+ * webapp to the identity understood by the local execution system.
+ * 
+ * @author Donal Fellows
+ */
+public interface LocalIdentityMapper {
+	/**
+	 * Given a user's identity, get the local identity to use for executing
+	 * their workflows. Note that it is assumed that there will never be a
+	 * failure from this interface; it is <i>not</i> a security policy
+	 * decision or enforcement point.
+	 * 
+	 * @param user
+	 *            An identity token.
+	 * @return A user name, which must be defined in the context that workflows
+	 *         will be running in.
+	 */
+	public String getUsernameForPrincipal(UsernamePrincipal user);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java
new file mode 100644
index 0000000..37dbf2c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/MessageDispatcher.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import javax.annotation.Nonnull;
+
+/**
+ * The interface supported by all notification message dispatchers.
+ * @author Donal Fellows
+ */
+public interface MessageDispatcher {
+	/**
+	 * @return Whether this message dispatcher is actually available (fully
+	 *         configured, etc.)
+	 */
+	boolean isAvailable();
+
+	/**
+	 * @return The name of this dispatcher, which must match the protocol
+	 *         supported by it (for a non-universal dispatcher) and the name of
+	 *         the message generator used to produce the message.
+	 */
+	String getName();
+
+	/**
+	 * Dispatch a message to a recipient.
+	 * 
+	 * @param originator
+	 *            The workflow run that produced the message.
+	 * @param messageSubject
+	 *            The subject of the message to send.
+	 * @param messageContent
+	 *            The plain-text content of the message to send.
+	 * @param targetParameter
+	 *            A description of where it is to go.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	void dispatch(@Nonnull TavernaRun originator,
+			@Nonnull String messageSubject, @Nonnull String messageContent,
+			@Nonnull String targetParameter) throws Exception;
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java
new file mode 100644
index 0000000..f57fe71
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/Policy.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.net.URI;
+import java.util.List;
+
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Simple policy interface.
+ * 
+ * @author Donal Fellows
+ */
+public interface Policy {
+	/**
+	 * @return The maximum number of runs that the system can support.
+	 */
+	int getMaxRuns();
+
+	/**
+	 * Get the limit on the number of runs for this user.
+	 * 
+	 * @param user
+	 *            Who to get the limit for
+	 * @return The maximum number of runs for this user, or <tt>null</tt> if no
+	 *         per-user limit is imposed and only system-wide limits are to be
+	 *         enforced.
+	 */
+	Integer getMaxRuns(UsernamePrincipal user);
+
+	/**
+	 * Test whether the user can create an instance of the given workflow.
+	 * 
+	 * @param user
+	 *            Who wants to do the creation.
+	 * @param workflow
+	 *            The workflow they wish to instantiate.
+	 * @throws NoCreateException
+	 *             If they may not instantiate it.
+	 */
+	void permitCreate(UsernamePrincipal user, Workflow workflow)
+			throws NoCreateException;
+
+	/**
+	 * Test whether the user can destroy a workflow instance run or manipulate
+	 * its expiry date.
+	 * 
+	 * @param user
+	 *            Who wants to do the deletion.
+	 * @param run
+	 *            What they want to delete.
+	 * @throws NoDestroyException
+	 *             If they may not destroy it.
+	 */
+	void permitDestroy(UsernamePrincipal user, TavernaRun run)
+			throws NoDestroyException;
+
+	/**
+	 * Return whether the user has access to a particular workflow run.
+	 * <b>Note</b> that this does not throw any exceptions!
+	 * 
+	 * @param user
+	 *            Who wants to read the workflow's state.
+	 * @param run
+	 *            What do they want to read from.
+	 * @return Whether they can read it. Note that this check is always applied
+	 *         before testing whether the workflow can be updated or deleted by
+	 *         the user.
+	 */
+	boolean permitAccess(UsernamePrincipal user, TavernaRun run);
+
+	/**
+	 * Test whether the user can modify a workflow run (other than for its
+	 * expiry date).
+	 * 
+	 * @param user
+	 *            Who wants to do the modification.
+	 * @param run
+	 *            What they want to modify.
+	 * @throws NoUpdateException
+	 *             If they may not modify it.
+	 */
+	void permitUpdate(UsernamePrincipal user, TavernaRun run)
+			throws NoUpdateException;
+
+	/**
+	 * Get the URIs of the workflows that the given user may execute.
+	 * 
+	 * @param user
+	 *            Who are we finding out on behalf of.
+	 * @return A list of workflow URIs that they may instantiate, or
+	 *         <tt>null</tt> if any workflow may be submitted.
+	 */
+	List<URI> listPermittedWorkflowURIs(UsernamePrincipal user);
+
+	/**
+	 * @return The maximum number of {@linkplain Status#Operating operating}
+	 *         runs that the system can support.
+	 */
+	int getOperatingLimit();
+
+	/**
+	 * Set the URIs of the workflows that the given user may execute.
+	 * 
+	 * @param user
+	 *            Who are we finding out on behalf of.
+	 * @param permitted
+	 *            A list of workflow URIs that they may instantiate.
+	 */
+	void setPermittedWorkflowURIs(UsernamePrincipal user, List<URI> permitted);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java
new file mode 100644
index 0000000..b5e84c5
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/RunStore.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.util.Map;
+
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Interface to the mechanism that looks after the mapping of names to runs.
+ * Instances of this class may also be responsible for enforcing timely cleanup
+ * of expired workflows.
+ * 
+ * @author Donal Fellows.
+ */
+public interface RunStore {
+	/**
+	 * Obtain the workflow run for a given user and name.
+	 * 
+	 * @param user
+	 *            Who wants to do the lookup.
+	 * @param p
+	 *            The general policy system context.
+	 * @param uuid
+	 *            The handle for the run.
+	 * @return The workflow instance run.
+	 * @throws UnknownRunException
+	 *             If the lookup fails (either because it does not exist or
+	 *             because it is not permitted for the user by the policy).
+	 */
+	TavernaRun getRun(UsernamePrincipal user, Policy p, String uuid)
+			throws UnknownRunException;
+
+	/**
+	 * Obtain the named workflow run.
+	 * 
+	 * @param uuid
+	 *            The handle for the run.
+	 * @return The workflow instance run.
+	 * @throws UnknownRunException
+	 *             If the lookup fails (either because it does not exist or
+	 *             because it is not permitted for the user by the policy).
+	 */
+	public TavernaRun getRun(String uuid) throws UnknownRunException;
+
+	/**
+	 * List the runs that a particular user may access.
+	 * 
+	 * @param user
+	 *            Who wants to do the lookup, or <code>null</code> if it is
+	 *            being done "by the system" when the full mapping should be
+	 *            returned.
+	 * @param p
+	 *            The general policy system context.
+	 * @return A mapping from run names to run instances.
+	 */
+	Map<String, TavernaRun> listRuns(UsernamePrincipal user, Policy p);
+
+	/**
+	 * Adds a workflow instance run to the store. Note that this operation is
+	 * <i>not</i> expected to be security-checked; that is the callers'
+	 * responsibility.
+	 * 
+	 * @param run
+	 *            The run itself.
+	 * @return The name of the run.
+	 */
+	String registerRun(TavernaRun run);
+
+	/**
+	 * Removes a run from the store. Note that this operation is <i>not</i>
+	 * expected to be security-checked; that is the callers' responsibility.
+	 * 
+	 * @param uuid
+	 *            The name of the run.
+	 */
+	void unregisterRun(String uuid);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java
new file mode 100644
index 0000000..902c4d0
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/SecurityContextFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.io.Serializable;
+
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * How to create instances of a security context.
+ * 
+ * @author Donal Fellows
+ */
+public interface SecurityContextFactory extends Serializable {
+	/**
+	 * Creates a security context.
+	 * 
+	 * @param run
+	 *            Handle to remote run. Allows the security context to know how
+	 *            to apply itself to the workflow run.
+	 * @param owner
+	 *            The identity of the owner of the workflow run.
+	 * @return The security context.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	TavernaSecurityContext create(TavernaRun run, UsernamePrincipal owner)
+			throws Exception;
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/TavernaRun.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/TavernaRun.java
new file mode 100644
index 0000000..399164d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/TavernaRun.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+
+/**
+ * The interface to a taverna workflow run, or "run" for short.
+ * 
+ * @author Donal Fellows
+ */
+public interface TavernaRun extends Serializable {
+	/**
+	 * @return The identifier of the run.
+	 */
+	String getId();
+
+	/**
+	 * @return What was this run was create to execute.
+	 */
+	Workflow getWorkflow();
+
+	/**
+	 * @return The name of the run.
+	 */
+	String getName();
+
+	/**
+	 * @param name
+	 *            The new name of the run. May be truncated.
+	 */
+	void setName(String name);
+
+	/**
+	 * @return The name of the Baclava file to use for all inputs, or
+	 *         <tt>null</tt> if no Baclava file is set.
+	 */
+	String getInputBaclavaFile();
+
+	/**
+	 * Sets the Baclava file to use for all inputs. This overrides the use of
+	 * individual inputs.
+	 * 
+	 * @param filename
+	 *            The filename to use. Must not start with a <tt>/</tt> or
+	 *            contain any <tt>..</tt> segments. Will be interpreted relative
+	 *            to the run's working directory.
+	 * @throws FilesystemAccessException
+	 *             If the filename is invalid.
+	 * @throws BadStateChangeException
+	 *             If the workflow is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	void setInputBaclavaFile(String filename) throws FilesystemAccessException,
+			BadStateChangeException;
+
+	/**
+	 * @return The list of input assignments.
+	 */
+	List<Input> getInputs();
+
+	/**
+	 * Create an input assignment.
+	 * 
+	 * @param name
+	 *            The name of the port that this will be an input for.
+	 * @return The assignment reference.
+	 * @throws BadStateChangeException
+	 *             If the workflow is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	Input makeInput(String name) throws BadStateChangeException;
+
+	/**
+	 * @return The file (relative to the working directory) to write the outputs
+	 *         of the run to as a Baclava document, or <tt>null</tt> if they are
+	 *         to be written to non-Baclava files in a directory called
+	 *         <tt>out</tt>.
+	 */
+	String getOutputBaclavaFile();
+
+	/**
+	 * Sets where the output of the run is to be written to. This will cause the
+	 * output to be generated as a Baclava document, rather than a collection of
+	 * individual non-Baclava files in the subdirectory of the working directory
+	 * called <tt>out</tt>.
+	 * 
+	 * @param filename
+	 *            Where to write the Baclava file (or <tt>null</tt> to cause the
+	 *            output to be written to individual files); overwrites any
+	 *            previous setting of this value.
+	 * @throws FilesystemAccessException
+	 *             If the filename starts with a <tt>/</tt> or contains a
+	 *             <tt>..</tt> segment.
+	 * @throws BadStateChangeException
+	 *             If the workflow is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 */
+	void setOutputBaclavaFile(String filename)
+			throws FilesystemAccessException, BadStateChangeException;
+
+	/**
+	 * @return When this run will expire, becoming eligible for automated
+	 *         deletion.
+	 */
+	Date getExpiry();
+
+	/**
+	 * Set when this run will expire.
+	 * 
+	 * @param d
+	 *            Expiry time. Deletion will happen some time after that.
+	 */
+	void setExpiry(Date d);
+
+	/**
+	 * @return The current status of the run.
+	 */
+	Status getStatus();
+
+	/**
+	 * Set the status of the run, which should cause it to move into the given
+	 * state. This may cause some significant changes.
+	 * 
+	 * @param s
+	 *            The state to try to change to.
+	 * @return <tt>null</tt>, or a string describing the incomplete state change
+	 *         if the operation has internally timed out.
+	 * @throws BadStateChangeException
+	 *             If the change to the given state is impossible.
+	 */
+	String setStatus(Status s) throws BadStateChangeException;
+
+	/**
+	 * @return Handle to the main working directory of the run.
+	 * @throws FilesystemAccessException
+	 */
+	Directory getWorkingDirectory() throws FilesystemAccessException;
+
+	/**
+	 * @return The list of listener instances attached to the run.
+	 */
+	List<Listener> getListeners();
+
+	/**
+	 * Add a listener to the run.
+	 * 
+	 * @param listener
+	 *            The listener to add.
+	 */
+	void addListener(Listener listener);
+
+	/**
+	 * @return The security context structure for this run.
+	 */
+	TavernaSecurityContext getSecurityContext();
+
+	/**
+	 * Kill off this run, removing all resources which it consumes.
+	 * 
+	 * @throws NoDestroyException
+	 *             If the destruction failed.
+	 */
+	void destroy() throws NoDestroyException;
+
+	/**
+	 * @return When this workflow run was created.
+	 */
+	Date getCreationTimestamp();
+
+	/**
+	 * @return When this workflow run was started, or <tt>null</tt> if it has
+	 *         never been started.
+	 */
+	Date getStartTimestamp();
+
+	/**
+	 * @return When this workflow run was found to have finished, or
+	 *         <tt>null</tt> if it has never finished (either still running or
+	 *         never started).
+	 */
+	Date getFinishTimestamp();
+
+	/**
+	 * Test if this run is really there.
+	 * 
+	 * <p>
+	 * <i>Implementation note:</i> Used to test communication fabrics, etc. so
+	 * implementations of this interface that do not delegate to another object
+	 * should do nothing.
+	 * 
+	 * @throws UnknownRunException
+	 *             If things fail.
+	 */
+	void ping() throws UnknownRunException;
+
+	/**
+	 * @return whether the run generates provenance data
+	 */
+	boolean getGenerateProvenance();
+
+	/**
+	 * @param generateProvenance
+	 *            whether the run generates provenance data
+	 */
+	void setGenerateProvenance(boolean generateProvenance);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/TavernaSecurityContext.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/TavernaSecurityContext.java
new file mode 100644
index 0000000..b227bfa
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/TavernaSecurityContext.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.Principal;
+import java.util.Set;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.xml.ws.handler.MessageContext;
+
+import org.springframework.security.core.context.SecurityContext;
+import org.taverna.server.localworker.remote.ImplementationException;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Security context for a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+public interface TavernaSecurityContext {
+	/**
+	 * @return Who owns the security context.
+	 */
+	UsernamePrincipal getOwner();
+
+	/**
+	 * Describe the names of the users (as extracted from their
+	 * {@link Principal} objects) that may destroy the run or manipulate its
+	 * lifetime.
+	 * 
+	 * @return The names of the users who may use destroy operations. Read-only.
+	 */
+	Set<String> getPermittedDestroyers();
+
+	/**
+	 * Sets the collection of names of users (as extracted from their
+	 * {@link Principal} objects) that may destroy the run or manipulate its
+	 * lifetime.
+	 * 
+	 * @param destroyers
+	 *            The names of the users who may use destroy operations.
+	 */
+	void setPermittedDestroyers(Set<String> destroyers);
+
+	/**
+	 * Describe the names of the users (as extracted from their
+	 * {@link Principal} objects) that may update the run (including writing to
+	 * files).
+	 * 
+	 * @return The names of the users who may use update operations. Read-only.
+	 */
+	Set<String> getPermittedUpdaters();
+
+	/**
+	 * Sets the collection of names of users (as extracted from their
+	 * {@link Principal} objects) that may update the run (including writing to
+	 * its files).
+	 * 
+	 * @param updaters
+	 *            The names of the users who may use update operations.
+	 */
+	void setPermittedUpdaters(Set<String> updaters);
+
+	/**
+	 * Describe the names of the users (as extracted from their
+	 * {@link Principal} objects) that may read from the run (including its
+	 * files).
+	 * 
+	 * @return The names of the users who may use read operations. Read-only.
+	 */
+	Set<String> getPermittedReaders();
+
+	/**
+	 * Sets the collection of names of users (as extracted from their
+	 * {@link Principal} objects) that may read from the run (including its
+	 * files).
+	 * 
+	 * @param readers
+	 *            The names of the users who may use read operations.
+	 */
+	void setPermittedReaders(Set<String> readers);
+
+	/**
+	 * @return The credentials owned by the user. Never <tt>null</tt>.
+	 */
+	Credential[] getCredentials();
+
+	/**
+	 * Add a credential to the owned set or replaces the old version with the
+	 * new one.
+	 * 
+	 * @param toAdd
+	 *            The credential to add.
+	 */
+	void addCredential(Credential toAdd);
+
+	/**
+	 * Remove a credential from the owned set. It's not a failure to remove
+	 * something that isn't in the set.
+	 * 
+	 * @param toDelete
+	 *            The credential to remove.
+	 */
+	void deleteCredential(Credential toDelete);
+
+	/**
+	 * Tests if the credential is valid. This includes testing whether the
+	 * underlying credential file exists and can be unlocked by the password in
+	 * the {@link Credential} object.
+	 * 
+	 * @param c
+	 *            The credential object to validate.
+	 * @throws InvalidCredentialException
+	 *             If it is invalid.
+	 */
+	void validateCredential(Credential c) throws InvalidCredentialException;
+
+	/**
+	 * @return The identities trusted by the user. Never <tt>null</tt>.
+	 */
+	Trust[] getTrusted();
+
+	/**
+	 * Add an identity to the trusted set.
+	 * 
+	 * @param toAdd
+	 *            The identity to add.
+	 */
+	void addTrusted(Trust toAdd);
+
+	/**
+	 * Remove an identity from the trusted set. It's not a failure to remove
+	 * something that isn't in the set.
+	 * 
+	 * @param toDelete
+	 *            The identity to remove.
+	 */
+	void deleteTrusted(Trust toDelete);
+
+	/**
+	 * Tests if the trusted identity descriptor is valid. This includes checking
+	 * whether the underlying trusted identity file exists.
+	 * 
+	 * @param t
+	 *            The trusted identity descriptor to check.
+	 * @throws InvalidCredentialException
+	 *             If it is invalid.
+	 */
+	void validateTrusted(Trust t) throws InvalidCredentialException;
+
+	/**
+	 * Establish the security context from how the owning workflow run was
+	 * created. In particular, this gives an opportunity for boot-strapping
+	 * things with any delegateable credentials.
+	 * 
+	 * @param securityContext
+	 *            The security context associated with the request that caused
+	 *            the workflow to be created.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	void initializeSecurityFromContext(SecurityContext securityContext)
+			throws Exception;
+
+	/**
+	 * Establish the security context from how the owning workflow run was
+	 * created. In particular, this gives an opportunity for boot-strapping
+	 * things with any delegateable credentials.
+	 * 
+	 * @param context
+	 *            The full information about the request that caused the
+	 *            workflow to be created.
+	 */
+	void initializeSecurityFromSOAPContext(MessageContext context);
+
+	/**
+	 * Establish the security context from how the owning workflow run was
+	 * created. In particular, this gives an opportunity for boot-strapping
+	 * things with any delegateable credentials.
+	 * 
+	 * @param headers
+	 *            The full information about the request that caused the
+	 *            workflow to be created.
+	 */
+	void initializeSecurityFromRESTContext(HttpHeaders headers);
+
+	/**
+	 * Transfer the security context to the remote system.
+	 * 
+	 * @throws IOException
+	 *             If the communication fails.
+	 * @throws GeneralSecurityException
+	 *             If the assembly of the context fails.
+	 * @throws ImplementationException
+	 *             If the local worker has problems with creating the realized
+	 *             security context.
+	 */
+	void conveySecurity() throws GeneralSecurityException, IOException,
+			ImplementationException;
+
+	/**
+	 * @return The factory that created this security context.
+	 */
+	SecurityContextFactory getFactory();
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/UriBuilderFactory.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/UriBuilderFactory.java
new file mode 100644
index 0000000..e5cd02c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/UriBuilderFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.interfaces;
+
+import java.net.URI;
+
+import javax.ws.rs.core.UriBuilder;
+
+/**
+ * How to manufacture URIs to workflow runs.
+ * 
+ * @author Donal Fellows
+ */
+public interface UriBuilderFactory {
+	/**
+	 * Given a run, get a factory for RESTful URIs to resources associated
+	 * with it.
+	 * 
+	 * @param run
+	 *            The run in question.
+	 * @return The {@link URI} factory.
+	 */
+	UriBuilder getRunUriBuilder(TavernaRun run);
+
+	/**
+	 * @return a URI factory that is preconfigured to point to the base of
+	 *         the webapp.
+	 */
+	UriBuilder getBaseUriBuilder();
+
+	/**
+	 * Resolves a URI with respect to the base URI of the factory.
+	 * 
+	 * @param uri
+	 *            The URI to resolve, or <tt>null</tt>.
+	 * @return The resolved URI, or <tt>null</tt> if <b>uri</b> is
+	 *         <tt>null</tt>.
+	 */
+	String resolve(String uri);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/interfaces/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/interfaces/package-info.java
new file mode 100644
index 0000000..cfbbe79
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/interfaces/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Interfaces to the main worker classes that provide the magical power
+ * that drives the webapp front-end.
+ */
+package org.taverna.server.master.interfaces;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/localworker/AbstractRemoteRunFactory.java b/server-webapp/src/main/java/org/taverna/server/master/localworker/AbstractRemoteRunFactory.java
new file mode 100644
index 0000000..fc7f881
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/localworker/AbstractRemoteRunFactory.java
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.localworker;
+
+import static java.lang.System.getSecurityManager;
+import static java.lang.System.setProperty;
+import static java.lang.System.setSecurityManager;
+import static java.rmi.registry.LocateRegistry.createRegistry;
+import static java.rmi.registry.LocateRegistry.getRegistry;
+import static java.rmi.registry.Registry.REGISTRY_PORT;
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.DIR;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.net.URI;
+import java.net.URL;
+import java.rmi.MarshalledObject;
+import java.rmi.RMISecurityManager;
+import java.rmi.RemoteException;
+import java.rmi.registry.LocateRegistry;
+import java.rmi.registry.Registry;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.Resource;
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.io.IOUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.taverna.server.localworker.remote.RemoteRunFactory;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.localworker.server.UsageRecordReceiver;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.factories.ListenerFactory;
+import org.taverna.server.master.factories.RunFactory;
+import org.taverna.server.master.interaction.InteractionFeedSupport;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.SecurityContextFactory;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.notification.atom.EventDAO;
+import org.taverna.server.master.usage.UsageRecordRecorder;
+import org.taverna.server.master.utils.UsernamePrincipal;
+import org.taverna.server.master.worker.FactoryBean;
+import org.taverna.server.master.worker.RemoteRunDelegate;
+import org.taverna.server.master.worker.RunFactoryConfiguration;
+
+import uk.org.taverna.scufl2.api.io.WriterException;
+
+/**
+ * Bridge to remote runs via RMI.
+ * 
+ * @author Donal Fellows
+ */
+@ManagedResource(objectName = JMX_ROOT + "Factory", description = "The factory for runs")
+public abstract class AbstractRemoteRunFactory extends RunFactoryConfiguration
+		implements ListenerFactory, RunFactory, FactoryBean {
+	/**
+	 * Whether to apply stronger limitations than normal to RMI. It is
+	 * recommended that this be true!
+	 */
+	@Value("${rmi.localhostOnly}")
+	private boolean rmiLocalhostOnly;
+	/** The interaction host name. */
+	private String interhost;
+	/** The interaction port number. */
+	private String interport;
+	private Process registryProcess;
+	/**
+	 * The interaction WebDAV location. Will be resolved before being passed to
+	 * the back-end.
+	 */
+	private String interwebdav;
+	/**
+	 * The interaction ATOM feed location. Will be resolved before being passed
+	 * to the back-end.
+	 */
+	private String interfeed;
+	/** Used for doing URI resolution. */
+	@Resource(name = "webapp")
+	private UriBuilderFactory baseurifactory;
+	@Autowired
+	private InteractionFeedSupport interactionFeedSupport;
+
+	@Value("${taverna.interaction.host}")
+	void setInteractionHost(String host) {
+		if (host != null && host.equals("none"))
+			host = null;
+		interhost = host;
+	}
+
+	@Value("${taverna.interaction.port}")
+	void setInteractionPort(String port) {
+		if (port != null && port.equals("none"))
+			port = null;
+		interport = port;
+	}
+
+	@Value("${taverna.interaction.webdav_path}")
+	void setInteractionWebdav(String webdav) {
+		if (webdav != null && webdav.equals("none"))
+			webdav = null;
+		interwebdav = webdav;
+	}
+
+	@Value("${taverna.interaction.feed_path}")
+	void setInteractionFeed(String feed) {
+		if (feed != null && feed.equals("none"))
+			feed = null;
+		interfeed = feed;
+	}
+
+	@Override
+	protected void reinitRegistry() {
+		registry = null;
+		if (registryProcess != null) {
+			registryProcess.destroy();
+			registryProcess = null;
+		}
+	}
+
+	protected void initInteractionDetails(RemoteRunFactory factory)
+			throws RemoteException {
+		if (interhost != null) {
+			String feed = baseurifactory.resolve(interfeed);
+			String webdav = baseurifactory.resolve(interwebdav);
+			factory.setInteractionServiceDetails(interhost, interport, webdav,
+					feed);
+		}
+	}
+
+	protected static final Process launchSubprocess(ProcessBuilder b)
+			throws IOException {
+		Thread t = Thread.currentThread();
+		ClassLoader ccl = t.getContextClassLoader();
+		try {
+			t.setContextClassLoader(null);
+			return b.start();
+		} finally {
+			t.setContextClassLoader(ccl);
+		}
+	}
+
+	/** Get a handle to a new instance of the RMI registry. */
+	private Registry makeRegistry(int port) throws RemoteException {
+		ProcessBuilder p = new ProcessBuilder(getJavaBinary());
+		p.command().add("-jar");
+		p.command().add(getRmiRegistryJar());
+		p.command().add(Integer.toString(port));
+		p.command().add(Boolean.toString(rmiLocalhostOnly));
+		try {
+			Process proc = launchSubprocess(p);
+			Thread.sleep(getSleepTime());
+			try {
+				if (proc.exitValue() == 0)
+					return null;
+				String error = IOUtils.toString(proc.getErrorStream());
+				throw new RemoteException(error);
+			} catch (IllegalThreadStateException ise) {
+				// Still running!
+			}
+			try (ObjectInputStream ois = new ObjectInputStream(
+					proc.getInputStream())) {
+				@SuppressWarnings("unchecked")
+				Registry r = ((MarshalledObject<Registry>) ois.readObject())
+						.get();
+				registryProcess = proc;
+				return r;
+			}
+		} catch (RemoteException e) {
+			throw e;
+		} catch (ClassNotFoundException e) {
+			throw new RemoteException("unexpected registry type", e);
+		} catch (IOException e) {
+			throw new RemoteException("unexpected IO problem with registry", e);
+		} catch (InterruptedException e) {
+			throw new RemoteException("unexpected interrupt");
+		}
+	}
+
+	/**
+	 * @return A handle to the current RMI registry.
+	 */
+	protected Registry getTheRegistry() {
+		try {
+			if (registry != null) {
+				registry.list();
+				return registry;
+			}
+		} catch (RemoteException e) {
+			log.warn("non-functioning existing registry handle", e);
+			registry = null;
+		}
+		try {
+			registry = getRegistry(getRegistryHost(), getRegistryPort());
+			registry.list();
+			return registry;
+		} catch (RemoteException e) {
+			log.warn("Failed to get working RMI registry handle.");
+			registry = null;
+			log.warn("Will build new registry, "
+					+ "but service restart ability is at risk.");
+			try {
+				registry = makeRegistry(getRegistryPort());
+				registry.list();
+				return registry;
+			} catch (RemoteException e2) {
+				log.error(
+						"failed to create local working RMI registry on port "
+								+ getRegistryPort(), e2);
+				log.info("original connection exception", e);
+			}
+		}
+		try {
+			registry = getRegistry(getRegistryHost(), REGISTRY_PORT);
+			registry.list();
+			return registry;
+		} catch (RemoteException e) {
+			log.warn("Failed to get working RMI registry handle on backup port.");
+			try {
+				registry = makeRegistry(REGISTRY_PORT);
+				registry.list();
+				return registry;
+			} catch (RemoteException e2) {
+				log.fatal(
+						"totally failed to get registry handle, even on fallback!",
+						e2);
+				log.info("original connection exception", e);
+				registry = null;
+				throw new RuntimeException("No RMI Registry Available");
+			}
+		}
+	}
+
+	private Registry registry;
+	/**
+	 * The name of the resource that describes the default security policy to
+	 * install.
+	 */
+	public static final String SECURITY_POLICY_FILE = "security.policy";
+	private SecurityContextFactory securityFactory;
+	UsageRecordRecorder usageRecordSink;
+	private EventDAO masterEventFeed;
+
+	@Autowired(required = true)
+	void setSecurityContextFactory(SecurityContextFactory factory) {
+		this.securityFactory = factory;
+	}
+
+	@Autowired(required = true)
+	void setMasterEventFeed(EventDAO masterEventFeed) {
+		this.masterEventFeed = masterEventFeed;
+	}
+
+	@Autowired(required = true)
+	void setUsageRecordSink(UsageRecordRecorder usageRecordSink) {
+		this.usageRecordSink = usageRecordSink;
+	}
+
+	/**
+	 * Configures the Java security model. Not currently used, as it is
+	 * viciously difficult to get right!
+	 */
+	@SuppressWarnings("unused")
+	private static void installSecurityManager() {
+		if (getSecurityManager() == null) {
+			setProperty("java.security.policy", AbstractRemoteRunFactory.class
+					.getClassLoader().getResource(SECURITY_POLICY_FILE)
+					.toExternalForm());
+			setSecurityManager(new RMISecurityManager());
+		}
+	}
+
+	// static {
+	// installSecurityManager();
+	// }
+
+	/**
+	 * Set up the run expiry management engine.
+	 * 
+	 * @throws JAXBException
+	 */
+	public AbstractRemoteRunFactory() throws JAXBException {
+		try {
+			registry = LocateRegistry.getRegistry();
+			registry.list();
+		} catch (RemoteException e) {
+			log.warn("Failed to get working RMI registry handle.");
+			log.warn("Will build new registry, but service restart ability is at risk.");
+			try {
+				registry = createRegistry(REGISTRY_PORT);
+				registry.list();
+			} catch (RemoteException e2) {
+				log.error("failed to create working RMI registry", e2);
+				log.info("original connection exception", e);
+			}
+		}
+	}
+
+	@Override
+	public List<String> getSupportedListenerTypes() {
+		try {
+			RemoteRunDelegate rrd = runDB.pickArbitraryRun();
+			if (rrd != null)
+				return rrd.getListenerTypes();
+			log.warn("no remote runs; no listener types");
+		} catch (Exception e) {
+			log.warn("failed to get list of listener types", e);
+		}
+		return new ArrayList<>();
+	}
+
+	@Override
+	public Listener makeListener(TavernaRun run, String listenerType,
+			String configuration) throws NoListenerException {
+		if (run instanceof RemoteRunDelegate)
+			return ((RemoteRunDelegate) run).makeListener(listenerType,
+					configuration);
+		throw new NoListenerException("unexpected run type: " + run.getClass());
+	}
+
+	@Override
+	public TavernaRun create(UsernamePrincipal creator, Workflow workflow)
+			throws NoCreateException {
+		try {
+			Date now = new Date();
+			UUID id = randomUUID();
+			RemoteSingleRun rsr = getRealRun(creator, workflow, id);
+			RemoteRunDelegate run = new RemoteRunDelegate(now, workflow, rsr,
+					state.getDefaultLifetime(), runDB, id,
+					state.getGenerateProvenance(), this);
+			run.setSecurityContext(securityFactory.create(run, creator));
+			@Nonnull
+			URI feed = interactionFeedSupport.getFeedURI(run);
+			@Nonnull
+			URL feedUrl = feed.toURL();
+			@Nonnull
+			URL webdavUrl = baseurifactory.getRunUriBuilder(run)
+					.path(DIR + "/interactions").build().toURL();
+			@Nullable
+			URL pub = interactionFeedSupport.getLocalFeedBase(feed);
+			rsr.setInteractionServiceDetails(feedUrl, webdavUrl, pub);
+			return run;
+		} catch (NoCreateException e) {
+			log.warn("failed to build run instance", e);
+			throw e;
+		} catch (Exception e) {
+			log.warn("failed to build run instance", e);
+			throw new NoCreateException("failed to build run instance", e);
+		}
+	}
+
+	/**
+	 * Gets the RMI connector for a new run.
+	 * 
+	 * @param creator
+	 *            Who is creating the workflow run.
+	 * @param workflow
+	 *            What workflow are they instantiating.
+	 * @param id
+	 *            The identity token for the run, newly minted.
+	 * @return The remote interface to the run.
+	 * @throws Exception
+	 *             Just about anything can go wrong...
+	 */
+	protected abstract RemoteSingleRun getRealRun(UsernamePrincipal creator,
+			Workflow workflow, UUID id) throws Exception;
+
+	/**
+	 * How to convert a wrapped workflow into XML.
+	 * 
+	 * @param workflow
+	 *            The wrapped workflow.
+	 * @return The XML version of the document.
+	 * @throws JAXBException
+	 *             If serialization fails.
+	 */
+	protected byte[] serializeWorkflow(Workflow workflow) throws JAXBException {
+		try {
+			return workflow.getScufl2Bytes();
+		} catch (IOException e) {
+			throw new JAXBException("problem converting to scufl2", e);
+		} catch (WriterException e) {
+			throw new JAXBException("problem converting to scufl2", e);
+		}
+	}
+
+	private void acceptUsageRecord(String usageRecord) {
+		if (usageRecordSink != null)
+			usageRecordSink.storeUsageRecord(usageRecord);
+		runDB.checkForFinishNow();
+	}
+
+	/**
+	 * Make a Remote object that can act as a consumer for usage records.
+	 * 
+	 * @param creator
+	 * 
+	 * @return The receiver, or <tt>null</tt> if the construction fails.
+	 */
+	protected UsageRecordReceiver makeURReciver(UsernamePrincipal creator) {
+		try {
+			@SuppressWarnings("serial")
+			class URReceiver extends UnicastRemoteObject implements
+					UsageRecordReceiver {
+				public URReceiver() throws RemoteException {
+					super();
+				}
+
+				@Override
+				public void acceptUsageRecord(String usageRecord) {
+					AbstractRemoteRunFactory.this.acceptUsageRecord(usageRecord);
+				}
+			}
+			return new URReceiver();
+		} catch (RemoteException e) {
+			log.warn("failed to build usage record receiver", e);
+			return null;
+		}
+	}
+
+	@Override
+	public EventDAO getMasterEventFeed() {
+		return masterEventFeed;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/localworker/ForkRunFactory.java b/server-webapp/src/main/java/org/taverna/server/master/localworker/ForkRunFactory.java
new file mode 100644
index 0000000..b67e121
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/localworker/ForkRunFactory.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.localworker;
+
+import static java.lang.System.getProperty;
+import static java.lang.Thread.sleep;
+import static java.util.Arrays.asList;
+import static java.util.Calendar.SECOND;
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+
+import java.io.File;
+import java.rmi.ConnectException;
+import java.rmi.ConnectIOException;
+import java.rmi.NotBoundException;
+import java.rmi.RemoteException;
+import java.util.Calendar;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.xml.bind.JAXBException;
+
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.taverna.server.localworker.remote.RemoteRunFactory;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.factories.ConfigurableRunFactory;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * A simple factory for workflow runs that forks runs from a subprocess.
+ * 
+ * @author Donal Fellows
+ */
+@ManagedResource(objectName = JMX_ROOT + "RunFactory", description = "The factory for simple singleton forked run.")
+public class ForkRunFactory extends AbstractRemoteRunFactory implements
+		ConfigurableRunFactory {
+	private int lastStartupCheckCount;
+	private Integer lastExitCode;
+	private RemoteRunFactory factory;
+	private Process factoryProcess;
+	private String factoryProcessName;
+
+	/**
+	 * Create a factory for remote runs that works by forking off a subprocess.
+	 * 
+	 * @throws JAXBException
+	 *             Shouldn't happen.
+	 */
+	public ForkRunFactory() throws JAXBException {
+	}
+
+	@PostConstruct
+	protected void initRegistry() {
+		log.info("waiting for availability of default RMI registry");
+		getTheRegistry();
+	}
+
+	@Override
+	protected void reinitFactory() {
+		boolean makeFactory = factory != null;
+		killFactory();
+		try {
+			if (makeFactory)
+				initFactory();
+		} catch (Exception e) {
+			log.fatal("failed to make connection to remote run factory", e);
+		}
+	}
+
+	private RemoteRunFactory getFactory() throws RemoteException {
+		try {
+			initFactory();
+		} catch (RemoteException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new RemoteException("problem constructing factory", e);
+		}
+		return factory;
+	}
+
+	/**
+	 * @return How many checks were done for the worker process the last time a
+	 *         spawn was tried.
+	 */
+	@ManagedAttribute(description = "How many checks were done for the worker process the last time a spawn was tried.", currencyTimeLimit = 60)
+	@Override
+	public int getLastStartupCheckCount() {
+		return lastStartupCheckCount;
+	}
+
+	/**
+	 * @return What was the exit code from the last time the factory subprocess
+	 *         was killed?
+	 */
+	@ManagedAttribute(description = "What was the exit code from the last time the factory subprocess was killed?")
+	@Override
+	public Integer getLastExitCode() {
+		return lastExitCode;
+	}
+
+	/**
+	 * @return What the factory subprocess's main RMI interface is registered
+	 *         as.
+	 */
+	@ManagedAttribute(description = "What the factory subprocess's main RMI interface is registered as.", currencyTimeLimit = 60)
+	@Override
+	public String getFactoryProcessName() {
+		return factoryProcessName;
+	}
+
+	/**
+	 * Makes the subprocess that manufactures runs.
+	 * 
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	public void initFactory() throws Exception {
+		if (factory != null)
+			return;
+		// Generate the arguments to use when spawning the subprocess
+		factoryProcessName = state.getFactoryProcessNamePrefix() + randomUUID();
+		ProcessBuilder p = new ProcessBuilder(getJavaBinary());
+		p.command().add("-jar");
+		p.command().add(getServerWorkerJar());
+		if (getExecuteWorkflowScript() == null)
+			log.fatal("no execute workflow script");
+		p.command().add(getExecuteWorkflowScript());
+		p.command().addAll(asList(getExtraArguments()));
+		p.command().add(factoryProcessName);
+		p.redirectErrorStream(true);
+		p.directory(new File(getProperty("javax.servlet.context.tempdir",
+				getProperty("java.io.tmpdir"))));
+
+		// Spawn the subprocess
+		log.info("about to create subprocess: " + p.command());
+		
+		factoryProcess = launchSubprocess(p);
+		outlog = new StreamLogger("FactoryStdout", factoryProcess.getInputStream()) {
+			@Override
+			protected void write(String msg) {
+				log.info(msg);
+			}
+		};
+		errlog = new StreamLogger("FactoryStderr", factoryProcess.getErrorStream()) {
+			@Override
+			protected void write(String msg) {
+				log.info(msg);
+			}
+		};
+
+		// Wait for the subprocess to register itself in the RMI registry
+		Calendar deadline = Calendar.getInstance();
+		deadline.add(SECOND, state.getWaitSeconds());
+		Exception lastException = null;
+		lastStartupCheckCount = 0;
+		while (deadline.after(Calendar.getInstance())) {
+			try {
+				sleep(state.getSleepMS());
+				lastStartupCheckCount++;
+				factory = getRemoteFactoryHandle(factoryProcessName);
+				initInteractionDetails(factory);
+				return;
+			} catch (InterruptedException ie) {
+				continue;
+			} catch (NotBoundException nbe) {
+				lastException = nbe;
+				log.info("resource \"" + factoryProcessName
+						+ "\" not yet registered...");
+				continue;
+			} catch (RemoteException re) {
+				// Unpack a remote exception if we can
+				lastException = re;
+				try {
+					if (re.getCause() != null)
+						lastException = (Exception) re.getCause();
+				} catch (Throwable t) {
+					// Ignore!
+				}
+			} catch (RuntimeException e) {
+				lastException = e;
+			}
+		}
+		if (lastException == null)
+			lastException = new InterruptedException();
+		throw lastException;
+	}
+
+	private StreamLogger outlog, errlog;
+
+	private void stopLoggers() {
+		if (outlog != null)
+			outlog.stop();
+		outlog = null;
+		if (errlog != null)
+			errlog.stop();
+		errlog = null;
+	}
+
+	private RemoteRunFactory getRemoteFactoryHandle(String name)
+			throws RemoteException, NotBoundException {
+		log.info("about to look up resource called " + name);
+		try {
+			// Validate registry connection first
+			getTheRegistry().list();
+		} catch (ConnectException | ConnectIOException e) {
+			log.warn("connection problems with registry", e);
+		}
+		RemoteRunFactory rrf = (RemoteRunFactory) getTheRegistry().lookup(name);
+		log.info("successfully connected to factory subprocess "
+				+ factoryProcessName);
+		return rrf;
+	}
+
+	/**
+	 * Destroys the subprocess that manufactures runs.
+	 */
+	@PreDestroy
+	public void killFactory() {
+		if (factory != null) {
+			log.info("requesting shutdown of " + factoryProcessName);
+			try {
+				factory.shutdown();
+				sleep(700);
+			} catch (RemoteException e) {
+				log.warn(factoryProcessName + " failed to shut down nicely", e);
+			} catch (InterruptedException e) {
+				if (log.isDebugEnabled())
+					log.debug("interrupted during wait after asking "
+							+ factoryProcessName + " to shut down", e);
+			} finally {
+				factory = null;
+			}
+		}
+
+		if (factoryProcess != null) {
+			int code = -1;
+			try {
+				lastExitCode = code = factoryProcess.exitValue();
+				log.info(factoryProcessName + " already dead?");
+			} catch (RuntimeException e) {
+				log.info("trying to force death of " + factoryProcessName);
+				try {
+					factoryProcess.destroy();
+					sleep(350); // takes a little time, even normally
+					lastExitCode = code = factoryProcess.exitValue();
+				} catch (Exception e2) {
+					code = -1;
+				}
+			} finally {
+				factoryProcess = null;
+				stopLoggers();
+			}
+			if (code > 128) {
+				log.info(factoryProcessName + " died with signal="
+						+ (code - 128));
+			} else if (code >= 0) {
+				log.info(factoryProcessName + " process killed: code=" + code);
+			} else {
+				log.warn(factoryProcessName + " not yet dead");
+			}
+		}
+	}
+
+	/**
+	 * The real core of the run builder, factored out from its reliability
+	 * support.
+	 * 
+	 * @param creator
+	 *            Who created this workflow?
+	 * @param wf
+	 *            The serialized workflow.
+	 * @return The remote handle of the workflow run.
+	 * @throws RemoteException
+	 *             If anything fails (communications error, etc.)
+	 */
+	private RemoteSingleRun getRealRun(@Nonnull UsernamePrincipal creator,
+			@Nonnull byte[] wf, UUID id) throws RemoteException {
+		@Nonnull
+		String globaluser = "Unknown Person";
+		if (creator != null)
+			globaluser = creator.getName();
+		RemoteSingleRun rsr = getFactory().make(wf, globaluser,
+				makeURReciver(creator), id);
+		incrementRunCount();
+		return rsr;
+	}
+
+	@Override
+	protected RemoteSingleRun getRealRun(UsernamePrincipal creator,
+			Workflow workflow, UUID id) throws Exception {
+		@Nonnull
+		byte[] wf = serializeWorkflow(workflow);
+		for (int i = 0; i < 3; i++) {
+			initFactory();
+			try {
+				return getRealRun(creator, wf, id);
+			} catch (ConnectException | ConnectIOException e) {
+				// factory was lost; try to recreate
+			}
+			killFactory();
+		}
+		throw new NoCreateException("total failure to connect to factory "
+				+ factoryProcessName + "despite attempting restart");
+	}
+
+	@Override
+	public String[] getFactoryProcessMapping() {
+		return new String[0];
+	}
+
+	@Override
+	protected int operatingCount() throws Exception {
+		return getFactory().countOperatingRuns();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/localworker/IdAwareForkRunFactory.java b/server-webapp/src/main/java/org/taverna/server/master/localworker/IdAwareForkRunFactory.java
new file mode 100644
index 0000000..e449373
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/localworker/IdAwareForkRunFactory.java
@@ -0,0 +1,516 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.localworker;
+
+import static java.lang.System.getProperty;
+import static java.lang.Thread.sleep;
+import static java.util.Arrays.asList;
+import static java.util.Calendar.SECOND;
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+import static org.taverna.server.master.localworker.AbstractRemoteRunFactory.launchSubprocess;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.rmi.ConnectException;
+import java.rmi.ConnectIOException;
+import java.rmi.NotBoundException;
+import java.rmi.RemoteException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.logging.Log;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.annotation.Order;
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.taverna.server.localworker.remote.RemoteRunFactory;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.factories.ConfigurableRunFactory;
+import org.taverna.server.master.interfaces.LocalIdentityMapper;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * A simple factory for workflow runs that forks runs from a subprocess.
+ * 
+ * @author Donal Fellows
+ */
+@ManagedResource(objectName = JMX_ROOT + "RunFactory", description = "The factory for a user-specific forked run.")
+public class IdAwareForkRunFactory extends AbstractRemoteRunFactory implements
+		ConfigurableRunFactory {
+	private MetaFactory forker;
+	private Map<String, RemoteRunFactory> factory;
+	private Map<String, String> factoryProcessName;
+
+	/**
+	 * Create a factory for remote runs that works by forking off a subprocess.
+	 * 
+	 * @throws JAXBException
+	 *             Shouldn't happen.
+	 */
+	public IdAwareForkRunFactory() throws JAXBException {
+		factory = new HashMap<>();
+		factoryProcessName = new HashMap<>();
+	}
+
+	@Override
+	protected void reinitFactory() {
+		boolean makeForker = forker != null;
+		try {
+			killForker();
+		} catch (Exception e) {
+			log.warn("exception when killing secure-fork process", e);
+		}
+		try {
+			if (makeForker)
+				initMetaFactory();
+		} catch (Exception e) {
+			log.fatal("failed to make secure-fork process", e);
+		}
+	}
+
+	/**
+	 * @return How many checks were done for the worker process the last time a
+	 *         spawn was tried.
+	 */
+	@Override
+	@ManagedAttribute(description = "How many checks were done for the worker process the last time a spawn was tried.", currencyTimeLimit = 60)
+	public int getLastStartupCheckCount() {
+		return forker == null ? 0 : forker.lastStartupCheckCount();
+	}
+
+	/**
+	 * @return What was the exit code from the last time the factory subprocess
+	 *         was killed?
+	 */
+	@Override
+	@ManagedAttribute(description = "What was the exit code from the last time the factory subprocess was killed?")
+	public Integer getLastExitCode() {
+		return forker == null ? null : forker.lastExitCode();
+	}
+
+	/**
+	 * @return The mapping of user names to RMI factory IDs.
+	 */
+	@Override
+	@ManagedAttribute(description = "The mapping of user names to RMI factory IDs.", currencyTimeLimit = 60)
+	public String[] getFactoryProcessMapping() {
+		ArrayList<String> result = new ArrayList<>();
+		ArrayList<String> keys = new ArrayList<>(factoryProcessName.keySet());
+		String[] ks = keys.toArray(new String[keys.size()]);
+		Arrays.sort(ks);
+		for (String k : ks) {
+			result.add(k);
+			result.add(factoryProcessName.get(k));
+		}
+		return result.toArray(new String[result.size()]);
+	}
+
+	/**
+	 * How construction of factories is actually done.
+	 * 
+	 * @author Donal Fellows
+	 */
+	public interface MetaFactory {
+		/**
+		 * Make a factory for the given user.
+		 * 
+		 * @param username
+		 *            Who to make it for.
+		 * @return Handle of the factory.
+		 * @throws Exception
+		 *             If anything goes wrong.
+		 */
+		RemoteRunFactory make(String username) throws Exception;
+
+		/**
+		 * Shut down the meta-factory. It is not defined whether factories
+		 * created by it are also shut down at the same time.
+		 * 
+		 * @throws IOException
+		 *             If something goes wrong when communicating with the
+		 *             meta-factory.
+		 * @throws InterruptedException
+		 *             If something stops us waiting for the shut down to
+		 *             happen.
+		 */
+		void close() throws IOException, InterruptedException;
+
+		int lastStartupCheckCount();
+
+		Integer lastExitCode();
+	}
+
+	void registerFactory(String username, String fpn, RemoteRunFactory f) {
+		factoryProcessName.put(username, fpn);
+		factory.put(username, f);
+	}
+
+	/**
+	 * Makes the connection to the meta-factory that makes factories.
+	 * 
+	 * @throws IOException
+	 *             If the connection fails.
+	 */
+	@PostConstruct
+	void initMetaFactory() throws IOException {
+		log.info("waiting for availability of default RMI registry");
+		getTheRegistry();
+		log.info("constructing secure fork subprocess");
+		forker = new SecureFork(this, state, log);
+	}
+
+	private void killForker() throws IOException, InterruptedException {
+		try {
+			if (forker != null)
+				forker.close();
+		} finally {
+			forker = null;
+		}
+	}
+
+	/**
+	 * Makes the subprocess that manufactures runs.
+	 * 
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	private void initFactory(String username) throws Exception {
+		if (factory.containsKey(username))
+			return;
+		if (forker == null)
+			initMetaFactory();
+		forker.make(username);
+	}
+
+	/**
+	 * Destroys the subprocess that manufactures runs.
+	 */
+	@PreDestroy
+	public void killFactories() {
+		if (!factory.isEmpty()) {
+			Iterator<String> keys = factory.keySet().iterator();
+			while (keys.hasNext()) {
+				String key = keys.next();
+				log.info("requesting shutdown of "
+						+ factoryProcessName.get(key));
+				try {
+					factory.get(key).shutdown();
+				} catch (RemoteException e) {
+					log.warn(factoryProcessName.get(key)
+							+ " failed to shut down nicely", e);
+				} finally {
+					keys.remove();
+					factoryProcessName.remove(key);
+				}
+			}
+			try {
+				sleep(700);
+			} catch (InterruptedException e) {
+				if (log.isDebugEnabled())
+					log.debug("interrupted during wait after "
+							+ "asking factories to shut down", e);
+			}
+		}
+
+		try {
+			killForker();
+		} catch (Exception e) {
+			if (log.isDebugEnabled())
+				log.debug("exception in shutdown of secure-fork process", e);
+		}
+	}
+
+	@Override
+	protected void finalize() throws Throwable {
+		killFactories();
+		super.finalize();
+	}
+
+	@Autowired
+	public void setIdMapper(LocalIdentityMapper mapper) {
+		this.mapper = mapper;
+	}
+
+	private LocalIdentityMapper mapper;
+
+	/**
+	 * The real core of the run builder, factored out from its reliability
+	 * support.
+	 * 
+	 * @param creator
+	 *            Who created this workflow?
+	 * @param username
+	 *            What user account is this workflow to be executed in?
+	 * @param wf
+	 *            The serialized workflow.
+	 * @return The remote handle of the workflow run.
+	 * @throws RemoteException
+	 *             If anything fails (communications error, etc.)
+	 */
+	private RemoteSingleRun getRealRun(@Nonnull UsernamePrincipal creator,
+			@Nonnull String username, @Nonnull byte[] wf, UUID id)
+			throws RemoteException {
+		String globaluser = "Unknown Person";
+		if (creator != null)
+			globaluser = creator.getName();
+		RemoteSingleRun rsr = factory.get(username).make(wf, globaluser,
+				makeURReciver(creator), id);
+		incrementRunCount();
+		return rsr;
+	}
+
+	@Override
+	protected RemoteSingleRun getRealRun(UsernamePrincipal creator,
+			Workflow workflow, UUID id) throws Exception {
+		byte[] wf = serializeWorkflow(workflow);
+		String username = mapper == null ? null : mapper
+				.getUsernameForPrincipal(creator);
+		if (username == null)
+			throw new Exception("cannot determine who to run workflow as; "
+					+ "local identity mapper returned null");
+		for (int i = 0; i < 3; i++) {
+			if (!factory.containsKey(username))
+				initFactory(username);
+			try {
+				return getRealRun(creator, username, wf, id);
+			} catch (ConnectException | ConnectIOException e) {
+				// factory was lost; try to recreate
+			}
+			factory.remove(username);
+		}
+		throw new NoCreateException("total failure to connect to factory "
+				+ factoryProcessName + "despite attempting restart");
+	}
+
+	@Value("${secureForkPasswordFile}")
+	@Order(20)
+	public void setPasswordSource(String passwordSource) {
+		if (passwordSource == null || passwordSource.isEmpty()
+				|| passwordSource.startsWith("${"))
+			state.setDefaultPasswordFile(null);
+		else
+			state.setDefaultPasswordFile(passwordSource);
+		if (state.getPasswordFile() == null)
+			log.info("assuming password-free forking enabled");
+		else
+			log.info("configured secureForkPasswordFile from context as "
+					+ state.getPasswordFile());
+	}
+
+	@Override
+	public String getFactoryProcessName() {
+		return "<PROPERTY-NOT-SUPPORTED>";
+	}
+
+	@Override
+	protected int operatingCount() throws Exception {
+		int total = 0;
+		for (RemoteRunFactory rrf : factory.values())
+			total += rrf.countOperatingRuns();
+		return total;
+	}
+}
+
+/**
+ * The connector that handles the secure fork process itself.
+ * 
+ * @author Donal Fellows
+ */
+class SecureFork implements IdAwareForkRunFactory.MetaFactory {
+	private IdAwareForkRunFactory main;
+	private Process process;
+	private PrintWriter channel;
+	private int lastStartupCheckCount;
+	private Integer lastExitCode;
+	private Log log;
+	private LocalWorkerState state;
+	private StreamLogger out, err;
+
+	/**
+	 * Construct the command to run the meta-factory process.
+	 * 
+	 * @param args
+	 *            The live list of arguments to pass.
+	 */
+	public void initFactoryArgs(List<String> args) {
+		args.add(main.getJavaBinary());
+		String pwf = main.getPasswordFile();
+		if (pwf != null) {
+			args.add("-Dpassword.file=" + pwf);
+		}
+		args.add("-jar");
+		args.add(main.getServerForkerJar());
+		args.add(main.getJavaBinary());
+		args.add("-jar");
+		args.add(main.getServerWorkerJar());
+		if (main.getExecuteWorkflowScript() == null)
+			log.fatal("no execute workflow script");
+		args.add(main.getExecuteWorkflowScript());
+		args.addAll(asList(main.getExtraArguments()));
+	}
+
+	SecureFork(IdAwareForkRunFactory main, LocalWorkerState state, Log log)
+			throws IOException {
+		this.main = main;
+		this.log = log;
+		this.state = state;
+		ProcessBuilder p = new ProcessBuilder();
+		initFactoryArgs(p.command());
+		p.redirectErrorStream(true);
+		p.directory(new File(getProperty("javax.servlet.context.tempdir",
+				getProperty("java.io.tmpdir"))));
+
+		// Spawn the subprocess
+		log.info("about to create subprocess: " + p.command());
+		log.info("subprocess directory: " + p.directory());
+		process = launchSubprocess(p);
+		channel = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
+				process.getOutputStream())), true);
+		// Log the responses
+		out = new StreamLogger("ForkedStdout", process.getInputStream()) {
+			@Override
+			protected void write(String msg) {
+				log.info(msg);
+			}
+		};
+		err = new StreamLogger("ForkedStderr", process.getErrorStream()) {
+			@Override
+			protected void write(String msg) {
+				log.info(msg);
+			}
+		};
+	}
+
+	@Override
+	public void close() throws IOException, InterruptedException {
+		try {
+			if (process != null) {
+				log.info("about to close down subprocess");
+				channel.close();
+				int code = -1;
+				try {
+					try {
+						code = process.exitValue();
+						log.info("secure-fork process already dead?");
+					} catch (IllegalThreadStateException e) {
+						try {
+							code = process.waitFor();
+						} catch (InterruptedException e1) {
+							log.info("interrupted waiting for natural death of secure-fork process?!");
+							process.destroy();
+							code = process.waitFor();
+						}
+					}
+				} finally {
+					lastExitCode = code;
+					if (code > 128) {
+						log.info("secure-fork process died with signal="
+								+ (code - 128));
+					} else if (code >= 0) {
+						log.info("secure-fork process killed: code=" + code);
+					} else {
+						log.warn("secure-fork process not yet dead");
+					}
+				}
+			}
+		} finally {
+			process = null;
+			channel = null;
+			out.stop();
+			err.stop();
+		}
+	}
+
+	protected void make(String username, String fpn) {
+		log.info("about to request subprocess creation for " + username
+				+ " producing ID " + fpn);
+		channel.println(username + " " + fpn);
+	}
+
+	@Override
+	public RemoteRunFactory make(String username) throws Exception {
+		try {
+			main.getTheRegistry().list(); // Validate registry connection first
+		} catch (ConnectException | ConnectIOException e) {
+			log.warn("connection problems with registry", e);
+		} catch (RemoteException e) {
+			if (e.getCause() != null && e.getCause() instanceof Exception) {
+				throw (Exception) e.getCause();
+			}
+			log.warn("connection problems with registry", e);
+		}
+
+		String fpn = state.getFactoryProcessNamePrefix() + randomUUID();
+		make(username, fpn);
+
+		// Wait for the subprocess to register itself in the RMI registry
+		Calendar deadline = Calendar.getInstance();
+		deadline.add(SECOND, state.getWaitSeconds());
+		Exception lastException = null;
+		lastStartupCheckCount = 0;
+		while (deadline.after(Calendar.getInstance())) {
+			try {
+				sleep(state.getSleepMS());
+				lastStartupCheckCount++;
+				log.info("about to look up resource called " + fpn);
+				RemoteRunFactory f = (RemoteRunFactory) main.getTheRegistry()
+						.lookup(fpn);
+				log.info("successfully connected to factory subprocess " + fpn);
+				main.initInteractionDetails(f);
+				main.registerFactory(username, fpn, f);
+				return f;
+			} catch (InterruptedException ie) {
+				continue;
+			} catch (NotBoundException nbe) {
+				lastException = nbe;
+				log.info("resource \"" + fpn + "\" not yet registered...");
+				continue;
+			} catch (RemoteException re) {
+				// Unpack a remote exception if we can
+				lastException = re;
+				try {
+					if (re.getCause() != null)
+						lastException = (Exception) re.getCause();
+				} catch (Throwable t) {
+					// Ignore!
+				}
+			} catch (Exception e) {
+				lastException = e;
+			}
+		}
+		if (lastException == null)
+			lastException = new InterruptedException();
+		throw lastException;
+	}
+
+	@Override
+	public Integer lastExitCode() {
+		return lastExitCode;
+	}
+
+	@Override
+	public int lastStartupCheckCount() {
+		return lastStartupCheckCount;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/localworker/LocalWorkerFactory.java b/server-webapp/src/main/java/org/taverna/server/master/localworker/LocalWorkerFactory.java
new file mode 100644
index 0000000..f890204
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/localworker/LocalWorkerFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.localworker;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Provider of the configuration of the "localworker.factory" bean, which is
+ * sufficiently complex to be too hard to manufacture directly from the XML
+ * configuration.
+ * 
+ * @author Donal Fellows
+ */
+@Configuration
+public class LocalWorkerFactory {
+	@Bean(name = "localworker.factory")
+	AbstractRemoteRunFactory getLocalworkerFactory(
+			@Value("${backEndFactory}") String mode) throws Exception {
+		if (mode == null || mode.isEmpty() || mode.startsWith("${"))
+			throw new Exception("no value for ${backEndFactory}");
+		Class<?> c = Class.forName(mode);
+		if (AbstractRemoteRunFactory.class.isAssignableFrom(c))
+			return (AbstractRemoteRunFactory) c.newInstance();
+		throw new Exception("unknown remote run factory: " + mode);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/localworker/LocalWorkerState.java b/server-webapp/src/main/java/org/taverna/server/master/localworker/LocalWorkerState.java
new file mode 100644
index 0000000..9f0f39b
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/localworker/LocalWorkerState.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.localworker;
+
+import static java.io.File.separator;
+import static java.lang.System.getProperty;
+import static java.rmi.registry.Registry.REGISTRY_PORT;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+import static org.taverna.server.master.defaults.Default.EXTRA_ARGUMENTS;
+import static org.taverna.server.master.defaults.Default.PASSWORD_FILE;
+import static org.taverna.server.master.defaults.Default.REGISTRY_JAR;
+import static org.taverna.server.master.defaults.Default.RMI_PREFIX;
+import static org.taverna.server.master.defaults.Default.RUN_LIFE_MINUTES;
+import static org.taverna.server.master.defaults.Default.RUN_OPERATING_LIMIT;
+import static org.taverna.server.master.defaults.Default.SECURE_FORK_IMPLEMENTATION_JAR;
+import static org.taverna.server.master.defaults.Default.SERVER_WORKER_IMPLEMENTATION_JAR;
+import static org.taverna.server.master.defaults.Default.SUBPROCESS_START_POLL_SLEEP;
+import static org.taverna.server.master.defaults.Default.SUBPROCESS_START_WAIT;
+import static org.taverna.server.master.localworker.PersistedState.KEY;
+import static org.taverna.server.master.localworker.PersistedState.makeInstance;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.net.URI;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.jdo.annotations.PersistenceAware;
+
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.defaults.Default;
+import org.taverna.server.master.utils.JDOSupport;
+import org.taverna.server.master.worker.WorkerModel;
+
+/**
+ * The persistent state of a local worker factory.
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceAware
+public class LocalWorkerState extends JDOSupport<PersistedState> implements
+		WorkerModel {
+	public LocalWorkerState() {
+		super(PersistedState.class);
+	}
+
+	private LocalWorkerState self;
+
+	@Required
+	public void setSelf(LocalWorkerState self) {
+		this.self = self;
+	}
+
+	/** Initial lifetime of runs, in minutes. */
+	int defaultLifetime;
+	/**
+	 * Maximum number of runs to exist at once. Note that this includes when
+	 * they are just existing for the purposes of file transfer (
+	 * {@link Status#Initialized}/{@link Status#Finished} states).
+	 */
+	int maxRuns;
+	/**
+	 * Prefix to use for RMI names.
+	 */
+	String factoryProcessNamePrefix;
+	/**
+	 * Full path name of the script used to start running a workflow; normally
+	 * expected to be "<i>somewhere/</i><tt>executeWorkflow.sh</tt>".
+	 */
+	String executeWorkflowScript;
+	/** Default value for {@link #executeWorkflowScript}. */
+	private transient String defaultExecuteWorkflowScript;
+	/**
+	 * Full path name of the file containing the password used to launch workers
+	 * as other users. The file is normally expected to contain a single line,
+	 * the password, and to be thoroughly locked down so only the user running
+	 * the server (e.g., "<tt>tomcat</tt>") can read it; it will probably reside
+	 * in either the user's home directory or in a system configuration
+	 * directory.
+	 */
+	String passwordFile;
+	/** Default value for {@link #passwordFile}. */
+	private transient String defaultPasswordFile = PASSWORD_FILE;
+	/**
+	 * The extra arguments to pass to the subprocess.
+	 */
+	String[] extraArgs;
+	/**
+	 * How long to wait for subprocess startup, in seconds.
+	 */
+	int waitSeconds;
+	/**
+	 * Polling interval to use during startup, in milliseconds.
+	 */
+	int sleepMS;
+	/**
+	 * Full path name to the worker process's implementation JAR.
+	 */
+	String serverWorkerJar;
+	private static final String DEFAULT_WORKER_JAR = LocalWorkerState.class
+			.getClassLoader().getResource(SERVER_WORKER_IMPLEMENTATION_JAR)
+			.getFile();
+	/**
+	 * Full path name to the Java binary to use to run the subprocess.
+	 */
+	String javaBinary;
+	private static final String DEFAULT_JAVA_BINARY = getProperty("java.home")
+			+ separator + "bin" + separator + "java";
+	/**
+	 * Full path name to the secure fork process's implementation JAR.
+	 */
+	String serverForkerJar;
+	private static final String DEFAULT_FORKER_JAR = LocalWorkerState.class
+			.getClassLoader().getResource(SECURE_FORK_IMPLEMENTATION_JAR)
+			.getFile();
+
+	String registryHost;
+	int registryPort;
+
+	int operatingLimit;
+
+	URI[] permittedWorkflows;
+	private String registryJar;
+	private static final String DEFAULT_REGISTRY_JAR = LocalWorkerState.class
+			.getClassLoader().getResource(REGISTRY_JAR).getFile();
+
+	@Override
+	public void setDefaultLifetime(int defaultLifetime) {
+		this.defaultLifetime = defaultLifetime;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public int getDefaultLifetime() {
+		return defaultLifetime < 1 ? RUN_LIFE_MINUTES : defaultLifetime;
+	}
+
+	@Override
+	public void setMaxRuns(int maxRuns) {
+		this.maxRuns = maxRuns;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public int getMaxRuns() {
+		return maxRuns < 1 ? Default.RUN_COUNT_MAX : maxRuns;
+	}
+
+	@Override
+	public int getOperatingLimit() {
+		return operatingLimit < 1 ? RUN_OPERATING_LIMIT : operatingLimit;
+	}
+
+	@Override
+	public void setOperatingLimit(int operatingLimit) {
+		this.operatingLimit = operatingLimit;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public void setFactoryProcessNamePrefix(String factoryProcessNamePrefix) {
+		this.factoryProcessNamePrefix = factoryProcessNamePrefix;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public String getFactoryProcessNamePrefix() {
+		return factoryProcessNamePrefix == null ? RMI_PREFIX
+				: factoryProcessNamePrefix;
+	}
+
+	@Override
+	public void setExecuteWorkflowScript(String executeWorkflowScript) {
+		this.executeWorkflowScript = executeWorkflowScript;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public String getExecuteWorkflowScript() {
+		return executeWorkflowScript == null ? defaultExecuteWorkflowScript
+				: executeWorkflowScript;
+	}
+
+	private static String guessWorkflowScript() {
+		File utilDir = new File(DEFAULT_WORKER_JAR).getParentFile();
+		File[] dirs = utilDir.listFiles(new FilenameFilter() {
+			@Override
+			public boolean accept(File dir, String name) {
+				return name.startsWith("taverna-commandline-");
+			}
+		});
+		assert dirs.length > 0;
+		return new File(dirs[0], "executeworkflow.sh").toString();
+	}
+
+	/**
+	 * Set what executeworkflow script to use by default. This is the value that
+	 * is used if not overridden by the administration interface.
+	 * 
+	 * @param defaultScript
+	 *            Full path to the script to use.
+	 */
+	public void setDefaultExecuteWorkflowScript(String defaultScript) {
+		if (defaultScript.startsWith("${") || defaultScript.equals("NONE")) {
+			this.defaultExecuteWorkflowScript = guessWorkflowScript();
+			return;
+		}
+		this.defaultExecuteWorkflowScript = defaultScript;
+	}
+
+	String getDefaultExecuteWorkflowScript() {
+		return defaultExecuteWorkflowScript;
+	}
+
+	@Override
+	public void setExtraArgs(String[] extraArgs) {
+		this.extraArgs = extraArgs.clone();
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public String[] getExtraArgs() {
+		return extraArgs == null ? EXTRA_ARGUMENTS : extraArgs.clone();
+	}
+
+	@Override
+	public void setWaitSeconds(int waitSeconds) {
+		this.waitSeconds = waitSeconds;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public int getWaitSeconds() {
+		return waitSeconds < 1 ? SUBPROCESS_START_WAIT : waitSeconds;
+	}
+
+	@Override
+	public void setSleepMS(int sleepMS) {
+		this.sleepMS = sleepMS;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public int getSleepMS() {
+		return sleepMS < 1 ? SUBPROCESS_START_POLL_SLEEP : sleepMS;
+	}
+
+	@Override
+	public void setServerWorkerJar(String serverWorkerJar) {
+		this.serverWorkerJar = serverWorkerJar;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public String getServerWorkerJar() {
+		return serverWorkerJar == null ? DEFAULT_WORKER_JAR : serverWorkerJar;
+	}
+
+	@Override
+	public void setServerForkerJar(String serverForkerJar) {
+		this.serverForkerJar = serverForkerJar;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public String getServerForkerJar() {
+		return serverForkerJar == null ? DEFAULT_FORKER_JAR : serverForkerJar;
+	}
+
+	@Override
+	public void setJavaBinary(String javaBinary) {
+		this.javaBinary = javaBinary;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public String getJavaBinary() {
+		return javaBinary == null ? DEFAULT_JAVA_BINARY : javaBinary;
+	}
+
+	@Override
+	public void setPasswordFile(String passwordFile) {
+		this.passwordFile = passwordFile;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public String getPasswordFile() {
+		return passwordFile == null ? defaultPasswordFile : passwordFile;
+	}
+
+	void setDefaultPasswordFile(String defaultPasswordFile) {
+		this.defaultPasswordFile = defaultPasswordFile;
+	}
+
+	@Override
+	public void setRegistryHost(String registryHost) {
+		this.registryHost = (registryHost == null ? "" : registryHost);
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public String getRegistryHost() {
+		return (registryHost == null || registryHost.isEmpty()) ? null
+				: registryHost;
+	}
+
+	@Override
+	public void setRegistryPort(int registryPort) {
+		this.registryPort = ((registryPort < 1 || registryPort > 65534) ? REGISTRY_PORT
+				: registryPort);
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public int getRegistryPort() {
+		return registryPort == 0 ? REGISTRY_PORT : registryPort;
+	}
+
+	@Override
+	public String getRegistryJar() {
+		return registryJar == null ? DEFAULT_REGISTRY_JAR : registryJar;
+	}
+
+	@Override
+	public void setRegistryJar(String rmiRegistryJar) {
+		this.registryJar = (rmiRegistryJar == null || rmiRegistryJar.isEmpty()) ? null
+				: rmiRegistryJar;
+		if (loadedState)
+			self.store();
+	}
+
+	@Override
+	public List<URI> getPermittedWorkflowURIs() {
+		if (permittedWorkflows == null || permittedWorkflows.length == 0)
+			return emptyList();
+		return unmodifiableList(asList(permittedWorkflows));
+	}
+
+	@Override
+	public void setPermittedWorkflowURIs(List<URI> permittedWorkflows) {
+		if (permittedWorkflows == null || permittedWorkflows.isEmpty())
+			this.permittedWorkflows = new URI[0];
+		else
+			this.permittedWorkflows = permittedWorkflows
+					.toArray(new URI[permittedWorkflows.size()]);
+		if (loadedState)
+			self.store();
+	}
+
+	public static final boolean DEFAULT_GENERATE_PROVENANCE = false;
+	private Boolean generateProvenance;
+
+	@Override
+	public boolean getGenerateProvenance() {
+		Boolean g = generateProvenance;
+		return g == null ? DEFAULT_GENERATE_PROVENANCE : (boolean) g;
+	}
+
+	@Override
+	public void setGenerateProvenance(boolean generate) {
+		this.generateProvenance = generate;
+		if (loadedState)
+			self.store();
+	}
+
+	// --------------------------------------------------------------
+
+	private boolean loadedState;
+
+	@PostConstruct
+	@WithinSingleTransaction
+	public void load() {
+		if (loadedState || !isPersistent())
+			return;
+		WorkerModel state = getById(KEY);
+		if (state == null) {
+			store();
+			return;
+		}
+
+		defaultLifetime = state.getDefaultLifetime();
+		executeWorkflowScript = state.getExecuteWorkflowScript();
+		extraArgs = state.getExtraArgs();
+		factoryProcessNamePrefix = state.getFactoryProcessNamePrefix();
+		javaBinary = state.getJavaBinary();
+		maxRuns = state.getMaxRuns();
+		serverWorkerJar = state.getServerWorkerJar();
+		serverForkerJar = state.getServerForkerJar();
+		passwordFile = state.getPasswordFile();
+		sleepMS = state.getSleepMS();
+		waitSeconds = state.getWaitSeconds();
+		registryHost = state.getRegistryHost();
+		registryPort = state.getRegistryPort();
+		operatingLimit = state.getOperatingLimit();
+		List<URI> pwu = state.getPermittedWorkflowURIs();
+		permittedWorkflows = (URI[]) pwu.toArray(new URI[pwu.size()]);
+		registryJar = state.getRegistryJar();
+		generateProvenance = state.getGenerateProvenance();
+
+		loadedState = true;
+	}
+
+	@WithinSingleTransaction
+	public void store() {
+		if (!isPersistent())
+			return;
+		WorkerModel state = getById(KEY);
+		if (state == null)
+			state = persist(makeInstance());
+
+		state.setDefaultLifetime(defaultLifetime);
+		state.setExecuteWorkflowScript(executeWorkflowScript);
+		state.setExtraArgs(extraArgs);
+		state.setFactoryProcessNamePrefix(factoryProcessNamePrefix);
+		state.setJavaBinary(javaBinary);
+		state.setMaxRuns(maxRuns);
+		state.setServerWorkerJar(serverWorkerJar);
+		state.setServerForkerJar(serverForkerJar);
+		state.setPasswordFile(passwordFile);
+		state.setSleepMS(sleepMS);
+		state.setWaitSeconds(waitSeconds);
+		state.setRegistryHost(registryHost);
+		state.setRegistryPort(registryPort);
+		state.setOperatingLimit(operatingLimit);
+		if (permittedWorkflows != null)
+			state.setPermittedWorkflowURIs(asList(permittedWorkflows));
+		state.setRegistryJar(registryJar);
+		if (generateProvenance != null)
+			state.setGenerateProvenance(generateProvenance);
+
+		loadedState = true;
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/localworker/PersistedState.java b/server-webapp/src/main/java/org/taverna/server/master/localworker/PersistedState.java
new file mode 100644
index 0000000..83d6bda
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/localworker/PersistedState.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.localworker;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jdo.annotations.Join;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.PrimaryKey;
+
+import org.taverna.server.master.worker.WorkerModel;
+
+/**
+ * The actual database connector for persisted local worker state.
+ * 
+ * @author Donal Fellows
+ */
+/*
+ * WARNING! If you change the name of this class, update persistence.xml as
+ * well!
+ */
+@PersistenceCapable(table = PersistedState.TABLE)
+class PersistedState implements WorkerModel {
+	static final String TABLE = "LOCALWORKERSTATE__PERSISTEDSTATE";
+
+	static PersistedState makeInstance() {
+		PersistedState o = new PersistedState();
+		o.ID = KEY;
+		return o;
+	}
+
+	@PrimaryKey(column = "ID")
+	protected int ID;
+
+	static final int KEY = 32;
+
+	@Persistent
+	private int defaultLifetime;
+	@Persistent
+	private int maxRuns;
+	@Persistent
+	private String factoryProcessNamePrefix;
+	@Persistent
+	private String executeWorkflowScript;
+	@Persistent(serialized = "true")
+	private String[] extraArgs;
+	@Persistent
+	private int waitSeconds;
+	@Persistent
+	private int sleepMS;
+	@Persistent
+	private String serverWorkerJar;
+	@Persistent
+	private String serverForkerJar;
+	@Persistent
+	private String registryJar;
+	@Persistent
+	private String passwordFile;
+	@Persistent
+	private String javaBinary;
+	@Persistent
+	private int registryPort;
+	@Persistent
+	private String registryHost;
+	@Persistent
+	private int operatingLimit;
+	@Persistent(defaultFetchGroup = "true")
+	@Join(table = TABLE + "_PERMWFURI", column = "ID")
+	private String[] permittedWorkflows;
+	@Persistent
+	private int generateProvenance;
+
+	@Override
+	public void setDefaultLifetime(int defaultLifetime) {
+		this.defaultLifetime = defaultLifetime;
+	}
+
+	@Override
+	public int getDefaultLifetime() {
+		return defaultLifetime;
+	}
+
+	@Override
+	public void setMaxRuns(int maxRuns) {
+		this.maxRuns = maxRuns;
+	}
+
+	@Override
+	public int getMaxRuns() {
+		return maxRuns;
+	}
+
+	@Override
+	public void setFactoryProcessNamePrefix(String factoryProcessNamePrefix) {
+		this.factoryProcessNamePrefix = factoryProcessNamePrefix;
+	}
+
+	@Override
+	public String getFactoryProcessNamePrefix() {
+		return factoryProcessNamePrefix;
+	}
+
+	@Override
+	public void setExecuteWorkflowScript(String executeWorkflowScript) {
+		this.executeWorkflowScript = executeWorkflowScript;
+	}
+
+	@Override
+	public String getExecuteWorkflowScript() {
+		return executeWorkflowScript;
+	}
+
+	@Override
+	public void setExtraArgs(String[] extraArgs) {
+		this.extraArgs = extraArgs;
+	}
+
+	@Override
+	public String[] getExtraArgs() {
+		return extraArgs;
+	}
+
+	@Override
+	public void setWaitSeconds(int waitSeconds) {
+		this.waitSeconds = waitSeconds;
+	}
+
+	@Override
+	public int getWaitSeconds() {
+		return waitSeconds;
+	}
+
+	@Override
+	public void setSleepMS(int sleepMS) {
+		this.sleepMS = sleepMS;
+	}
+
+	@Override
+	public int getSleepMS() {
+		return sleepMS;
+	}
+
+	@Override
+	public void setServerWorkerJar(String serverWorkerJar) {
+		this.serverWorkerJar = serverWorkerJar;
+	}
+
+	@Override
+	public String getServerWorkerJar() {
+		return serverWorkerJar;
+	}
+
+	@Override
+	public void setJavaBinary(String javaBinary) {
+		this.javaBinary = javaBinary;
+	}
+
+	@Override
+	public String getJavaBinary() {
+		return javaBinary;
+	}
+
+	@Override
+	public void setRegistryPort(int registryPort) {
+		this.registryPort = registryPort;
+	}
+
+	@Override
+	public int getRegistryPort() {
+		return registryPort;
+	}
+
+	@Override
+	public void setRegistryHost(String registryHost) {
+		this.registryHost = registryHost;
+	}
+
+	@Override
+	public String getRegistryHost() {
+		return registryHost;
+	}
+
+	@Override
+	public void setServerForkerJar(String serverForkerJar) {
+		this.serverForkerJar = serverForkerJar;
+	}
+
+	@Override
+	public String getServerForkerJar() {
+		return serverForkerJar;
+	}
+
+	@Override
+	public void setPasswordFile(String passwordFile) {
+		this.passwordFile = passwordFile;
+	}
+
+	@Override
+	public String getPasswordFile() {
+		return passwordFile;
+	}
+
+	@Override
+	public void setOperatingLimit(int operatingLimit) {
+		this.operatingLimit = operatingLimit;
+	}
+
+	@Override
+	public int getOperatingLimit() {
+		return operatingLimit;
+	}
+
+	@Override
+	public List<URI> getPermittedWorkflowURIs() {
+		String[] pw = this.permittedWorkflows;
+		if (pw == null)
+			return new ArrayList<>();
+		List<URI> uris = new ArrayList<>(pw.length);
+		for (String uri : pw)
+			uris.add(URI.create(uri));
+		return uris;
+	}
+
+	@Override
+	public void setPermittedWorkflowURIs(List<URI> permittedWorkflows) {
+		String[] pw = new String[permittedWorkflows.size()];
+		for (int i = 0; i < pw.length; i++)
+			pw[i] = permittedWorkflows.get(i).toString();
+		this.permittedWorkflows = pw;
+	}
+
+	@Override
+	public String getRegistryJar() {
+		return registryJar;
+	}
+
+	@Override
+	public void setRegistryJar(String registryJar) {
+		this.registryJar = registryJar;
+	}
+
+	@Override
+	public boolean getGenerateProvenance() {
+		return generateProvenance > 0;
+	}
+
+	@Override
+	public void setGenerateProvenance(boolean generateProvenance) {
+		this.generateProvenance = (generateProvenance ? 1 : 0);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/localworker/StreamLogger.java b/server-webapp/src/main/java/org/taverna/server/master/localworker/StreamLogger.java
new file mode 100644
index 0000000..f361e17
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/localworker/StreamLogger.java
@@ -0,0 +1,62 @@
+package org.taverna.server.master.localworker;
+
+import static java.lang.Thread.interrupted;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.apache.commons.logging.Log;
+
+abstract class StreamLogger {
+	protected final Log log;
+	private Thread t;
+	private InputStream in;
+
+	protected StreamLogger(final String name, InputStream is) {
+		log = getLog("Taverna.Server.LocalWorker." + name);
+		in = is;
+		t = new Thread(new Runnable() {
+			@Override
+			public void run() {
+				try (BufferedReader br = new BufferedReader(
+						new InputStreamReader(in))) {
+					String line;
+					while (!interrupted() && (line = br.readLine()) != null)
+						if (!line.isEmpty())
+							write(line);
+				} catch (IOException e) {
+					// Do nothing...
+				} catch (Exception e) {
+					log.warn("failure in reading from " + name, e);
+				}
+			}
+		}, name + ".StreamLogger");
+		t.setContextClassLoader(null);
+		t.setDaemon(true);
+		t.start();
+	}
+
+	/**
+	 * Write a line read from the subprocess to the log.
+	 * <p>
+	 * This needs to be implemented by subclasses in order for the log to be
+	 * correctly written with the class name.
+	 * 
+	 * @param msg
+	 *            The message to write. Guaranteed to have no newline characters
+	 *            in it and to be non-empty.
+	 */
+	protected abstract void write(String msg);
+
+	public void stop() {
+		log.info("trying to close down " + t.getName());
+		t.interrupt();
+		try {
+			in.close();
+		} catch (IOException e) {
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/localworker/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/localworker/package-info.java
new file mode 100644
index 0000000..7139dd7
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/localworker/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Implementation of a Taverna Server back-end that works by forking off
+ * workflow executors on the local system.
+ */
+package org.taverna.server.master.localworker;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/EmailDispatcher.java b/server-webapp/src/main/java/org/taverna/server/master/notification/EmailDispatcher.java
new file mode 100644
index 0000000..3e27806
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/EmailDispatcher.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+
+import javax.annotation.PostConstruct;
+
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.mail.MailSender;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+
+/**
+ * How to send a plain text message by email to someone.
+ * 
+ * @author Donal Fellows
+ */
+public class EmailDispatcher extends RateLimitedDispatcher {
+	@Override
+	public String getName() {
+		return "mailto";
+	}
+
+	/**
+	 * @param from
+	 *            Email address that the notification is to come from.
+	 */
+	@Required
+	public void setFrom(String from) {
+		this.from = valid(from, "");
+	}
+
+	/**
+	 * @param host
+	 *            The outgoing SMTP server address.
+	 */
+	@Required
+	public void setSmtpHost(String host) {
+		this.host = valid(host, "");
+	}
+
+	/**
+	 * @param contentType
+	 *            The content type of the message to be sent. For example, "
+	 *            <tt>text/plain</tt>".
+	 */
+	public void setMessageContentType(String contentType) {
+		this.contentType = contentType;
+	}
+
+	/**
+	 * @param sender
+	 *            the sender to set
+	 */
+	public void setSender(MailSender sender) {
+		this.sender = sender;
+	}
+
+	private String from;
+	private String host;
+	private MailSender sender;
+	@SuppressWarnings("unused")
+	private String contentType = TEXT_PLAIN;
+
+	/**
+	 * Try to perform the lookup of the email service. This is called during
+	 * configuration so that any failure happens at a useful, predictable time.
+	 */
+	@PostConstruct
+	public void tryLookup() {
+		if (!isAvailable()) {
+			log.warn("no mail support; disabling email dispatch");
+			sender = null;
+			return;
+		}
+		try {
+			if (sender instanceof JavaMailSender)
+				((JavaMailSender) sender).createMimeMessage();
+		} catch (Throwable t) {
+			log.warn("sender having problems constructing messages; "
+					+ "disabling...", t);
+			sender = null;
+		}
+	}
+
+	@Override
+	public void dispatch(String messageSubject, String messageContent, String to)
+			throws Exception {
+		// Simple checks for acceptability
+		if (!to.matches(".+@.+")) {
+			log.info("did not send email notification: improper email address \""
+					+ to + "\"");
+			return;
+		}
+
+		SimpleMailMessage message = new SimpleMailMessage();
+		message.setFrom(from);
+		message.setTo(to.trim());
+		message.setSubject(messageSubject);
+		message.setText(messageContent);
+		sender.send(message);
+	}
+
+	@Override
+	public boolean isAvailable() {
+		return (host != null && !host.isEmpty() && sender != null
+				&& from != null && !from.isEmpty());
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/JabberDispatcher.java b/server-webapp/src/main/java/org/taverna/server/master/notification/JabberDispatcher.java
new file mode 100644
index 0000000..61640bf
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/JabberDispatcher.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+
+package org.taverna.server.master.notification;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.jivesoftware.smack.Chat;
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.MessageListener;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.packet.Message;
+import org.taverna.server.master.interfaces.MessageDispatcher;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * Send notifications by Jabber/XMPP.
+ * 
+ * @author Donal Fellows
+ */
+public class JabberDispatcher implements MessageDispatcher {
+	@Override
+	public String getName() {
+		return "xmpp";
+	}
+
+	private Log log = LogFactory.getLog("Taverna.Server.Notification");
+	private XMPPConnection conn;
+	private String resource = "TavernaServer";
+	private String host = "";
+	private String user = "";
+	private String pass = "";
+
+	/**
+	 * @param resource
+	 *            The XMPP resource to use when connecting the server. This
+	 *            defaults to "<tt>TavernaServer</tt>".
+	 */
+	public void setResource(String resource) {
+		this.resource = resource;
+	}
+
+	/**
+	 * @param service
+	 *            The XMPP service URL.
+	 */
+	public void setHost(String service) {
+		if (service == null || service.trim().isEmpty()
+				|| service.trim().startsWith("$"))
+			this.host = "";
+		else
+			this.host = service.trim();
+	}
+
+	/**
+	 * @param user
+	 *            The user identity to use with the XMPP service.
+	 */
+	public void setUsername(String user) {
+		if (user == null || user.trim().isEmpty()
+				|| user.trim().startsWith("$"))
+			this.user = "";
+		else
+			this.user = user.trim();
+	}
+
+	/**
+	 * @param pass
+	 *            The password to use with the XMPP service.
+	 */
+	public void setPassword(String pass) {
+		if (pass == null || pass.trim().isEmpty()
+				|| pass.trim().startsWith("$"))
+			this.pass = "";
+		else
+			this.pass = pass.trim();
+	}
+
+	@PostConstruct
+	void setup() {
+		try {
+			if (host.isEmpty() || user.isEmpty() || pass.isEmpty()) {
+				log.info("disabling XMPP support; incomplete configuration");
+				conn = null;
+				return;
+			}
+			ConnectionConfiguration cfg = new ConnectionConfiguration(host);
+			cfg.setSendPresence(false);
+			XMPPConnection c = new XMPPConnection(cfg);
+			c.connect();
+			c.login(user, pass, resource);
+			conn = c;
+			log.info("connected to XMPP service <" + host + "> as user <"
+					+ user + ">");
+		} catch (Exception e) {
+			log.info("failed to connect to XMPP server", e);
+		}
+	}
+
+	@PreDestroy
+	public void close() {
+		if (conn != null)
+			conn.disconnect();
+		conn = null;
+	}
+
+	@Override
+	public boolean isAvailable() {
+		return conn != null;
+	}
+
+	@Override
+	public void dispatch(TavernaRun ignored, String messageSubject,
+			String messageContent, String targetParameter) throws Exception {
+		Chat chat = conn.getChatManager().createChat(targetParameter,
+				new DroppingListener());
+		Message m = new Message();
+		m.addBody(null, messageContent);
+		m.setSubject(messageSubject);
+		chat.sendMessage(m);
+	}
+
+	static class DroppingListener implements MessageListener {
+		private Log log = LogFactory
+				.getLog("Taverna.Server.Notification.Jabber");
+
+		@Override
+		public void processMessage(Chat chat, Message message) {
+			if (log.isDebugEnabled())
+				log.debug("unexpectedly received XMPP message from <"
+						+ message.getFrom() + ">; ignoring");
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/NotificationEngine.java b/server-webapp/src/main/java/org/taverna/server/master/notification/NotificationEngine.java
new file mode 100644
index 0000000..bc0f60d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/NotificationEngine.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.interfaces.MessageDispatcher;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * A common object for handling dispatch of event-driven messages.
+ * 
+ * @author Donal Fellows
+ */
+public class NotificationEngine {
+	private Log log = LogFactory.getLog("Taverna.Server.Notification");
+	private Map<String, MessageDispatcher> dispatchers;
+	private List<MessageDispatcher> universalDispatchers;
+
+	/**
+	 * @param dispatchers
+	 *            The various dispatchers we want to install.
+	 */
+	@Required
+	public void setDispatchers(List<MessageDispatcher> dispatchers) {
+		this.dispatchers = new HashMap<>();
+		for (MessageDispatcher d : dispatchers)
+			this.dispatchers.put(d.getName(), d);
+	}
+
+	/**
+	 * @param dispatcherList
+	 *            A list of dispatch objects to always dispatch to.
+	 */
+	@Required
+	public void setUniversalDispatchers(List<MessageDispatcher> dispatcherList) {
+		this.universalDispatchers = dispatcherList;
+	}
+
+	private void dispatchToChosenTarget(TavernaRun originator, String scheme,
+			String target, Message message) throws Exception {
+		try {
+			MessageDispatcher d = dispatchers.get(scheme);
+			if (d != null && d.isAvailable())
+				d.dispatch(originator, message.getTitle(scheme),
+						message.getContent(scheme), target);
+			else
+				log.warn("no such notification dispatcher for " + scheme);
+		} catch (URISyntaxException e) {
+			// See if *someone* will handle the message
+			Exception e2 = null;
+			for (MessageDispatcher d : dispatchers.values())
+				try {
+					if (d.isAvailable()) {
+						d.dispatch(originator, message.getTitle(d.getName()),
+								message.getContent(d.getName()), scheme + ":"
+										+ target);
+						return;
+					}
+				} catch (Exception ex) {
+					if (log.isDebugEnabled())
+						log.debug("failed in pseudo-directed dispatch of "
+								+ scheme + ":" + target, ex);
+					e2 = ex;
+				}
+			if (e2 != null)
+				throw e2;
+		}
+	}
+
+	private void dispatchUniversally(TavernaRun originator, Message message)
+			throws Exception {
+		for (MessageDispatcher d : universalDispatchers)
+			try {
+				if (d.isAvailable())
+					d.dispatch(originator, message.getTitle(d.getName()),
+							message.getContent(d.getName()), null);
+			} catch (Exception e) {
+				log.warn("problem in universal dispatcher", e);
+			}
+	}
+
+	/**
+	 * Dispatch a message over the notification fabric.
+	 * 
+	 * @param originator
+	 *            What workflow run was the source of this message?
+	 * @param destination
+	 *            Where the message should get delivered to. The correct format
+	 *            of this is either as a URI of some form (where the scheme
+	 *            determines the dispatcher) or as an invalid URI in which case
+	 *            it is just tried against the possibilities to see if any
+	 *            succeeds.
+	 * @param subject
+	 *            The subject line of the message.
+	 * @param message
+	 *            The plain text body of the message.
+	 * @throws Exception
+	 *             If anything goes wrong with the dispatch process.
+	 */
+	public void dispatchMessage(TavernaRun originator, String destination,
+			Message message) throws Exception {
+		if (destination != null && !destination.trim().isEmpty()) {
+			try {
+				URI toURI = new URI(destination.trim());
+				dispatchToChosenTarget(originator, toURI.getScheme(),
+						toURI.getSchemeSpecificPart(), message);
+			} catch (URISyntaxException e) {
+				// Ignore
+			}
+		}
+		dispatchUniversally(originator, message);
+	}
+
+	/**
+	 * @return The message dispatchers that are actually available (i.e., not
+	 *         disabled by configuration somewhere).
+	 */
+	public List<String> listAvailableDispatchers() {
+		ArrayList<String> result = new ArrayList<>();
+		for (Map.Entry<String, MessageDispatcher> entry : dispatchers
+				.entrySet()) {
+			if (entry.getValue().isAvailable())
+				result.add(entry.getKey());
+		}
+		return result;
+	}
+
+	public interface Message {
+		String getContent(String type);
+
+		String getTitle(String type);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/RateLimitedDispatcher.java b/server-webapp/src/main/java/org/taverna/server/master/notification/RateLimitedDispatcher.java
new file mode 100644
index 0000000..c8d7ef6
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/RateLimitedDispatcher.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.joda.time.DateTime;
+import org.taverna.server.master.interfaces.MessageDispatcher;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * Rate-limiting support. Some message fabrics simply should not be used to send
+ * a lot of messages.
+ * 
+ * @author Donal Fellows
+ */
+public abstract class RateLimitedDispatcher implements MessageDispatcher {
+	/** Pre-configured logger. */
+	protected Log log = LogFactory.getLog("Taverna.Server.Notification");
+	private int cooldownSeconds;
+	private Map<String, DateTime> lastSend = new HashMap<>();
+
+	String valid(String value, String def) {
+		if (value == null || value.trim().isEmpty()
+				|| value.trim().startsWith("${"))
+			return def;
+		else
+			return value.trim();
+	}
+
+	/**
+	 * Set how long must elapse between updates to the status of any particular
+	 * user. Calls before that time are just silently dropped.
+	 * 
+	 * @param cooldownSeconds
+	 *            Time to elapse, in seconds.
+	 */
+	public void setCooldownSeconds(int cooldownSeconds) {
+		this.cooldownSeconds = cooldownSeconds;
+	}
+
+	/**
+	 * Test whether the rate limiter allows the given user to send a message.
+	 * 
+	 * @param who
+	 *            Who wants to send the message?
+	 * @return <tt>true</tt> iff they are permitted.
+	 */
+	protected boolean isSendAllowed(String who) {
+		DateTime now = new DateTime();
+		synchronized (lastSend) {
+			DateTime last = lastSend.get(who);
+			if (last != null) {
+				if (!now.isAfter(last.plusSeconds(cooldownSeconds)))
+					return false;
+			}
+			lastSend.put(who, now);
+		}
+		return true;
+	}
+
+	@Override
+	public void dispatch(TavernaRun ignored, String messageSubject,
+			String messageContent, String target) throws Exception {
+		if (isSendAllowed(target))
+			dispatch(messageSubject, messageContent, target);
+	}
+
+	/**
+	 * Dispatch a message to a recipient that doesn't care what produced it.
+	 * 
+	 * @param messageSubject
+	 *            The subject of the message to send.
+	 * @param messageContent
+	 *            The plain-text content of the message to send.
+	 * @param target
+	 *            A description of where it is to go.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	public abstract void dispatch(String messageSubject, String messageContent,
+			String target) throws Exception;
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/SMSDispatcher.java b/server-webapp/src/main/java/org/taverna/server/master/notification/SMSDispatcher.java
new file mode 100644
index 0000000..5553141
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/SMSDispatcher.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import static org.taverna.server.master.defaults.Default.SMS_GATEWAY_URL;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.springframework.beans.factory.annotation.Required;
+
+/**
+ * Dispatch termination messages via SMS.
+ * 
+ * @author Donal Fellows
+ */
+public class SMSDispatcher extends RateLimitedDispatcher {
+	@Override
+	public String getName() {
+		return "sms";
+	}
+
+	private CloseableHttpClient client;
+	private URI service;
+	private String user = "", pass = "";
+	private String usernameField = "username", passwordField = "password",
+			destinationField = "to", messageField = "text";
+
+	/**
+	 * @param usernameField
+	 *            The name of the field that conveys the sending username; this
+	 *            is the <i>server</i>'s identity.
+	 */
+	@Required
+	public void setUsernameField(String usernameField) {
+		this.usernameField = usernameField;
+	}
+
+	/**
+	 * @param passwordField
+	 *            The field holding the password to authenticate the server to
+	 *            the SMS gateway.
+	 */
+	@Required
+	public void setPasswordField(String passwordField) {
+		this.passwordField = passwordField;
+	}
+
+	/**
+	 * @param destinationField
+	 *            The field holding the number to send the SMS to.
+	 */
+	@Required
+	public void setDestinationField(String destinationField) {
+		this.destinationField = destinationField;
+	}
+
+	/**
+	 * @param messageField
+	 *            The field holding the plain-text message to send.
+	 */
+	@Required
+	public void setMessageField(String messageField) {
+		this.messageField = messageField;
+	}
+
+	public void setService(String serviceURL) {
+		String s = valid(serviceURL, "");
+		if (s.isEmpty()) {
+			log.warn("did not get sms.service from servlet config; using default ("
+					+ SMS_GATEWAY_URL + ")");
+			s = SMS_GATEWAY_URL;
+		}
+		try {
+			service = new URI(s);
+		} catch (URISyntaxException e) {
+			service = null;
+		}
+	}
+
+	public void setUser(String user) {
+		this.user = valid(user, "");
+	}
+
+	public void setPassword(String pass) {
+		this.pass = valid(pass, "");
+	}
+
+	@PostConstruct
+	void init() {
+		client = HttpClientBuilder.create().build();
+	}
+
+	@PreDestroy
+	void close() throws IOException {
+		try {
+			if (client != null)
+				client.close();
+		} finally {
+			client = null;
+		}
+	}
+
+	@Override
+	public boolean isAvailable() {
+		return service != null && !user.isEmpty() && !pass.isEmpty();
+	}
+
+	@Override
+	public void dispatch(String messageSubject, String messageContent,
+			String targetParameter) throws Exception {
+		// Sanity check
+		if (!targetParameter.matches("[^0-9]+"))
+			throw new Exception("invalid phone number");
+
+		if (!isSendAllowed("anyone"))
+			return;
+
+		// Build the message to send
+		List<NameValuePair> params = new ArrayList<>();
+		params.add(new BasicNameValuePair(usernameField, user));
+		params.add(new BasicNameValuePair(passwordField, pass));
+		params.add(new BasicNameValuePair(destinationField, targetParameter));
+		params.add(new BasicNameValuePair(messageField, messageContent));
+
+		// Send the message
+		HttpPost post = new HttpPost(service);
+		post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
+		HttpResponse response = client.execute(post);
+
+		// Log the response
+		HttpEntity entity = response.getEntity();
+		if (entity != null)
+			try (BufferedReader e = new BufferedReader(new InputStreamReader(
+					entity.getContent()))) {
+				log.info(e.readLine());
+			}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/TwitterDispatcher.java b/server-webapp/src/main/java/org/taverna/server/master/notification/TwitterDispatcher.java
new file mode 100644
index 0000000..8ee4815
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/TwitterDispatcher.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification;
+
+import java.util.Properties;
+
+import twitter4j.Twitter;
+import twitter4j.TwitterFactory;
+import twitter4j.conf.Configuration;
+import twitter4j.conf.PropertyConfiguration;
+import twitter4j.auth.AuthorizationFactory;
+
+/**
+ * Super simple-minded twitter dispatcher. You need to tell it your consumer key
+ * and secret as part of the connection parameters, for example via a dispatcher
+ * URN of "<tt>twitter:fred:bloggs</tt>" where <tt>fred</tt> is the key and
+ * <tt>bloggs</tt> is the secret.
+ * 
+ * @author Donal Fellows
+ */
+public class TwitterDispatcher extends RateLimitedDispatcher {
+	@Override
+	public String getName() {
+		return "twitter";
+	}
+
+	public static final int MAX_MESSAGE_LENGTH = 140;
+	public static final char ELLIPSIS = '\u2026';
+
+	private String token = "";
+	private String secret = "";
+
+	public void setAccessToken(String token) {
+		this.token = valid(token, "");
+	}
+
+	public void setAccessSecret(String secret) {
+		this.secret = valid(secret, "");
+	}
+
+	private Properties getConfig() throws NotConfiguredException {
+		if (token.isEmpty() || secret.isEmpty())
+			throw new NotConfiguredException();
+		Properties p = new Properties();
+		p.setProperty(ACCESS_TOKEN_PROP, token);
+		p.setProperty(ACCESS_SECRET_PROP, secret);
+		return p;
+	}
+
+	public static final String ACCESS_TOKEN_PROP = "oauth.accessToken";
+	public static final String ACCESS_SECRET_PROP = "oauth.accessTokenSecret";
+
+	private Twitter getTwitter(String key, String secret) throws Exception {
+		if (key.isEmpty() || secret.isEmpty())
+			throw new NoCredentialsException();
+
+		Properties p = getConfig();
+		p.setProperty("oauth.consumerKey", key);
+		p.setProperty("oauth.consumerSecret", secret);
+
+		Configuration config = new PropertyConfiguration(p);
+		TwitterFactory factory = new TwitterFactory(config);
+		Twitter t = factory.getInstance(AuthorizationFactory
+				.getInstance(config));
+		// Verify that we can connect!
+		t.getOAuthAccessToken();
+		return t;
+	}
+
+	// TODO: Get secret from credential manager
+	@Override
+	public void dispatch(String messageSubject, String messageContent,
+			String targetParameter) throws Exception {
+		// messageSubject ignored
+		String[] target = targetParameter.split(":", 2);
+		if (target == null || target.length != 2)
+			throw new Exception("missing consumer key or secret");
+		String who = target[0];
+		if (!isSendAllowed(who))
+			return;
+		Twitter twitter = getTwitter(who, target[1]);
+
+		if (messageContent.length() > MAX_MESSAGE_LENGTH)
+			messageContent = messageContent
+					.substring(0, MAX_MESSAGE_LENGTH - 1) + ELLIPSIS;
+		twitter.updateStatus(messageContent);
+	}
+
+	@Override
+	public boolean isAvailable() {
+		try {
+			// Try to create the configuration and push it through as far as
+			// confirming that we can build an access object (even if it isn't
+			// bound to a user)
+			new TwitterFactory(new PropertyConfiguration(getConfig()))
+					.getInstance();
+			return true;
+		} catch (Exception e) {
+			return false;
+		}
+	}
+
+	/**
+	 * Indicates that the dispatcher has not been configured with service
+	 * credentials.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@SuppressWarnings("serial")
+	public static class NotConfiguredException extends Exception {
+		NotConfiguredException() {
+			super("not configured with xAuth key and secret; "
+					+ "dispatch not possible");
+		}
+	}
+
+	/**
+	 * Indicates that the user did not supply their credentials.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@SuppressWarnings("serial")
+	public static class NoCredentialsException extends Exception {
+		NoCredentialsException() {
+			super("no consumer key and secret present; "
+					+ "dispatch not possible");
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/atom/AtomFeed.java b/server-webapp/src/main/java/org/taverna/server/master/notification/atom/AtomFeed.java
new file mode 100644
index 0000000..e5beaeb
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/atom/AtomFeed.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification.atom;
+
+import static java.lang.String.format;
+import static java.util.UUID.randomUUID;
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.common.Uri.secure;
+
+import java.net.URI;
+import java.util.Date;
+
+import javax.annotation.security.RolesAllowed;
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.model.Feed;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.web.context.ServletContextAware;
+import org.taverna.server.master.TavernaServerSupport;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.rest.TavernaServerREST.EventFeed;
+import org.taverna.server.master.utils.InvocationCounter.CallCounted;
+
+/**
+ * Simple REST handler that allows an Atom feed to be served up of events
+ * generated by workflow runs.
+ * 
+ * @author Donal Fellows
+ */
+public class AtomFeed implements EventFeed, UriBuilderFactory,
+		ServletContextAware {
+	/**
+	 * The name of a parameter that states what address we should claim that the
+	 * feed's internally-generated URIs are relative to. If not set, a default
+	 * will be guessed.
+	 */
+	public static final String PREFERRED_URI_PARAM = "taverna.preferredUserUri";
+	private EventDAO eventSource;
+	private TavernaServerSupport support;
+	private URI baseURI;
+	private Abdera abdera;
+	private String feedLanguage = "en";
+	private String uuid = randomUUID().toString();
+
+	@Required
+	public void setEventSource(EventDAO eventSource) {
+		this.eventSource = eventSource;
+	}
+
+	@Required
+	public void setSupport(TavernaServerSupport support) {
+		this.support = support;
+	}
+
+	public void setFeedLanguage(String language) {
+		this.feedLanguage = language;
+	}
+
+	public String getFeedLanguage() {
+		return feedLanguage;
+	}
+
+	@Required
+	public void setAbdera(Abdera abdera) {
+		this.abdera = abdera;
+	}
+
+	@Override
+	@CallCounted
+	@RolesAllowed(USER)
+	public Feed getFeed(UriInfo ui) {
+		Feed feed = abdera.getFactory().newFeed();
+		feed.setTitle("events relating to workflow runs").setLanguage(
+				feedLanguage);
+		String user = support.getPrincipal().toString()
+				.replaceAll("[^A-Za-z0-9]+", "");
+		feed.setId(format("urn:taverna-server:%s:%s", uuid, user));
+		org.joda.time.DateTime modification = null;
+		for (Event e : eventSource.getEvents(support.getPrincipal())) {
+			if (modification == null || e.getPublished().isAfter(modification))
+				modification = e.getPublished();
+			feed.addEntry(e.getEntry(abdera, feedLanguage));
+		}
+		if (modification == null)
+			feed.setUpdated(new Date());
+		else
+			feed.setUpdated(modification.toDate());
+		feed.addLink(ui.getAbsolutePath().toASCIIString(), "self");
+		return feed;
+	}
+
+	@Override
+	@CallCounted
+	@RolesAllowed(USER)
+	public Entry getEvent(String id) {
+		return eventSource.getEvent(support.getPrincipal(), id).getEntry(
+				abdera, feedLanguage);
+	}
+
+	@Override
+	public UriBuilder getRunUriBuilder(TavernaRun run) {
+		return secure(fromUri(getBaseUriBuilder().path("runs/{uuid}").build(
+				run.getId())));
+	}
+
+	@Override
+	public UriBuilder getBaseUriBuilder() {
+		return secure(fromUri(baseURI));
+	}
+
+	@Override
+	public String resolve(String uri) {
+		if (uri == null)
+			return null;
+		return secure(baseURI, uri).toString();
+	}
+
+	@Override
+	public void setServletContext(ServletContext servletContext) {
+		String base = servletContext.getInitParameter(PREFERRED_URI_PARAM);
+		if (base == null)
+			base = servletContext.getContextPath() + "/rest";
+		baseURI = URI.create(base);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/atom/Event.java b/server-webapp/src/main/java/org/taverna/server/master/notification/atom/Event.java
new file mode 100644
index 0000000..825029d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/atom/Event.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification.atom;
+
+import static java.util.UUID.randomUUID;
+
+import java.io.Serializable;
+import java.net.URI;
+import java.util.Date;
+
+import javax.jdo.annotations.Column;
+import javax.jdo.annotations.Index;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.Queries;
+import javax.jdo.annotations.Query;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.model.Entry;
+import org.joda.time.DateTime;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Parent class of all events that may appear on the feed for a workflow run.
+ * 
+ * @author Donal Fellows
+ */
+@SuppressWarnings("serial")
+@PersistenceCapable(schema = "ATOM", table = "EVENTS")
+@Queries({
+		@Query(name = "eventsForUser", language = "SQL", value = "SELECT id FROM ATOM.EVENTS WHERE owner = ? ORDER BY published DESC", resultClass = String.class),
+		@Query(name = "eventForUserAndId", language = "SQL", value = "SELECT id FROM ATOM.EVENTS WHERE owner = ? AND id = ?", resultClass = String.class),
+		@Query(name = "eventsFromBefore", language = "SQL", value = "SELECT id FROM ATOM.EVENTS where published < ?", resultClass = String.class) })
+public class Event implements Serializable {
+	@Persistent(primaryKey = "true")
+	@Column(length = 48)
+	private String id;
+	@Persistent
+	private String owner;
+	@Persistent
+	@Index
+	private Date published;
+	@Persistent
+	private String message;
+	@Persistent
+	private String title;
+	@Persistent
+	private String link;
+
+	Event() {
+	}
+
+	/**
+	 * Initialise the identity of this event and the point at which it was
+	 * published.
+	 * 
+	 * @param idPrefix
+	 *            A prefix for the identity of this event.
+	 * @param owner
+	 *            Who is the owner of this event.
+	 */
+	Event(String idPrefix, URI workflowLink, UsernamePrincipal owner,
+			String title, String message) {
+		id = idPrefix + "." + randomUUID().toString();
+		published = new Date();
+		this.owner = owner.getName();
+		this.title = title;
+		this.message = message;
+		this.link = workflowLink.toASCIIString();
+	}
+
+	public final String getId() {
+		return id;
+	}
+
+	public final String getOwner() {
+		return owner;
+	}
+
+	public final DateTime getPublished() {
+		return new DateTime(published);
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public String getTitle() {
+		return title;
+	}
+
+	public String getLink() {
+		return link;
+	}
+
+	public Entry getEntry(Abdera abdera, String language) {
+		Entry entry = abdera.getFactory().newEntry();
+		entry.setId(id);
+		entry.setPublished(published);
+		entry.addAuthor(owner).setLanguage(language);
+		entry.setUpdated(published);
+		entry.setTitle(title).setLanguage(language);
+		entry.addLink(link, "related").setTitle("workflow run");
+		entry.setContent(message).setLanguage(language);
+		return entry;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/atom/EventDAO.java b/server-webapp/src/main/java/org/taverna/server/master/notification/atom/EventDAO.java
new file mode 100644
index 0000000..56f25ff
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/atom/EventDAO.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.notification.atom;
+
+import static java.lang.Thread.interrupted;
+import static java.lang.Thread.sleep;
+import static java.util.Arrays.asList;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PreDestroy;
+import javax.jdo.annotations.PersistenceAware;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.joda.time.DateTime;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.interfaces.MessageDispatcher;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.utils.JDOSupport;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * The database interface that supports the event feed.
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceAware
+public class EventDAO extends JDOSupport<Event> implements MessageDispatcher {
+	public EventDAO() {
+		super(Event.class);
+	}
+
+	@Override
+	public String getName() {
+		return "atom";
+	}
+
+	private Log log = LogFactory.getLog("Taverna.Server.Atom");
+	private UriBuilderFactory ubf;
+	private int expiryAgeDays;
+
+	@Required
+	public void setExpiryAgeDays(int expiryAgeDays) {
+		this.expiryAgeDays = expiryAgeDays;
+	}
+
+	@Required
+	public void setUriBuilderFactory(UriBuilderFactory ubf) {
+		this.ubf = ubf;
+	}
+
+	/**
+	 * Get the given user's list of events.
+	 * 
+	 * @param user
+	 *            The identity of the user to get the events for.
+	 * @return A copy of the list of events currently known about.
+	 */
+	@Nonnull
+	@WithinSingleTransaction
+	public List<Event> getEvents(@Nonnull UsernamePrincipal user) {
+		@SuppressWarnings("unchecked")
+		List<String> ids = (List<String>) namedQuery("eventsForUser").execute(
+				user.getName());
+		if (log.isDebugEnabled())
+			log.debug("found " + ids.size() + " events for user " + user);
+
+		List<Event> result = new ArrayList<>();
+		for (String id : ids) {
+			Event event = getById(id);
+			result.add(detach(event));
+		}
+		return result;
+	}
+
+	/**
+	 * Get a particular event.
+	 * 
+	 * @param user
+	 *            The identity of the user to get the event for.
+	 * @param id
+	 *            The handle of the event to look up.
+	 * @return A copy of the event.
+	 */
+	@Nonnull
+	@WithinSingleTransaction
+	public Event getEvent(@Nonnull UsernamePrincipal user, @Nonnull String id) {
+		@SuppressWarnings("unchecked")
+		List<String> ids = (List<String>) namedQuery("eventForUserAndId")
+				.execute(user.getName(), id);
+		if (log.isDebugEnabled())
+			log.debug("found " + ids.size() + " events for user " + user
+					+ " with id = " + id);
+
+		if (ids.size() != 1)
+			throw new IllegalArgumentException("no such id");
+		return detach(getById(ids.get(0)));
+	}
+
+	/**
+	 * Delete a particular event.
+	 * 
+	 * @param id
+	 *            The identifier of the event to delete.
+	 */
+	@WithinSingleTransaction
+	public void deleteEventById(@Nonnull String id) {
+		delete(getById(id));
+	}
+
+	/**
+	 * Delete all events that have expired.
+	 */
+	@WithinSingleTransaction
+	public void deleteExpiredEvents() {
+		Date death = new DateTime().plusDays(-expiryAgeDays).toDate();
+		death = new Timestamp(death.getTime()); // UGLY SQL HACK
+
+		@SuppressWarnings("unchecked")
+		List<String> ids = (List<String>) namedQuery("eventsFromBefore")
+				.execute(death);
+		if (log.isDebugEnabled() && !ids.isEmpty())
+			log.debug("found " + ids.size()
+					+ " events to be squelched (older than " + death + ")");
+
+		for (String id : ids)
+			delete(getById(id));
+	}
+
+	@Override
+	public boolean isAvailable() {
+		return true;
+	}
+
+	private BlockingQueue<Event> insertQueue = new ArrayBlockingQueue<>(16);
+
+	@Override
+	public void dispatch(TavernaRun originator, String messageSubject,
+			String messageContent, String targetParameter) throws Exception {
+		insertQueue.put(new Event("finish", ubf.getRunUriBuilder(originator)
+				.build(), originator.getSecurityContext().getOwner(),
+				messageSubject, messageContent));
+	}
+
+	public void started(TavernaRun originator, String messageSubject,
+			String messageContent) throws InterruptedException {
+		insertQueue.put(new Event("start", ubf.getRunUriBuilder(originator)
+				.build(), originator.getSecurityContext().getOwner(),
+				messageSubject, messageContent));
+	}
+
+	private Thread eventDaemon;
+	private boolean shuttingDown = false;
+
+	@Required
+	public void setSelf(final EventDAO dao) {
+		eventDaemon = new Thread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					while (!shuttingDown && !interrupted()) {
+						transferEvents(dao, new ArrayList<Event>(
+								asList(insertQueue.take())));
+						sleep(5000);
+					}
+				} catch (InterruptedException e) {
+				} finally {
+					transferEvents(dao, new ArrayList<Event>());
+				}
+			}
+		}, "ATOM event daemon");
+		eventDaemon.setContextClassLoader(null);
+		eventDaemon.setDaemon(true);
+		eventDaemon.start();
+	}
+
+	private void transferEvents(EventDAO dao, List<Event> e) {
+		insertQueue.drainTo(e);
+		dao.storeEvents(e);
+	}
+
+	@PreDestroy
+	void stopDaemon() {
+		shuttingDown = true;
+		if (eventDaemon != null)
+			eventDaemon.interrupt();
+	}
+
+	@WithinSingleTransaction
+	protected void storeEvents(List<Event> events) {
+		for (Event e : events)
+			persist(e);
+		log.info("stored " + events.size() + " notification events");
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/atom/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/notification/atom/package-info.java
new file mode 100644
index 0000000..9cc592d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/atom/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the Atom feed implementation within Taverna Server.
+ * @author Donal Fellows
+ */
+@XmlSchema(namespace = FEED, elementFormDefault = QUALIFIED, attributeFormDefault = QUALIFIED, xmlns = {
+		@XmlNs(prefix = "xlink", namespaceURI = XLINK),
+		@XmlNs(prefix = "ts", namespaceURI = SERVER),
+		@XmlNs(prefix = "ts-rest", namespaceURI = SERVER_REST),
+		@XmlNs(prefix = "ts-soap", namespaceURI = SERVER_SOAP),
+		@XmlNs(prefix = "feed", namespaceURI = FEED),
+		@XmlNs(prefix = "admin", namespaceURI = ADMIN) })
+package org.taverna.server.master.notification.atom;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.master.common.Namespaces.ADMIN;
+import static org.taverna.server.master.common.Namespaces.FEED;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.SERVER_REST;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+
diff --git a/server-webapp/src/main/java/org/taverna/server/master/notification/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/notification/package-info.java
new file mode 100644
index 0000000..979066a
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/notification/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * The notification fabric and implementations of notification dispatchers
+ * that support subscription.
+ */
+package org.taverna.server.master.notification;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/package-info.java
new file mode 100644
index 0000000..17bfd03
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/package-info.java
@@ -0,0 +1,11 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * The core of the implementation of Taverna Server, including the
+ * implementations of the SOAP and REST interfaces.
+ */
+package org.taverna.server.master;
+
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/ContentTypes.java b/server-webapp/src/main/java/org/taverna/server/master/rest/ContentTypes.java
new file mode 100644
index 0000000..b0819a5
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/ContentTypes.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_ATOM_XML;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
+import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+
+/**
+ * Miscellaneous content type constants.
+ * 
+ * @author Donal Fellows
+ */
+interface ContentTypes {
+	static final String URI_LIST = "text/uri-list";
+	static final String ZIP = "application/zip";
+	static final String TEXT = TEXT_PLAIN;
+	static final String XML = APPLICATION_XML;
+	static final String JSON = APPLICATION_JSON;
+	static final String BYTES = APPLICATION_OCTET_STREAM;
+	static final String ATOM = APPLICATION_ATOM_XML;
+	static final String ROBUNDLE = "application/vnd.wf4ever.robundle+zip";
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/DirectoryContents.java b/server-webapp/src/main/java/org/taverna/server/master/rest/DirectoryContents.java
new file mode 100644
index 0000000..e01d1c4
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/DirectoryContents.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static org.taverna.server.master.common.Uri.secure;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlElementRef;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+
+import org.taverna.server.master.common.DirEntryReference;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+
+/**
+ * The result of a RESTful operation to list the contents of a directory. Done
+ * with JAXB.
+ * 
+ * @author Donal Fellows
+ */
+@XmlRootElement
+@XmlType(name = "DirectoryContents")
+@XmlSeeAlso(MakeOrUpdateDirEntry.class)
+public class DirectoryContents {
+	/**
+	 * The contents of the directory.
+	 */
+	@XmlElementRef
+	public List<DirEntryReference> contents;
+
+	/**
+	 * Make an empty directory description. Required for JAXB.
+	 */
+	public DirectoryContents() {
+		contents = new ArrayList<>();
+	}
+
+	/**
+	 * Make a directory description.
+	 * 
+	 * @param ui
+	 *            The factory for URIs.
+	 * @param collection
+	 *            The real directory contents that we are to describe.
+	 */
+	public DirectoryContents(UriInfo ui, Collection<DirectoryEntry> collection) {
+		contents = new ArrayList<>();
+		UriBuilder ub = secure(ui).path("{filename}");
+		for (DirectoryEntry e : collection)
+			contents.add(DirEntryReference.newInstance(ub, e));
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/FileSegment.java b/server-webapp/src/main/java/org/taverna/server/master/rest/FileSegment.java
new file mode 100644
index 0000000..74269c1
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/FileSegment.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static javax.ws.rs.core.Response.ok;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+/**
+ * Representation of a segment of a file to be read by JAX-RS.
+ * 
+ * @author Donal Fellows
+ */
+public class FileSegment {
+	/** The file to read a segment of. */
+	public final File file;
+	/** The offset of the first byte of the segment to read. */
+	public Integer from;
+	/** The offset of the first byte after the segment to read. */
+	public Integer to;
+
+	/**
+	 * Parse the HTTP Range header and determine what exact range of the file to
+	 * read.
+	 * 
+	 * @param f
+	 *            The file this refers to
+	 * @param range
+	 *            The content of the Range header.
+	 * @throws FilesystemAccessException
+	 *             If we can't determine the length of the file (shouldn't
+	 *             happen).
+	 */
+	public FileSegment(File f, String range) throws FilesystemAccessException {
+		file = f;
+		Matcher m = Pattern.compile("^\\s*bytes=(\\d*)-(\\d*)\\s*$").matcher(
+				range);
+		if (m.matches()) {
+			if (!m.group(1).isEmpty())
+				from = Integer.valueOf(m.group(1));
+			if (!m.group(2).isEmpty())
+				to = Integer.valueOf(m.group(2)) + 1;
+			int size = (int) f.getSize();
+			if (from == null) {
+				from = size - to;
+				to = size;
+			} else if (to == null)
+				to = size;
+			else if (to > size)
+				to = size;
+		}
+	}
+
+	/**
+	 * Convert to a response, as per RFC 2616.
+	 * 
+	 * @param type
+	 *            The expected type of the data.
+	 * @return A JAX-RS response.
+	 */
+	public Response toResponse(MediaType type) {
+		if (from == null && to == null)
+			return ok(file).type(type).build();
+		if (from >= to)
+			return ok("Requested range not satisfiable").status(416).build();
+		return ok(this).status(206).type(type).build();
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/InteractionFeedREST.java b/server-webapp/src/main/java/org/taverna/server/master/rest/InteractionFeedREST.java
new file mode 100644
index 0000000..b9b4718
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/InteractionFeedREST.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static org.taverna.server.master.rest.ContentTypes.ATOM;
+
+import java.net.MalformedURLException;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.model.Feed;
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+
+/**
+ * A very stripped down ATOM feed for the interaction service.
+ * 
+ * @author Donal Fellows
+ */
+public interface InteractionFeedREST {
+	/**
+	 * Get the feed document for this ATOM feed.
+	 * 
+	 * @return The feed.
+	 * @throws FilesystemAccessException
+	 *             If we can't read from the feed directory.
+	 * @throws NoDirectoryEntryException
+	 *             If something changes things under our feet.
+	 */
+	@GET
+	@Path("/")
+	@Produces(ATOM)
+	@Description("Get the feed document for this ATOM feed.")
+	Feed getFeed() throws FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Adds an entry to this ATOM feed.
+	 * 
+	 * @param entry
+	 *            The entry to create.
+	 * @return A redirect to the created entry.
+	 * @throws MalformedURLException
+	 *             If we have problems generating the URI of the entry.
+	 * @throws FilesystemAccessException
+	 *             If we can't create the feed entry file.
+	 * @throws NoDirectoryEntryException
+	 *             If things get changed under our feet.
+	 * @throws NoUpdateException
+	 *             If we don't have permission to change things relating to this
+	 *             run.
+	 */
+	@POST
+	@Path("/")
+	@Consumes(ATOM)
+	@Produces(ATOM)
+	@Description("Adds an entry to this ATOM feed.")
+	Response addEntry(Entry entry) throws MalformedURLException,
+			FilesystemAccessException, NoDirectoryEntryException,
+			NoUpdateException;
+
+	/** Handles the OPTIONS request. */
+	@OPTIONS
+	@Path("/")
+	@Description("Describes what HTTP operations are supported on the feed.")
+	Response feedOptions();
+
+	/**
+	 * Gets the content of an entry in this ATOM feed.
+	 * 
+	 * @param id
+	 *            The ID of the entry to fetch.
+	 * @return The entry contents.
+	 * @throws FilesystemAccessException
+	 *             If we have problems reading the entry.
+	 * @throws NoDirectoryEntryException
+	 *             If we can't find the entry to read.
+	 */
+	@GET
+	@Path("{id}")
+	@Produces(ATOM)
+	@Description("Get the entry with a particular ID within this ATOM feed.")
+	Entry getEntry(@PathParam("id") String id)
+			throws FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Delete an entry from this ATOM feed.
+	 * 
+	 * @param id
+	 *            The ID of the entry to delete.
+	 * @return A simple message. Not very important!
+	 * @throws FilesystemAccessException
+	 *             If we have problems deleting the entry.
+	 * @throws NoDirectoryEntryException
+	 *             If we can't find the entry to delete.
+	 * @throws NoUpdateException
+	 *             If we don't have permission to alter things relating to this
+	 *             run.
+	 */
+	@DELETE
+	@Path("{id}")
+	@Produces("text/plain")
+	@Description("Deletes an entry from this ATOM feed.")
+	String deleteEntry(@PathParam("id") String id)
+			throws FilesystemAccessException, NoDirectoryEntryException,
+			NoUpdateException;
+
+	/** Handles the OPTIONS request. */
+	@OPTIONS
+	@Path("{id}")
+	@Description("Describes what HTTP operations are supported on an entry.")
+	Response entryOptions(@PathParam("{id}") String id);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/ListenerDefinition.java b/server-webapp/src/main/java/org/taverna/server/master/rest/ListenerDefinition.java
new file mode 100644
index 0000000..7a072be
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/ListenerDefinition.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+/**
+ * Description of what sort of event listener to create and attach to a workflow
+ * run. Bound via JAXB.
+ * 
+ * @author Donal Fellows
+ */
+@XmlRootElement(name = "listenerDefinition")
+@XmlType(name="ListenerDefinition")
+public class ListenerDefinition {
+	/**
+	 * The type of event listener to create.
+	 */
+	@XmlAttribute
+	public String type;
+	/**
+	 * How the event listener should be configured.
+	 */
+	@XmlValue
+	public String configuration;
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/MakeOrUpdateDirEntry.java b/server-webapp/src/main/java/org/taverna/server/master/rest/MakeOrUpdateDirEntry.java
new file mode 100644
index 0000000..0138f88
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/MakeOrUpdateDirEntry.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+/**
+ * The input to the REST interface for making directories and files, and
+ * uploading file contents. Done with JAXB.
+ * 
+ * @author Donal Fellows
+ */
+@XmlRootElement(name = "filesystemOperation")
+@XmlType(name = "FilesystemCreationOperation")
+@XmlSeeAlso( { MakeOrUpdateDirEntry.MakeDirectory.class,
+		MakeOrUpdateDirEntry.SetFileContents.class })
+public abstract class MakeOrUpdateDirEntry {
+	/**
+	 * The name of the file or directory that the operation applies to.
+	 */
+	@XmlAttribute
+	public String name;
+	/**
+	 * The contents of the file to upload.
+	 */
+	@XmlValue
+	public byte[] contents;
+
+	/**
+	 * Create a directory, described with JAXB. Should leave the
+	 * {@link MakeOrUpdateDirEntry#contents contents} field empty.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "mkdir")
+	@XmlType(name = "MakeDirectory")
+	public static class MakeDirectory extends MakeOrUpdateDirEntry {
+	}
+
+	/**
+	 * Create a file or set its contents, described with JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "upload")
+	@XmlType(name = "UploadFile")
+	public static class SetFileContents extends MakeOrUpdateDirEntry {
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerDirectoryREST.java b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerDirectoryREST.java
new file mode 100644
index 0000000..ea2f776
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerDirectoryREST.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static java.util.Collections.unmodifiableList;
+import static javax.ws.rs.core.MediaType.WILDCARD;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.ContentTypes.BYTES;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.URI_LIST;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+import static org.taverna.server.master.rest.ContentTypes.ZIP;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.PathSegment;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.core.Variant;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.File;
+
+/**
+ * Representation of how a workflow run's working directory tree looks.
+ * 
+ * @author Donal Fellows
+ */
+@RolesAllowed(USER)
+@Produces({ XML, JSON })
+@Consumes({ XML, JSON })
+@Description("Representation of how a workflow run's working directory tree looks.")
+public interface TavernaServerDirectoryREST {
+	/**
+	 * Get the working directory of the workflow run.
+	 * 
+	 * @param ui
+	 *            About how this method was called.
+	 * @return A description of the working directory.
+	 * @throws FilesystemAccessException
+	 */
+	@GET
+	@Path("/")
+	@Description("Describes the working directory of the workflow run.")
+	@Nonnull
+	DirectoryContents getDescription(@Nonnull @Context UriInfo ui)
+			throws FilesystemAccessException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path("{path:.*}")
+	@Description("Produces the description of the files/directories' baclava operations.")
+	Response options(@PathParam("path") List<PathSegment> path);
+
+	/**
+	 * Gets a description of the named entity in or beneath the working
+	 * directory of the workflow run, which may be either a {@link Directory} or
+	 * a {@link File}.
+	 * 
+	 * @param path
+	 *            The path to the thing to describe.
+	 * @param ui
+	 *            About how this method was called.
+	 * @param headers
+	 *            About what the caller was looking for.
+	 * @return An HTTP response containing a description of the named thing.
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the file or directory can't be looked up.
+	 * @throws FilesystemAccessException
+	 *             If something went wrong during the filesystem operation.
+	 * @throws NegotiationFailedException
+	 *             If the content type being downloaded isn't one that this
+	 *             method can support.
+	 */
+	@GET
+	@Path("{path:.+}")
+	@Produces({ XML, JSON, BYTES, ZIP, WILDCARD })
+	@Description("Gives a description of the named entity in or beneath the "
+			+ "working directory of the workflow run (either a Directory or File).")
+	@Nonnull
+	Response getDirectoryOrFileContents(
+			@Nonnull @PathParam("path") List<PathSegment> path,
+			@Nonnull @Context UriInfo ui, @Nonnull @Context HttpHeaders headers)
+			throws NoDirectoryEntryException, FilesystemAccessException,
+			NegotiationFailedException;
+
+	/**
+	 * Creates a directory in the filesystem beneath the working directory of
+	 * the workflow run, or creates or updates a file's contents, where that
+	 * file is in or below the working directory of a workflow run.
+	 * 
+	 * @param parent
+	 *            The directory to create the directory in.
+	 * @param operation
+	 *            What to call the directory to create.
+	 * @param ui
+	 *            About how this method was called.
+	 * @return An HTTP response indicating where the directory was actually made
+	 *         or what file was created/updated.
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the containing directory can't be looked up.
+	 * @throws NoUpdateException
+	 *             If the user is not permitted to update the run.
+	 * @throws FilesystemAccessException
+	 *             If something went wrong during the filesystem operation.
+	 */
+	@POST
+	@Path("{path:.*}")
+	@Description("Creates a directory in the filesystem beneath the working "
+			+ "directory of the workflow run, or creates or updates a file's "
+			+ "contents, where that file is in or below the working directory "
+			+ "of a workflow run.")
+	@Nonnull
+	Response makeDirectoryOrUpdateFile(
+			@Nonnull @PathParam("path") List<PathSegment> parent,
+			@Nonnull MakeOrUpdateDirEntry operation,
+			@Nonnull @Context UriInfo ui) throws NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Creates or updates a file in a particular location beneath the working
+	 * directory of the workflow run.
+	 * 
+	 * @param file
+	 *            The path to the file to create or update.
+	 * @param referenceList
+	 *            Location to get the file's contents from. Must be
+	 *            <i>publicly</i> readable.
+	 * @param ui
+	 *            About how this method was called.
+	 * @return An HTTP response indicating what file was created/updated.
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the containing directory can't be looked up.
+	 * @throws NoUpdateException
+	 *             If the user is not permitted to update the run.
+	 * @throws FilesystemAccessException
+	 *             If something went wrong during the filesystem operation.
+	 */
+	@POST
+	@Path("{path:(.*)}")
+	@Consumes(URI_LIST)
+	@Description("Creates or updates a file in a particular location beneath the "
+			+ "working directory of the workflow run with the contents of a "
+			+ "publicly readable URL.")
+	@Nonnull
+	Response setFileContentsFromURL(@PathParam("path") List<PathSegment> file,
+			List<URI> referenceList, @Context UriInfo ui)
+			throws NoDirectoryEntryException, NoUpdateException,
+			FilesystemAccessException;
+
+    /**
+	 * Creates or updates a file in a particular location beneath the working
+	 * directory of the workflow run.
+	 * 
+	 * @param file
+	 *            The path to the file to create or update.
+	 * @param contents
+	 *            Stream of bytes to set the file's contents to.
+	 * @param ui
+	 *            About how this method was called.
+	 * @return An HTTP response indicating what file was created/updated.
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the containing directory can't be looked up.
+	 * @throws NoUpdateException
+	 *             If the user is not permitted to update the run.
+	 * @throws FilesystemAccessException
+	 *             If something went wrong during the filesystem operation.
+	 */
+	@PUT
+	@Path("{path:(.*)}")
+	@Consumes({ BYTES, WILDCARD })
+	@Description("Creates or updates a file in a particular location beneath the "
+			+ "working directory of the workflow run.")
+	@Nonnull
+	Response setFileContents(@PathParam("path") List<PathSegment> file,
+			InputStream contents, @Context UriInfo ui)
+			throws NoDirectoryEntryException, NoUpdateException,
+			FilesystemAccessException;
+
+	/**
+	 * Deletes a file or directory that is in or below the working directory of
+	 * a workflow run.
+	 * 
+	 * @param path
+	 *            The path to the file or directory.
+	 * @return An HTTP response to the method.
+	 * @throws NoUpdateException
+	 *             If the user is not permitted to update the run.
+	 * @throws FilesystemAccessException
+	 *             If something went wrong during the filesystem operation.
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the file or directory can't be looked up.
+	 */
+	@DELETE
+	@Path("{path:.*}")
+	@Description("Deletes a file or directory that is in or below the working "
+			+ "directory of a workflow run.")
+	@Nonnull
+	Response destroyDirectoryEntry(@PathParam("path") List<PathSegment> path)
+			throws NoUpdateException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Exception thrown to indicate a failure by the client to provide an
+	 * acceptable content type.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@SuppressWarnings("serial")
+	public static class NegotiationFailedException extends Exception {
+		public List<Variant> accepted;
+
+		public NegotiationFailedException(String msg, List<Variant> accepted) {
+			super(msg);
+			this.accepted = unmodifiableList(accepted);
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerInputREST.java b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerInputREST.java
new file mode 100644
index 0000000..faed19d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerInputREST.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.TEXT;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+import static org.taverna.server.master.rest.TavernaServerInputREST.PathNames.BACLAVA;
+import static org.taverna.server.master.rest.TavernaServerInputREST.PathNames.EXPECTED;
+import static org.taverna.server.master.rest.TavernaServerInputREST.PathNames.ONE_INPUT;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.PathSegment;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElements;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+import org.taverna.server.master.exceptions.BadInputPortNameException;
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.port_description.InputDescription;
+
+/**
+ * This represents how a Taverna Server workflow run's inputs looks to a RESTful
+ * API.
+ * 
+ * @author Donal Fellows.
+ */
+@RolesAllowed(USER)
+@Description("This represents how a Taverna Server workflow run's inputs "
+		+ "looks to a RESTful API.")
+public interface TavernaServerInputREST {
+	/**
+	 * @return A description of the various URIs to inputs associated with a
+	 *         workflow run.
+	 */
+	@GET
+	@Path("/")
+	@Produces({ XML, JSON })
+	@Description("Describe the sub-URIs of this resource.")
+	@Nonnull
+	InputsDescriptor get();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path("/")
+	@Description("Produces the description of one run's inputs' operations.")
+	Response options();
+
+	/**
+	 * @return A description of the various URIs to inputs associated with a
+	 *         workflow run.
+	 */
+	@GET
+	@Path(EXPECTED)
+	@Produces({ XML, JSON })
+	@Description("Describe the expected inputs of this workflow run.")
+	@Nonnull
+	InputDescription getExpected();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(EXPECTED)
+	@Description("Produces the description of the expected inputs' operations.")
+	Response expectedOptions();
+
+	/**
+	 * @return The Baclava file that will supply all the inputs to the workflow
+	 *         run, or empty to indicate that no such file is specified.
+	 */
+	@GET
+	@Path(BACLAVA)
+	@Produces(TEXT)
+	@Description("Gives the Baclava file describing the inputs, or empty if "
+			+ "individual files are used.")
+	@Nonnull
+	String getBaclavaFile();
+
+	/**
+	 * Set the Baclava file that will supply all the inputs to the workflow run.
+	 * 
+	 * @param filename
+	 *            The filename to set.
+	 * @return The name of the Baclava file that was actually set.
+	 * @throws NoUpdateException
+	 *             If the user can't update the run.
+	 * @throws BadStateChangeException
+	 *             If the run is not Initialized.
+	 * @throws FilesystemAccessException
+	 *             If the filename starts with a <tt>/</tt> or if it contains a
+	 *             <tt>..</tt> segment.
+	 */
+	@PUT
+	@Path(BACLAVA)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Sets the Baclava file describing the inputs.")
+	@Nonnull
+	String setBaclavaFile(@Nonnull String filename) throws NoUpdateException,
+			BadStateChangeException, FilesystemAccessException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(BACLAVA)
+	@Description("Produces the description of the inputs' baclava operations.")
+	Response baclavaOptions();
+
+	/**
+	 * Get what input is set for the specific input.
+	 * 
+	 * @param name
+	 *            The input to set.
+	 * @param uriInfo
+	 *            About the URI used to access this resource.
+	 * @return A description of the input.
+	 * @throws BadInputPortNameException
+	 *             If no input with that name exists.
+	 */
+	@GET
+	@Path(ONE_INPUT)
+	@Produces({ XML, JSON })
+	@Description("Gives a description of what is used to supply a particular "
+			+ "input.")
+	@Nonnull
+	InDesc getInput(@Nonnull @PathParam("name") String name,
+			@Context UriInfo uriInfo) throws BadInputPortNameException;
+
+	/**
+	 * Set what an input uses to provide data into the workflow run.
+	 * 
+	 * @param name
+	 *            The name of the input.
+	 * @param inputDescriptor
+	 *            A description of the input
+	 * @param uriInfo
+	 *            About the URI used to access this resource.
+	 * @return A description of the input.
+	 * @throws NoUpdateException
+	 *             If the user can't update the run.
+	 * @throws BadStateChangeException
+	 *             If the run is not Initialized.
+	 * @throws FilesystemAccessException
+	 *             If a filename is being set and the filename starts with a
+	 *             <tt>/</tt> or if it contains a <tt>..</tt> segment.
+	 * @throws BadInputPortNameException
+	 *             If no input with that name exists.
+	 * @throws BadPropertyValueException
+	 *             If some bad misconfiguration has happened.
+	 */
+	@PUT
+	@Path(ONE_INPUT)
+	@Consumes({ XML, JSON })
+	@Produces({ XML, JSON })
+	@Description("Sets the source for a particular input port.")
+	@Nonnull
+	InDesc setInput(@Nonnull @PathParam("name") String name,
+			@Nonnull InDesc inputDescriptor, @Context UriInfo uriInfo) throws NoUpdateException,
+			BadStateChangeException, FilesystemAccessException,
+			BadPropertyValueException, BadInputPortNameException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ONE_INPUT)
+	@Description("Produces the description of the one input's operations.")
+	Response inputOptions(@PathParam("name") String name);
+
+	interface PathNames {
+		final String EXPECTED = "expected";
+		final String BACLAVA = "baclava";
+		final String ONE_INPUT = "input/{name}";
+	}
+
+	/**
+	 * A description of the structure of inputs to a Taverna workflow run, done
+	 * with JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "runInputs")
+	@XmlType(name = "TavernaRunInputs")
+	public static class InputsDescriptor extends VersionedElement {
+		/**
+		 * Where to find a description of the expected inputs to this workflow
+		 * run.
+		 */
+		public Uri expected;
+		/**
+		 * Where to find the overall Baclava document filename (if set).
+		 */
+		public Uri baclava;
+		/**
+		 * Where to find the details of inputs to particular ports (if set).
+		 */
+		public List<Uri> input;
+
+		/**
+		 * Make a blank description of the inputs.
+		 */
+		public InputsDescriptor() {
+		}
+
+		/**
+		 * Make the description of the inputs.
+		 * 
+		 * @param ui
+		 *            Information about the URIs to generate.
+		 * @param run
+		 *            The run whose inputs are to be described.
+		 */
+		public InputsDescriptor(UriInfo ui, TavernaRun run) {
+			super(true);
+			expected = new Uri(ui, EXPECTED);
+			baclava = new Uri(ui, BACLAVA);
+			input = new ArrayList<>();
+			for (Input i : run.getInputs())
+				input.add(new Uri(ui, ONE_INPUT, i.getName()));
+		}
+	}
+
+	/**
+	 * The Details of a particular input port's value assignment, done with
+	 * JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "runInput")
+	@XmlType(name = "InputDescription")
+	public static class InDesc extends VersionedElement {
+		/** Make a blank description of an input port. */
+		public InDesc() {
+		}
+
+		/**
+		 * Make a description of the given input port.
+		 * 
+		 * @param inputPort
+		 */
+		public InDesc(Input inputPort, UriInfo ui) {
+			super(true);
+			name = inputPort.getName();
+			if (inputPort.getFile() != null) {
+				assignment = new InDesc.File();
+				assignment.contents = inputPort.getFile();
+			} else {
+				assignment = new InDesc.Value();
+				assignment.contents = inputPort.getValue();
+			}
+			// .../runs/{id}/input/input/{name} ->
+			// .../runs/{id}/input/expected#{name}
+			UriBuilder ub = ui.getBaseUriBuilder();
+			List<PathSegment> segments = ui.getPathSegments();
+			for (PathSegment s : segments.subList(0, segments.size() - 2))
+				ub.segment(s.getPath());
+			ub.fragment(name);
+			descriptorRef = new Uri(ub).ref;
+		}
+
+		/** The name of the port. */
+		@XmlAttribute(required = false)
+		public String name;
+		/** Where the port is described. Ignored in user input. */
+		@XmlAttribute(required = false)
+		@XmlSchemaType(name = "anyURI")
+		public URI descriptorRef;
+		/** The character to use to split the input into a list. */
+		@XmlAttribute(name = "listDelimiter", required = false)
+		public String delimiter;
+
+		/**
+		 * Either a filename or a literal string, used to provide input to a
+		 * workflow port.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "InputContents")
+		public static abstract class AbstractContents {
+			/**
+			 * The contents of the description of the input port. Meaning not
+			 * defined.
+			 */
+			@XmlValue
+			public String contents;
+		};
+
+		/**
+		 * The name of a file that provides input to the port. The
+		 * {@link AbstractContents#contents contents} field is a filename.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "")
+		public static class File extends AbstractContents {
+		}
+
+		/**
+		 * The literal input to the port. The {@link AbstractContents#contents
+		 * contents} field is a literal input value.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "")
+		public static class Value extends AbstractContents {
+		}
+
+		/**
+		 * A reference to a file elsewhere <i>on this server</i>. The
+		 * {@link AbstractContents#contents contents} field is a URL to the file
+		 * (using the RESTful notation).
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "")
+		public static class Reference extends AbstractContents {
+		}
+
+		/**
+		 * The assignment of input values to the port.
+		 */
+		@XmlElements({ @XmlElement(name = "file", type = File.class),
+				@XmlElement(name = "reference", type = Reference.class),
+				@XmlElement(name = "value", type = Value.class) })
+		public AbstractContents assignment;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerListenersREST.java b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerListenersREST.java
new file mode 100644
index 0000000..e9b067e
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerListenersREST.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static org.taverna.server.master.common.Namespaces.XLINK;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.TEXT;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Listener;
+
+/**
+ * This represents <i>all</i> the event listeners attached to a workflow run.
+ * 
+ * @author Donal Fellows
+ * @see TavernaServerListenerREST
+ */
+@RolesAllowed(USER)
+@Description("This represents all the event listeners attached to a workflow "
+		+ "run.")
+public interface TavernaServerListenersREST {
+	/**
+	 * Get the listeners installed in the workflow run.
+	 * 
+	 * @param ui
+	 *            About how this method was called.
+	 * @return A list of descriptions of listeners.
+	 */
+	@GET
+	@Path("/")
+	@Produces({ XML, JSON })
+	@Description("Get the listeners installed in the workflow run.")
+	@Nonnull
+	Listeners getDescription(@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Add a new event listener to the named workflow run.
+	 * 
+	 * @param typeAndConfiguration
+	 *            What type of run should be created, and how should it be
+	 *            configured.
+	 * @param ui
+	 *            About how this method was called.
+	 * @return An HTTP response to the creation request.
+	 * @throws NoUpdateException
+	 *             If the user is not permitted to update the run.
+	 * @throws NoListenerException
+	 *             If no listener with the given type exists, or if the
+	 *             configuration is unacceptable in some way.
+	 */
+	@POST
+	@Path("/")
+	@Consumes({ XML, JSON })
+	@Description("Add a new event listener to the named workflow run.")
+	@Nonnull
+	Response addListener(@Nonnull ListenerDefinition typeAndConfiguration,
+			@Nonnull @Context UriInfo ui) throws NoUpdateException,
+			NoListenerException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path("/")
+	@Description("Produces the description of the run listeners' operations.")
+	Response listenersOptions();
+
+	/**
+	 * Resolve a particular listener from its name.
+	 * 
+	 * @param name
+	 *            The name of the listener to look up.
+	 * @return The listener's delegate in the REST world.
+	 * @throws NoListenerException
+	 *             If no listener with the given name exists.
+	 */
+	@Path("{name}")
+	@Description("Resolve a particular listener from its name.")
+	@Nonnull
+	TavernaServerListenerREST getListener(
+			@Nonnull @PathParam("name") String name) throws NoListenerException;
+
+	/**
+	 * This represents a single event listener attached to a workflow run.
+	 * 
+	 * @author Donal Fellows
+	 * @see TavernaServerListenersREST
+	 * @see Property
+	 */
+	@RolesAllowed(USER)
+	@Description("This represents a single event listener attached to a "
+			+ "workflow run.")
+	public interface TavernaServerListenerREST {
+		/**
+		 * Get the description of this listener.
+		 * 
+		 * @param ui
+		 *            Information about this request.
+		 * @return A description document.
+		 */
+		@GET
+		@Path("/")
+		@Produces({ XML, JSON })
+		@Description("Get the description of this listener.")
+		@Nonnull
+		ListenerDescription getDescription(@Nonnull @Context UriInfo ui);
+
+		/** Get an outline of the operations supported. */
+		@OPTIONS
+		@Path("/")
+		@Description("Produces the description of one run listener's operations.")
+		Response listenerOptions();
+
+		/**
+		 * Get the configuration for the given event listener that is attached
+		 * to a workflow run.
+		 * 
+		 * @return The configuration of the listener.
+		 */
+		@GET
+		@Path("configuration")
+		@Produces(TEXT)
+		@Description("Get the configuration for the given event listener that "
+				+ "is attached to a workflow run.")
+		@Nonnull
+		String getConfiguration();
+
+		/** Get an outline of the operations supported. */
+		@OPTIONS
+		@Path("configuration")
+		@Description("Produces the description of one run listener's "
+				+ "configuration's operations.")
+		Response configurationOptions();
+
+		/**
+		 * Get the list of properties supported by a given event listener
+		 * attached to a workflow run.
+		 * 
+		 * @param ui
+		 *            Information about this request.
+		 * @return The list of property names.
+		 */
+		@GET
+		@Path("properties")
+		@Produces({ XML, JSON })
+		@Description("Get the list of properties supported by a given event "
+				+ "listener attached to a workflow run.")
+		@Nonnull
+		Properties getProperties(@Nonnull @Context UriInfo ui);
+
+		/** Get an outline of the operations supported. */
+		@OPTIONS
+		@Path("properties")
+		@Description("Produces the description of one run listener's "
+				+ "properties' operations.")
+		Response propertiesOptions();
+
+		/**
+		 * Get an object representing a particular property.
+		 * 
+		 * @param propertyName
+		 * @return The property delegate.
+		 * @throws NoListenerException
+		 *             If there is no such property.
+		 */
+		@Path("properties/{propertyName}")
+		@Description("Get an object representing a particular property.")
+		@Nonnull
+		Property getProperty(
+				@Nonnull @PathParam("propertyName") String propertyName)
+				throws NoListenerException;
+	}
+
+	/**
+	 * This represents a single property attached of an event listener.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@RolesAllowed(USER)
+	@Description("This represents a single property attached of an event "
+			+ "listener.")
+	public interface Property {
+		/**
+		 * Get the value of the particular property of an event listener
+		 * attached to a workflow run.
+		 * 
+		 * @return The value of the property.
+		 */
+		@GET
+		@Path("/")
+		@Produces(TEXT)
+		@Description("Get the value of the particular property of an event "
+				+ "listener attached to a workflow run.")
+		@Nonnull
+		String getValue();
+
+		/**
+		 * Set the value of the particular property of an event listener
+		 * attached to a workflow run. Changing the value of the property may
+		 * cause the listener to alter its behaviour significantly.
+		 * 
+		 * @param value
+		 *            The value to set the property to.
+		 * @return The value of the property after being set.
+		 * @throws NoUpdateException
+		 *             If the user is not permitted to update the run.
+		 * @throws NoListenerException
+		 *             If the property is in the wrong format.
+		 */
+		@PUT
+		@Path("/")
+		@Consumes(TEXT)
+		@Produces(TEXT)
+		@Description("Set the value of the particular property of an event "
+				+ "listener attached to a workflow run.")
+		@Nonnull
+		String setValue(@Nonnull String value) throws NoUpdateException,
+				NoListenerException;
+
+		/** Get an outline of the operations supported. */
+		@OPTIONS
+		@Path("/")
+		@Description("Produces the description of one run listener's "
+				+ "property's operations.")
+		Response options();
+	}
+
+	/**
+	 * A description of an event listener that is attached to a workflow run.
+	 * Done with JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "ListenerDescription")
+	public class ListenerDescription extends VersionedElement {
+		/** Where this listener is located. */
+		@XmlAttribute(name = "href", namespace = XLINK)
+		@XmlSchemaType(name = "anyURI")
+		public URI location;
+		/** The (arbitrary) name of the event listener. */
+		@XmlAttribute
+		public String name;
+		/** The type of the event listener. */
+		@XmlAttribute
+		public String type;
+		/**
+		 * The location of the configuration document for the event listener.
+		 */
+		public Uri configuration;
+		/**
+		 * The name and location of the properties supported by the event
+		 * listener.
+		 */
+		@XmlElementWrapper(name = "properties", nillable = false)
+		@XmlElement(name = "property", nillable = false)
+		public List<PropertyDescription> properties;
+
+		/**
+		 * Make a blank listener description.
+		 */
+		public ListenerDescription() {
+		}
+
+		/**
+		 * Make a listener description that characterizes the given listener.
+		 * 
+		 * @param listener
+		 *            The listener to describe.
+		 * @param ub
+		 *            The factory for URIs. Must have already been secured.
+		 */
+		public ListenerDescription(Listener listener, UriBuilder ub) {
+			super(true);
+			name = listener.getName();
+			type = listener.getType();
+			configuration = new Uri(ub.clone().path("configuration"));
+			UriBuilder ub2 = ub.clone().path("properties/{prop}");
+			String[] props = listener.listProperties();
+			properties = new ArrayList<>(props.length);
+			for (String propName : props)
+				properties.add(new PropertyDescription(propName, ub2));
+		}
+	}
+
+	/**
+	 * The description of a single property, done with JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlType(name = "PropertyDescription")
+	public static class PropertyDescription extends Uri {
+		/**
+		 * The name of the property.
+		 */
+		@XmlAttribute
+		String name;
+
+		/**
+		 * Make an empty description of a property.
+		 */
+		public PropertyDescription() {
+		}
+
+		/**
+		 * Make a description of a property.
+		 * 
+		 * @param listenerName
+		 *            The name of the listener whose property this is.
+		 * @param propName
+		 *            The name of the property.
+		 * @param ub
+		 *            The factory for URIs. Must have already been secured.
+		 */
+		PropertyDescription(String propName, UriBuilder ub) {
+			super(ub, propName);
+			this.name = propName;
+		}
+	}
+
+	/**
+	 * The list of descriptions of listeners attached to a run. Done with JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class Listeners extends VersionedElement {
+		/**
+		 * The listeners for a workflow run.
+		 */
+		@XmlElement(name = "listener")
+		public List<ListenerDescription> listener;
+
+		/**
+		 * Make a blank description of listeners.
+		 */
+		public Listeners() {
+			listener = new ArrayList<>();
+		}
+
+		/**
+		 * Make a description of the whole group out of the given list of
+		 * listener descriptions.
+		 * 
+		 * @param listeners
+		 *            The collection of (partial) listener descriptions.
+		 * @param ub
+		 *            How to build the location of the listeners. Must have
+		 *            already been secured.
+		 */
+		public Listeners(List<ListenerDescription> listeners, UriBuilder ub) {
+			super(true);
+			listener = listeners;
+			for (ListenerDescription ld : listeners)
+				ld.location = ub.build(ld.name);
+		}
+	}
+
+	/**
+	 * The list of properties of a listener. Done with JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class Properties extends VersionedElement {
+		/**
+		 * The references to the properties of a listener.
+		 */
+		@XmlElement
+		public List<PropertyDescription> property;
+
+		/**
+		 * Make an empty description of the properties of a listener.
+		 */
+		public Properties() {
+		}
+
+		/**
+		 * Make the description of the properties of a listener.
+		 * 
+		 * @param ub
+		 *            The factory for URIs, configured. Must have already been
+		 *            secured.
+		 * @param properties
+		 *            The names of the properties.
+		 */
+		public Properties(UriBuilder ub, String[] properties) {
+			super(true);
+			property = new ArrayList<>(properties.length);
+			for (String propName : properties)
+				property.add(new PropertyDescription(propName, ub));
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerREST.java b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerREST.java
new file mode 100644
index 0000000..62158bb
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerREST.java
@@ -0,0 +1,604 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.URI_LIST;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.POL;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.POL_CAPABILITIES;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.POL_NOTIFIERS;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.POL_OP_LIMIT;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.POL_PERM_LIST;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.POL_PERM_WF;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.POL_RUN_LIMIT;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.ROOT;
+import static org.taverna.server.master.rest.TavernaServerREST.PathNames.RUNS;
+import static org.taverna.server.master.rest.handler.Scufl2DocumentHandler.SCUFL2;
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.model.Feed;
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.common.Capability;
+import org.taverna.server.master.common.RunReference;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.common.version.Version;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.soap.TavernaServerSOAP;
+
+/**
+ * The REST service interface to Taverna 3 Server.
+ * 
+ * @author Donal Fellows
+ * @see TavernaServerSOAP
+ */
+@RolesAllowed(USER)
+@Description("This is REST service interface to Taverna " + Version.JAVA
+		+ " Server.")
+public interface TavernaServerREST {
+	/**
+	 * Produces the description of the service.
+	 * 
+	 * @param ui
+	 *            About the URI being accessed.
+	 * @return The description.
+	 */
+	@GET
+	@Path(ROOT)
+	@Produces({ XML, JSON })
+	@Description("Produces the description of the service.")
+	@Nonnull
+	ServerDescription describeService(@Nonnull @Context UriInfo ui);
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ROOT)
+	@Description("Produces the description of the service.")
+	Response serviceOptions();
+
+	/**
+	 * Produces a description of the list of runs.
+	 * 
+	 * @param ui
+	 *            About the URI being accessed.
+	 * @return A description of the list of runs that are available.
+	 */
+	@GET
+	@Path(RUNS)
+	@Produces({ XML, JSON })
+	@RolesAllowed(USER)
+	@Description("Produces a list of all runs visible to the user.")
+	@Nonnull
+	RunList listUsersRuns(@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Accepts (or not) a request to create a new run executing the given
+	 * workflow.
+	 * 
+	 * @param workflow
+	 *            The workflow document to execute.
+	 * @param ui
+	 *            About the URI being accessed.
+	 * @return A response to the POST describing what was created.
+	 * @throws NoUpdateException
+	 *             If the POST failed.
+	 */
+	@POST
+	@Path(RUNS)
+	@Consumes({ T2FLOW, SCUFL2, XML })
+	@RolesAllowed(USER)
+	@Description("Accepts (or not) a request to create a new run executing "
+			+ "the given workflow.")
+	@Nonnull
+	Response submitWorkflow(@Nonnull Workflow workflow,
+			@Nonnull @Context UriInfo ui) throws NoUpdateException;
+
+	/**
+	 * Accepts (or not) a request to create a new run executing the workflow at
+	 * the given location.
+	 * 
+	 * @param workflowReference
+	 *            The wrapped URI to workflow document to execute.
+	 * @param ui
+	 *            About the URI being POSTed to.
+	 * @return A response to the POST describing what was created.
+	 * @throws NoUpdateException
+	 *             If the POST failed.
+	 * @throw NoCreateException If the workflow couldn't be read into the server
+	 *        or the engine rejects it.
+	 */
+	@POST
+	@Path(RUNS)
+	@Consumes(URI_LIST)
+	@RolesAllowed(USER)
+	@Description("Accepts a URL to a workflow to download and run. The URL "
+			+ "must be hosted on a publicly-accessible service.")
+	@Nonnull
+	Response submitWorkflowByURL(@Nonnull List<URI> referenceList,
+			@Nonnull @Context UriInfo ui) throws NoCreateException,
+			NoUpdateException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(RUNS)
+	@Description("Produces the description of the operations on the "
+			+ "collection of runs.")
+	Response runsOptions();
+
+	/**
+	 * @return A description of the policies supported by this server.
+	 */
+	@Path(POL)
+	@Description("The policies supported by this server.")
+	@Nonnull
+	PolicyView getPolicyDescription();
+
+	/**
+	 * Get a particular named run resource.
+	 * 
+	 * @param runName
+	 *            The name of the run.
+	 * @param uriInfo
+	 *            About the URI used to access this run.
+	 * @return A RESTful delegate for the run.
+	 * @throws UnknownRunException
+	 *             If the run handle is unknown to the current user.
+	 */
+	@Path(RUNS + "/{runName}")
+	@RolesAllowed(USER)
+	@Description("Get a particular named run resource to dispatch to.")
+	@Nonnull
+	TavernaServerRunREST getRunResource(
+			@Nonnull @PathParam("runName") String runName,
+			@Nonnull @Context UriInfo uriInfo) throws UnknownRunException;
+
+	/**
+	 * Factored out path names used in the {@link TavernaServerREST} interface
+	 * and related places.
+	 * 
+	 * @author Donal Fellows
+	 */
+	interface PathNames {
+		public static final String ROOT = "/";
+		public static final String RUNS = "runs";
+		public static final String POL = "policy";
+		public static final String POL_CAPABILITIES = "capabilities";
+		public static final String POL_RUN_LIMIT = "runLimit";
+		public static final String POL_OP_LIMIT = "operatingLimit";
+		public static final String POL_PERM_WF = "permittedWorkflows";
+		public static final String POL_PERM_LIST = "permittedListenerTypes";
+		public static final String POL_NOTIFIERS = "enabledNotificationFabrics";
+	}
+
+	/**
+	 * Helper class for describing the server's user-facing management API via
+	 * JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class ServerDescription extends VersionedElement {
+		/**
+		 * References to the collection of runs (known about by the current
+		 * user) in this server.
+		 */
+		public Uri runs;
+		/**
+		 * Reference to the policy description part of this server.
+		 */
+		public Uri policy;
+		/**
+		 * Reference to the Atom event feed produced by this server.
+		 */
+		public Uri feed;
+		/**
+		 * Reference to the interaction feed for this server.
+		 */
+		public Uri interactionFeed;
+
+		/** Make a blank server description. */
+		public ServerDescription() {
+		}
+
+		/**
+		 * Make a description of the server.
+		 * 
+		 * @param ui
+		 *            The factory for URIs.
+		 */
+		public ServerDescription(UriInfo ui, String interactionFeed) {
+			super(true);
+			String base = ui.getBaseUri().toString();
+			runs = new Uri(ui, RUNS);
+			policy = new Uri(ui, false, POL);
+			feed = new Uri(java.net.URI.create(base.replaceFirst("/rest$",
+					"/feed")));
+			if (interactionFeed != null && !interactionFeed.isEmpty())
+				this.interactionFeed = new Uri(
+						java.net.URI.create(interactionFeed));
+		}
+	}
+
+	/**
+	 * How to discover the publicly-visible policies supported by this server.
+	 * 
+	 * @author Donal Fellows
+	 */
+	public interface PolicyView {
+		/**
+		 * Describe the URIs in this view of the server's policies.
+		 * 
+		 * @param ui
+		 *            About the URI used to retrieve the description.
+		 * @return The description, which may be serialised as XML or JSON.
+		 */
+		@GET
+		@Path(ROOT)
+		@Produces({ XML, JSON })
+		@Description("Describe the parts of this policy.")
+		@Nonnull
+		public PolicyDescription getDescription(@Nonnull @Context UriInfo ui);
+
+		/**
+		 * Gets the maximum number of simultaneous runs that the user may
+		 * create. The <i>actual</i> number they can create may be lower than
+		 * this. If this number is lower than the number they currently have,
+		 * they will be unable to create any runs at all.
+		 * 
+		 * @return The maximum number of existing runs.
+		 */
+		@GET
+		@Path(POL_RUN_LIMIT)
+		@Produces("text/plain")
+		@RolesAllowed(USER)
+		@Description("Gets the maximum number of simultaneous runs in any "
+				+ "state that the user may create.")
+		@Nonnull
+		public int getMaxSimultaneousRuns();
+
+		/**
+		 * Gets the maximum number of simultaneous
+		 * {@linkplain org.taverna.server.master.common.Status.Operating
+		 * operating} runs that the user may create. The <i>actual</i> number
+		 * they can start may be lower than this. If this number is lower than
+		 * the number they currently have, they will be unable to start any runs
+		 * at all.
+		 * 
+		 * @return The maximum number of operating runs.
+		 */
+		@GET
+		@Path(POL_OP_LIMIT)
+		@Produces("text/plain")
+		@RolesAllowed(USER)
+		@Description("Gets the maximum number of simultaneously operating "
+				+ "runs that the user may have. Note that this is often a "
+				+ "global limit; it does not represent a promise that a "
+				+ "particular user may be able to have that many operating "
+				+ "runs at once.")
+		public int getMaxOperatingRuns();
+
+		/**
+		 * Gets the list of permitted workflows. Any workflow may be submitted
+		 * if the list is empty, otherwise it must be one of the workflows on
+		 * this list.
+		 * 
+		 * @return The list of workflow documents.
+		 */
+		@GET
+		@Path(POL_PERM_WF)
+		@Produces({ XML, JSON })
+		@RolesAllowed(USER)
+		@Description("Gets the list of permitted workflows.")
+		@Nonnull
+		public PermittedWorkflows getPermittedWorkflows();
+
+		/**
+		 * Gets the list of permitted event listener types. All event listeners
+		 * must be of a type described on this list.
+		 * 
+		 * @return The types of event listeners allowed.
+		 */
+		@GET
+		@Path(POL_PERM_LIST)
+		@Produces({ XML, JSON })
+		@RolesAllowed(USER)
+		@Description("Gets the list of permitted event listener types.")
+		@Nonnull
+		public PermittedListeners getPermittedListeners();
+
+		/**
+		 * Gets the list of supported, enabled notification fabrics. Each
+		 * corresponds (approximately) to a protocol, e.g., email.
+		 * 
+		 * @return List of notifier names; each is the scheme of a notification
+		 *         destination URI.
+		 */
+		@GET
+		@Path(POL_NOTIFIERS)
+		@Produces({ XML, JSON })
+		@RolesAllowed(USER)
+		@Description("Gets the list of supported, enabled notification "
+				+ "fabrics. Each corresponds (approximately) to a protocol, "
+				+ "e.g., email.")
+		@Nonnull
+		public EnabledNotificationFabrics getEnabledNotifiers();
+
+		@GET
+		@Path(POL_CAPABILITIES)
+		@Produces({ XML, JSON })
+		@RolesAllowed(USER)
+		@Description("Gets a description of the capabilities supported by "
+				+ "this installation of Taverna Server.")
+		@Nonnull
+		public CapabilityList getCapabilities();
+
+		/**
+		 * A description of the parts of a server policy.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlRootElement
+		@XmlType(name = "")
+		public static class PolicyDescription extends VersionedElement {
+			/**
+			 * Where to go to find out about the maximum number of runs.
+			 */
+			public Uri runLimit;
+			/**
+			 * Where to go to find out about the maximum number of operating
+			 * runs.
+			 */
+			public Uri operatingLimit;
+			/**
+			 * Where to go to find out about what workflows are allowed.
+			 */
+			public Uri permittedWorkflows;
+			/**
+			 * Where to go to find out about what listeners are allowed.
+			 */
+			public Uri permittedListenerTypes;
+			/**
+			 * How notifications may be sent.
+			 */
+			public Uri enabledNotificationFabrics;
+
+			public Uri capabilities;
+
+			/** Make a blank server description. */
+			public PolicyDescription() {
+			}
+
+			/**
+			 * Make a server description.
+			 * 
+			 * @param ui
+			 *            About the URI used to access this description.
+			 */
+			public PolicyDescription(UriInfo ui) {
+				super(true);
+				runLimit = new Uri(ui, false, POL_RUN_LIMIT);
+				operatingLimit = new Uri(ui, false, POL_OP_LIMIT);
+				permittedWorkflows = new Uri(ui, false, POL_PERM_WF);
+				permittedListenerTypes = new Uri(ui, false, POL_PERM_LIST);
+				enabledNotificationFabrics = new Uri(ui, false, POL_NOTIFIERS);
+				capabilities = new Uri(ui, false, POL_CAPABILITIES);
+			}
+		}
+
+		/**
+		 * A list of Taverna Server capabilities.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlRootElement(name = "capabilities")
+		@XmlType(name = "")
+		public static class CapabilityList {
+			@XmlElement(name = "capability", namespace = SERVER)
+			public List<Capability> capability = new ArrayList<>();
+		}
+	}
+
+	/**
+	 * Helper class for describing the workflows that are allowed via JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class PermittedWorkflows {
+		/** The workflows that are permitted. */
+		@XmlElement
+		public List<URI> workflow;
+
+		/**
+		 * Make an empty list of permitted workflows.
+		 */
+		public PermittedWorkflows() {
+			workflow = new ArrayList<>();
+		}
+
+		/**
+		 * Make a list of permitted workflows.
+		 * 
+		 * @param permitted
+		 */
+		public PermittedWorkflows(List<URI> permitted) {
+			if (permitted == null)
+				workflow = new ArrayList<>();
+			else
+				workflow = new ArrayList<>(permitted);
+		}
+	}
+
+	/**
+	 * Helper class for describing the listener types that are allowed via JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class PermittedListeners {
+		/** The listener types that are permitted. */
+		@XmlElement
+		public List<String> type;
+
+		/**
+		 * Make an empty list of permitted listener types.
+		 */
+		public PermittedListeners() {
+			type = new ArrayList<>();
+		}
+
+		/**
+		 * Make a list of permitted listener types.
+		 * 
+		 * @param listenerTypes
+		 */
+		public PermittedListeners(List<String> listenerTypes) {
+			type = listenerTypes;
+		}
+	}
+
+	/**
+	 * Helper class for describing the workflow runs.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class RunList {
+		/** The references to the workflow runs. */
+		@XmlElement
+		public List<RunReference> run;
+
+		/**
+		 * Make an empty list of run references.
+		 */
+		public RunList() {
+			run = new ArrayList<>();
+		}
+
+		/**
+		 * Make a list of references to workflow runs.
+		 * 
+		 * @param runs
+		 *            The mapping of runs to describe.
+		 * @param ub
+		 *            How to construct URIs to the runs. Must have already been
+		 *            secured as it needs to have its pattern applied.
+		 */
+		public RunList(Map<String, TavernaRun> runs, UriBuilder ub) {
+			run = new ArrayList<>(runs.size());
+			for (String name : runs.keySet())
+				run.add(new RunReference(name, ub));
+		}
+	}
+
+	/**
+	 * Helper class for describing the listener types that are allowed via JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class EnabledNotificationFabrics {
+		/** The notification fabrics that are enabled. */
+		@XmlElement
+		public List<String> notifier;
+
+		/**
+		 * Make an empty list of enabled notifiers.
+		 */
+		public EnabledNotificationFabrics() {
+			notifier = new ArrayList<>();
+		}
+
+		/**
+		 * Make a list of enabled notifiers.
+		 * 
+		 * @param enabledNodifiers
+		 */
+		public EnabledNotificationFabrics(List<String> enabledNodifiers) {
+			notifier = enabledNodifiers;
+		}
+	}
+
+	/**
+	 * The interface exposed by the Atom feed of events.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@RolesAllowed(USER)
+	public interface EventFeed {
+		/**
+		 * @return the feed of events for the current user.
+		 */
+		@GET
+		@Path("/")
+		@Produces("application/atom+xml;type=feed")
+		@Description("Get an Atom feed for the user's events.")
+		@Nonnull
+		Feed getFeed(@Context UriInfo ui);
+
+		/**
+		 * @param id
+		 *            The identifier for a particular event.
+		 * @return the details about the given event.
+		 */
+		@GET
+		@Path("{id}")
+		@Produces("application/atom+xml;type=entry")
+		@Description("Get a particular Atom event.")
+		@Nonnull
+		Entry getEvent(@Nonnull @PathParam("id") String id);
+	}
+
+	/**
+	 * A reference to a workflow hosted on some public HTTP server.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "workflowurl")
+	@XmlType(name = "WorkflowReference")
+	public static class WorkflowReference {
+		@XmlValue
+		@XmlSchemaType(name = "anyURI")
+		public URI url;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerRunREST.java b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerRunREST.java
new file mode 100644
index 0000000..2b328fd
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerRunREST.java
@@ -0,0 +1,797 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static javax.ws.rs.core.UriBuilder.fromUri;
+import static org.joda.time.format.ISODateTimeFormat.basicDateTime;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.handler.Scufl2DocumentHandler.SCUFL2;
+import static org.taverna.server.master.interaction.InteractionFeedSupport.FEED_URL_DIR;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.ROBUNDLE;
+import static org.taverna.server.master.rest.ContentTypes.TEXT;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.DIR;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.GENERATE_PROVENANCE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.IN;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.LISTEN;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.LOG;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.NAME;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.OUT;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.PROFILE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.ROOT;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.RUNBUNDLE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.SEC;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.STATUS;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.STDERR;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.STDOUT;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.T_CREATE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.T_EXPIRE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.T_FINISH;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.T_START;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.USAGE;
+import static org.taverna.server.master.rest.TavernaServerRunREST.PathNames.WF;
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.joda.time.format.DateTimeFormatter;
+import org.taverna.server.master.common.Namespaces;
+import org.taverna.server.master.common.ProfileList;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.NotOwnerException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.port_description.OutputDescription;
+
+/**
+ * This represents how a Taverna Server workflow run looks to a RESTful API.
+ * 
+ * @author Donal Fellows.
+ */
+@Description("This represents how a Taverna Server workflow run looks to a "
+		+ "RESTful API.")
+@RolesAllowed(USER)
+public interface TavernaServerRunREST {
+	/**
+	 * Describes a workflow run.
+	 * 
+	 * @param ui
+	 *            About the URI used to access this resource.
+	 * @return The description.
+	 */
+	@GET
+	@Path(ROOT)
+	@Description("Describes a workflow run.")
+	@Produces({ XML, JSON })
+	@Nonnull
+	public RunDescription getDescription(@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Deletes a workflow run.
+	 * 
+	 * @return An HTTP response to the deletion.
+	 * @throws NoUpdateException
+	 *             If the user may see the handle but may not delete it.
+	 */
+	@DELETE
+	@Path(ROOT)
+	@Description("Deletes a workflow run.")
+	@Nonnull
+	public Response destroy() throws NoUpdateException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ROOT)
+	@Description("Produces the description of the run.")
+	Response runOptions();
+
+	/**
+	 * Returns the workflow document used to create the workflow run.
+	 * 
+	 * @return The workflow document.
+	 */
+	@GET
+	@Path(WF)
+	@Produces({ T2FLOW, SCUFL2, XML, JSON })
+	@Description("Gives the workflow document used to create the workflow run.")
+	@Nonnull
+	public Workflow getWorkflow();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(WF)
+	@Description("Produces the description of the run workflow.")
+	Response workflowOptions();
+
+	/** Get the workflow name. */
+	@GET
+	@Path(NAME)
+	@Produces(TEXT)
+	@Description("Gives the descriptive name of the workflow run.")
+	@Nonnull
+	public String getName();
+
+	/**
+	 * Set the workflow name.
+	 * 
+	 * @throws NoUpdateException
+	 *             If the user is not permitted to change the workflow.
+	 */
+	@PUT
+	@Path(NAME)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Set the descriptive name of the workflow run. Note that "
+			+ "this value may be arbitrarily truncated by the implementation.")
+	@Nonnull
+	public String setName(String name) throws NoUpdateException;
+
+	/** Produce the workflow name HTTP operations. */
+	@OPTIONS
+	@Path(NAME)
+	@Description("Produces the description of the operations on the run's "
+			+ "descriptive name.")
+	@Nonnull
+	Response nameOptions();
+
+	/**
+	 * Produces the name of the workflow's main profile.
+	 * 
+	 * @return The main profile name, or the empty string if there is no such
+	 *         profile.
+	 */
+	@GET
+	@Path(PROFILE)
+	@Produces(TEXT)
+	@Description("Gives the name of the workflow's main profile, or the empty string if none is defined.")
+	@Nonnull
+	String getMainProfileName();
+
+	/**
+	 * Get a description of the profiles supported by the workflow document used
+	 * to create this run.
+	 * 
+	 * @return A description of the supported profiles.
+	 */
+	@GET
+	@Path(PROFILE)
+	@Produces({ XML, JSON })
+	@Description("Describes what profiles exist on the workflow.")
+	@Nonnull
+	ProfileList getProfiles();
+
+	/** Produce the workflow profile HTTP operations. */
+	@OPTIONS
+	@Path(PROFILE)
+	@Description("Produces the description of the operations on the run's "
+			+ "profile.")
+	@Nonnull
+	Response profileOptions();
+
+	/**
+	 * Returns a resource that represents the workflow run's security
+	 * properties. These may only be accessed by the owner.
+	 * 
+	 * @return The security resource.
+	 * @throws NotOwnerException
+	 *             If the accessing principal isn't the owning principal.
+	 */
+	@Path(SEC)
+	@Description("Access the workflow run's security.")
+	@Nonnull
+	public TavernaServerSecurityREST getSecurity() throws NotOwnerException;
+
+	/**
+	 * Returns the time when the workflow run becomes eligible for automatic
+	 * deletion.
+	 * 
+	 * @return When the run expires.
+	 */
+	@GET
+	@Path(T_EXPIRE)
+	@Produces(TEXT)
+	@Description("Gives the time when the workflow run becomes eligible for "
+			+ "automatic deletion.")
+	@Nonnull
+	public String getExpiryTime();
+
+	/**
+	 * Sets the time when the workflow run becomes eligible for automatic
+	 * deletion.
+	 * 
+	 * @param expiry
+	 *            When the run will expire.
+	 * @return When the run will actually expire.
+	 * @throws NoUpdateException
+	 *             If the current user is not permitted to manage the lifetime
+	 *             of the run.
+	 */
+	@PUT
+	@Path(T_EXPIRE)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Sets the time when the workflow run becomes eligible for "
+			+ "automatic deletion.")
+	@Nonnull
+	public String setExpiryTime(@Nonnull String expiry)
+			throws NoUpdateException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(T_EXPIRE)
+	@Description("Produces the description of the run expiry.")
+	Response expiryOptions();
+
+	/**
+	 * Returns the time when the workflow run was created.
+	 * 
+	 * @return When the run was first submitted to the server.
+	 */
+	@GET
+	@Path(T_CREATE)
+	@Produces(TEXT)
+	@Description("Gives the time when the workflow run was first submitted "
+			+ "to the server.")
+	@Nonnull
+	public String getCreateTime();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(T_CREATE)
+	@Description("Produces the description of the run create time.")
+	Response createTimeOptions();
+
+	/**
+	 * Returns the time when the workflow run was started (through a user-driven
+	 * state change).
+	 * 
+	 * @return When the run was started, or <tt>null</tt>.
+	 */
+	@GET
+	@Path(T_START)
+	@Produces(TEXT)
+	@Description("Gives the time when the workflow run was started, or an "
+			+ "empty string if the run has not yet started.")
+	@Nonnull
+	public String getStartTime();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(T_START)
+	@Description("Produces the description of the run start time.")
+	Response startTimeOptions();
+
+	/**
+	 * Returns the time when the workflow run was detected to have finished.
+	 * 
+	 * @return When the run finished, or <tt>null</tt>.
+	 */
+	@GET
+	@Path(T_FINISH)
+	@Produces(TEXT)
+	@Description("Gives the time when the workflow run was first detected as "
+			+ "finished, or an empty string if it has not yet finished "
+			+ "(including if it has never started).")
+	@Nonnull
+	public String getFinishTime();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(T_FINISH)
+	@Description("Produces the description of the run finish time.")
+	Response finishTimeOptions();
+
+	/**
+	 * Gets the current status of the workflow run.
+	 * 
+	 * @return The status code.
+	 */
+	@GET
+	@Path(STATUS)
+	@Produces(TEXT)
+	@Description("Gives the current status of the workflow run.")
+	@Nonnull
+	public String getStatus();
+
+	/**
+	 * Sets the status of the workflow run. This does nothing if the status code
+	 * is the same as the run's current state.
+	 * 
+	 * @param status
+	 *            The new status code.
+	 * @return Description of what status the run is actually in, or a 202 to
+	 *         indicate that things are still changing.
+	 * @throws NoUpdateException
+	 *             If the current user is not permitted to update the run.
+	 * @throws BadStateChangeException
+	 *             If the state cannot be modified in the manner requested.
+	 */
+	@PUT
+	@Path(STATUS)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Attempts to update the status of the workflow run.")
+	@Nonnull
+	public Response setStatus(@Nonnull String status) throws NoUpdateException,
+			BadStateChangeException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(STATUS)
+	@Description("Produces the description of the run status.")
+	Response statusOptions();
+
+	/**
+	 * Get the working directory of this workflow run.
+	 * 
+	 * @return A RESTful delegate for the working directory.
+	 */
+	@Path(DIR)
+	@Description("Get the working directory of this workflow run.")
+	@Nonnull
+	public TavernaServerDirectoryREST getWorkingDirectory();
+
+	/**
+	 * Get the event listeners attached to this workflow run.
+	 * 
+	 * @return A RESTful delegate for the list of listeners.
+	 */
+	@Path(LISTEN)
+	@Description("Get the event listeners attached to this workflow run.")
+	@Nonnull
+	public TavernaServerListenersREST getListeners();
+
+	/**
+	 * Get a delegate for working with the inputs to this workflow run.
+	 * 
+	 * @param ui
+	 *            About the URI used to access this resource.
+	 * @return A RESTful delegate for the inputs.
+	 */
+	@Path(IN)
+	@Description("Get the inputs to this workflow run.")
+	@Nonnull
+	public TavernaServerInputREST getInputs(@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Get the output Baclava file for this workflow run.
+	 * 
+	 * @return The filename, or empty string to indicate that the outputs will
+	 *         be written to the <tt>out</tt> directory.
+	 */
+	@GET
+	@Path(OUT)
+	@Produces(TEXT)
+	@Description("Gives the Baclava file where output will be written; empty "
+			+ "means use multiple simple files in the out directory.")
+	@Nonnull
+	public String getOutputFile();
+
+	/**
+	 * Get a description of the outputs.
+	 * 
+	 * @param ui
+	 *            About the URI used to access this operation.
+	 * @return A description of the outputs (higher level than the filesystem).
+	 * @throws BadStateChangeException
+	 *             If the run is in the {@link Status#Initialized Initialized}
+	 *             state.
+	 * @throws FilesystemAccessException
+	 *             If problems occur when accessing the filesystem.
+	 * @throws NoDirectoryEntryException
+	 *             If things are odd in the filesystem.
+	 */
+	@GET
+	@Path(OUT)
+	@Produces({ XML, JSON })
+	@Description("Gives a description of the outputs, as currently understood")
+	@Nonnull
+	public OutputDescription getOutputDescription(@Nonnull @Context UriInfo ui)
+			throws BadStateChangeException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Set the output Baclava file for this workflow run.
+	 * 
+	 * @param filename
+	 *            The Baclava file to use, or empty to make the outputs be
+	 *            written to individual files in the <tt>out</tt> subdirectory
+	 *            of the working directory.
+	 * @return The Baclava file as actually set.
+	 * @throws NoUpdateException
+	 *             If the current user is not permitted to update the run.
+	 * @throws FilesystemAccessException
+	 *             If the filename is invalid (starts with <tt>/</tt> or
+	 *             contains a <tt>..</tt> segment).
+	 * @throws BadStateChangeException
+	 *             If the workflow is not in the Initialized state.
+	 */
+	@PUT
+	@Path(OUT)
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Description("Sets the Baclava file where output will be written; empty "
+			+ "means use multiple simple files in the out directory.")
+	@Nonnull
+	public String setOutputFile(@Nonnull String filename)
+			throws NoUpdateException, FilesystemAccessException,
+			BadStateChangeException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(OUT)
+	@Description("Produces the description of the run output.")
+	Response outputOptions();
+
+	/**
+	 * Get a handle to the interaction feed.
+	 * 
+	 * @return
+	 */
+	@Path(FEED_URL_DIR)
+	@Description("Access the interaction feed for the workflow run.")
+	@Nonnull
+	InteractionFeedREST getInteractionFeed();
+
+	/**
+	 * @return The stdout for the workflow run, or empty string if the run has
+	 *         not yet started.
+	 * @throws NoListenerException
+	 */
+	@GET
+	@Path(STDOUT)
+	@Description("Return the stdout for the workflow run.")
+	@Produces(TEXT)
+	@Nonnull
+	String getStdout() throws NoListenerException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(STDOUT)
+	@Description("Return the stdout for the workflow run.")
+	Response stdoutOptions();
+
+	/**
+	 * @return The stderr for the workflow run, or empty string if the run has
+	 *         not yet started.
+	 * @throws NoListenerException
+	 */
+	@GET
+	@Path(STDERR)
+	@Description("Return the stderr for the workflow run.")
+	@Produces(TEXT)
+	@Nonnull
+	String getStderr() throws NoListenerException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(STDERR)
+	@Description("Return the stderr for the workflow run.")
+	Response stderrOptions();
+
+	/**
+	 * @return The usage record for the workflow run, wrapped in a Response, or
+	 *         "empty content" if the run has not yet finished.
+	 * @throws NoListenerException
+	 * @throws JAXBException
+	 */
+	@GET
+	@Path(USAGE)
+	@Description("Return the usage record for the workflow run.")
+	@Produces(XML)
+	@Nonnull
+	Response getUsage() throws NoListenerException, JAXBException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(USAGE)
+	@Description("Return the usage record for the workflow run.")
+	Response usageOptions();
+
+	/**
+	 * @return The log for the workflow run, or empty string if the run has not
+	 *         yet started.
+	 */
+	@GET
+	@Path(LOG)
+	@Description("Return the log for the workflow run.")
+	@Produces(TEXT)
+	@Nonnull
+	Response getLogContents();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(LOG)
+	@Description("Return the log for the workflow run.")
+	Response logOptions();
+
+	/**
+	 * @return The log for the workflow run, or empty string if the run has not
+	 *         yet started.
+	 */
+	@GET
+	@Path(RUNBUNDLE)
+	@Description("Return the run bundle for the workflow run.")
+	@Produces(ROBUNDLE)
+	@Nonnull
+	Response getRunBundle();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(RUNBUNDLE)
+	@Description("Return the run bundle for the workflow run.")
+	Response runBundleOptions();
+
+	/**
+	 * @return Whether to create the run bundle for the workflow run. Only
+	 *         usefully set-able before the start of the run.
+	 */
+	@GET
+	@Path(GENERATE_PROVENANCE)
+	@Description("Whether to create the run bundle for the workflow run.")
+	@Produces(TEXT)
+	@Nonnull
+	boolean getGenerateProvenance();
+
+	/**
+	 * @param provenanceFlag
+	 *            Whether to create the run bundle for the workflow run. Only
+	 *            usefully set-able before the start of the run.
+	 * @return What it was actually set to.
+	 * @throws NoUpdateException 
+	 */
+	@PUT
+	@Path(GENERATE_PROVENANCE)
+	@Description("Whether to create the run bundle for the workflow run.")
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Nonnull
+	boolean setGenerateProvenance(boolean provenanceFlag) throws NoUpdateException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(GENERATE_PROVENANCE)
+	@Description("Whether to create the run bundle for the workflow run.")
+	Response generateProvenanceOptions();
+
+	/**
+	 * Factored out path names used in the {@link TavernaServerRunREST}
+	 * interface and related places.
+	 * 
+	 * @author Donal Fellows
+	 */
+	interface PathNames {
+		public static final String ROOT = "/";
+		public static final String WF = "workflow";
+		public static final String DIR = "wd";
+		public static final String NAME = "name";
+		public static final String T_EXPIRE = "expiry";
+		public static final String T_CREATE = "createTime";
+		public static final String T_START = "startTime";
+		public static final String T_FINISH = "finishTime";
+		public static final String STATUS = "status";
+		public static final String IN = "input";
+		public static final String OUT = "output";
+		public static final String PROFILE = "profile";
+		public static final String LISTEN = "listeners";
+		public static final String SEC = "security";
+		public static final String STDOUT = "stdout";
+		public static final String STDERR = "stderr";
+		public static final String USAGE = "usage";
+		public static final String LOG = "log";
+		public static final String RUNBUNDLE = "run-bundle";
+		public static final String GENERATE_PROVENANCE = "generate-provenance";
+	}
+
+	/**
+	 * The description of where everything is in a RESTful view of a workflow
+	 * run. Done with JAXB.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement
+	@XmlType(name = "")
+	public static class RunDescription extends VersionedElement {
+		/** The identity of the owner of the workflow run. */
+		@XmlAttribute(namespace = Namespaces.SERVER_REST)
+		public String owner;
+		/** The description of the expiry. */
+		public Expiry expiry;
+		/** The location of the creation workflow description. */
+		public Uri creationWorkflow;
+		/** The location of the creation time property. */
+		public Uri createTime;
+		/** The location of the start time property. */
+		public Uri startTime;
+		/** The location of the finish time property. */
+		public Uri finishTime;
+		/** The location of the status description. */
+		public Uri status;
+		/** The location of the working directory. */
+		public Uri workingDirectory;
+		/** The location of the inputs. */
+		public Uri inputs;
+		/** The location of the Baclava output. */
+		public Uri output;
+		/** The location of the security context. */
+		public Uri securityContext;
+		/** The list of listeners. */
+		public ListenerList listeners;
+		/** The location of the interaction feed. */
+		public Uri interaction;
+		/** The name of the run. */
+		public Uri name;
+		/** The stdout of the run. */
+		public Uri stdout;
+		/** The stderr of the run. */
+		public Uri stderr;
+		/** The usage record for the run. */
+		public Uri usage;
+		/** The log from the run. */
+		public Uri log;
+		/** The bundle describing the run. */
+		@XmlElement(name = RUNBUNDLE)
+		public Uri runBundle;
+		/** Whether to generate a bundle describing the run. */
+		@XmlElement(name = GENERATE_PROVENANCE)
+		public Uri generateProvenance;
+
+		/**
+		 * How to describe a run's expiry.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "")
+		public static class Expiry {
+			/**
+			 * Where to go to read the exiry
+			 */
+			@XmlAttribute(name = "href", namespace = Namespaces.XLINK)
+			@XmlSchemaType(name = "anyURI")
+			public URI ref;
+			/**
+			 * What the expiry currently is.
+			 */
+			@XmlValue
+			public String timeOfDeath;
+
+			/**
+			 * Make a blank expiry description.
+			 */
+			public Expiry() {
+			}
+
+			private static DateTimeFormatter dtf;
+
+			Expiry(TavernaRun r, UriInfo ui, String path, String... parts) {
+				ref = fromUri(new Uri(ui, true, path, parts).ref).build();
+				if (dtf == null)
+					dtf = basicDateTime();
+				timeOfDeath = dtf.print(r.getExpiry().getTime());
+			}
+		}
+
+		/**
+		 * The description of a list of listeners attached to a run.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "")
+		public static class ListenerList extends Uri {
+			/**
+			 * The references to the individual listeners.
+			 */
+			public List<Uri> listener;
+
+			/**
+			 * An empty description of listeners.
+			 */
+			public ListenerList() {
+				listener = new ArrayList<>();
+			}
+
+			/**
+			 * @param r
+			 *            The run whose listeners we're talking about.
+			 * @param ub
+			 *            Uri factory; must've been secured
+			 */
+			private ListenerList(TavernaRun r, UriBuilder ub) {
+				super(ub);
+				listener = new ArrayList<>(r.getListeners().size());
+				UriBuilder pathUB = ub.clone().path("{name}");
+				for (Listener l : r.getListeners())
+					listener.add(new Uri(pathUB.build(l.getName())));
+			}
+
+			/**
+			 * @param run
+			 *            The run whose listeners we're talking about.
+			 * @param ui
+			 *            The source of information about URIs.
+			 * @param path
+			 *            Where we are relative to the URI source.
+			 * @param parts
+			 *            Anything required to fill out the path.
+			 */
+			ListenerList(TavernaRun run, UriInfo ui, String path,
+					String... parts) {
+				this(run, secure(fromUri(new Uri(ui, path, parts).ref)));
+			}
+		}
+
+		/**
+		 * An empty description of a run.
+		 */
+		public RunDescription() {
+		}
+
+		/**
+		 * A description of a particular run.
+		 * 
+		 * @param run
+		 *            The run to describe.
+		 * @param ui
+		 *            The factory for URIs.
+		 */
+		public RunDescription(TavernaRun run, UriInfo ui) {
+			super(true);
+			creationWorkflow = new Uri(ui, WF);
+			expiry = new Expiry(run, ui, T_EXPIRE);
+			status = new Uri(ui, STATUS);
+			workingDirectory = new Uri(ui, DIR);
+			listeners = new ListenerList(run, ui, LISTEN);
+			securityContext = new Uri(ui, SEC);
+			inputs = new Uri(ui, IN);
+			output = new Uri(ui, OUT);
+			createTime = new Uri(ui, T_CREATE);
+			startTime = new Uri(ui, T_START);
+			finishTime = new Uri(ui, T_FINISH);
+			interaction = new Uri(ui, FEED_URL_DIR);
+			name = new Uri(ui, NAME);
+			owner = run.getSecurityContext().getOwner().getName();
+			stdout = new Uri(ui, STDOUT);
+			stderr = new Uri(ui, STDERR);
+			usage = new Uri(ui, USAGE);
+			log = new Uri(ui, LOG);
+			runBundle = new Uri(ui, RUNBUNDLE);
+			generateProvenance = new Uri(ui, GENERATE_PROVENANCE);
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerSecurityREST.java b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerSecurityREST.java
new file mode 100644
index 0000000..73000e8
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/TavernaServerSecurityREST.java
@@ -0,0 +1,775 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest;
+
+import static java.util.Collections.emptyList;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+import static org.taverna.server.master.common.Roles.USER;
+import static org.taverna.server.master.rest.ContentTypes.JSON;
+import static org.taverna.server.master.rest.ContentTypes.TEXT;
+import static org.taverna.server.master.rest.ContentTypes.XML;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.CREDS;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.ONE_CRED;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.ONE_PERM;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.ONE_TRUST;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.OWNER;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.PERMS;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.ROOT;
+import static org.taverna.server.master.rest.TavernaServerSecurityREST.PathNames.TRUSTS;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.security.RolesAllowed;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElements;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.Permission;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.VersionedElement;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.exceptions.NoCredentialException;
+
+/**
+ * Manages the security of the workflow run. In general, only the owner of a run
+ * may access this resource. Many of these security-related resources may only
+ * be changed before the run is set to operating.
+ * 
+ * @author Donal Fellows
+ */
+@RolesAllowed(USER)
+@Description("Manages the security of the workflow run. In general, only the "
+		+ "owner of a run may access this resource.")
+public interface TavernaServerSecurityREST {
+	interface PathNames {
+		final String ROOT = "/";
+		final String OWNER = "owner";
+		final String CREDS = "credentials";
+		final String ONE_CRED = CREDS + "/{id}";
+		final String TRUSTS = "trusts";
+		final String ONE_TRUST = TRUSTS + "/{id}";
+		final String PERMS = "permissions";
+		final String ONE_PERM = PERMS + "/{id}";
+	}
+
+	/**
+	 * Gets a description of the security information supported by the workflow
+	 * run.
+	 * 
+	 * @param ui
+	 *            About the URI used to access this resource.
+	 * @return A description of the security information.
+	 */
+	@GET
+	@Path(ROOT)
+	@Produces({ XML, JSON })
+	@Description("Gives a description of the security information supported "
+			+ "by the workflow run.")
+	@Nonnull
+	Descriptor describe(@Nonnull @Context UriInfo ui);
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ROOT)
+	@Description("Produces the description of the run security.")
+	Response descriptionOptions();
+
+	/**
+	 * Gets the identity of who owns the workflow run.
+	 * 
+	 * @return The name of the owner of the run.
+	 */
+	@GET
+	@Path(OWNER)
+	@Produces(TEXT)
+	@Description("Gives the identity of who owns the workflow run.")
+	@Nonnull
+	String getOwner();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(OWNER)
+	@Description("Produces the description of the run owner.")
+	Response ownerOptions();
+
+	/*
+	 * @PUT @Path("/") @Consumes(ContentTypes.BYTES) @CallCounted @Nonnull
+	 * public void set(@Nonnull InputStream contents, @Nonnull @Context UriInfo
+	 * ui);
+	 */
+
+	/**
+	 * @return A list of credentials supplied to this workflow run.
+	 */
+	@GET
+	@Path(CREDS)
+	@Produces({ XML, JSON })
+	@Description("Gives a list of credentials supplied to this workflow run.")
+	@Nonnull
+	CredentialList listCredentials();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(CREDS)
+	@Description("Produces the description of the run credentials' operations.")
+	Response credentialsOptions();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ONE_CRED)
+	@Description("Produces the description of one run credential's operations.")
+	Response credentialOptions(@PathParam("id") String id);
+
+	/**
+	 * Describe a particular credential.
+	 * 
+	 * @param id
+	 *            The id of the credential to fetch.
+	 * @return The description of the credential.
+	 * @throws NoCredentialException
+	 *             If the credential doesn't exist.
+	 */
+	@GET
+	@Path(ONE_CRED)
+	@Produces({ XML, JSON })
+	@Description("Describes a particular credential.")
+	@Nonnull
+	CredentialHolder getParticularCredential(@Nonnull @PathParam("id") String id)
+			throws NoCredentialException;
+
+	/**
+	 * Update a particular credential.
+	 * 
+	 * @param id
+	 *            The id of the credential to update.
+	 * @param c
+	 *            The details of the credential to use in the update.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return Description of the updated credential.
+	 * @throws InvalidCredentialException
+	 *             If the credential description isn't valid.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@PUT
+	@Path(ONE_CRED)
+	@Consumes({ XML, JSON })
+	@Produces({ XML, JSON })
+	@Description("Updates a particular credential.")
+	@Nonnull
+	CredentialHolder setParticularCredential(
+			@Nonnull @PathParam("id") String id, @Nonnull CredentialHolder c,
+			@Nonnull @Context UriInfo ui) throws InvalidCredentialException,
+			BadStateChangeException;
+
+	/**
+	 * Adds a new credential.
+	 * 
+	 * @param c
+	 *            The details of the credential to create.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return Description of the created credential.
+	 * @throws InvalidCredentialException
+	 *             If the credential description isn't valid.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@POST
+	@Path(CREDS)
+	@Consumes({ XML, JSON })
+	@Description("Creates a new credential.")
+	@Nonnull
+	Response addCredential(@Nonnull CredentialHolder c,
+			@Nonnull @Context UriInfo ui) throws InvalidCredentialException,
+			BadStateChangeException;
+
+	/**
+	 * Deletes all credentials associated with a run.
+	 * 
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return A characterisation of a successful delete.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@DELETE
+	@Path(CREDS)
+	@Description("Deletes all credentials.")
+	@Nonnull
+	Response deleteAllCredentials(@Nonnull @Context UriInfo ui)
+			throws BadStateChangeException;
+
+	/**
+	 * Deletes one credential associated with a run.
+	 * 
+	 * @param id
+	 *            The identity of the credential to delete.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return A characterisation of a successful delete.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@DELETE
+	@Path(ONE_CRED)
+	@Description("Deletes a particular credential.")
+	@Nonnull
+	Response deleteCredential(@Nonnull @PathParam("id") String id,
+			@Nonnull @Context UriInfo ui) throws BadStateChangeException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(TRUSTS)
+	@Description("Produces the description of the run trusted certificates' "
+			+ "operations.")
+	Response trustsOptions();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ONE_TRUST)
+	@Description("Produces the description of one run trusted certificate's "
+			+ "operations.")
+	Response trustOptions(@PathParam("id") String id);
+
+	/**
+	 * @return A list of trusted identities supplied to this workflow run.
+	 */
+	@GET
+	@Path(TRUSTS)
+	@Produces({ XML, JSON })
+	@Description("Gives a list of trusted identities supplied to this "
+			+ "workflow run.")
+	@Nonnull
+	TrustList listTrusted();
+
+	/**
+	 * Describe a particular trusted identity.
+	 * 
+	 * @param id
+	 *            The id of the trusted identity to fetch.
+	 * @return The description of the trusted identity.
+	 * @throws NoCredentialException
+	 *             If the trusted identity doesn't exist.
+	 */
+	@GET
+	@Path(ONE_TRUST)
+	@Produces({ XML, JSON })
+	@Description("Describes a particular trusted identity.")
+	@Nonnull
+	Trust getParticularTrust(@Nonnull @PathParam("id") String id)
+			throws NoCredentialException;
+
+	/**
+	 * Update a particular trusted identity.
+	 * 
+	 * @param id
+	 *            The id of the trusted identity to update.
+	 * @param t
+	 *            The details of the trusted identity to use in the update.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return Description of the updated trusted identity.
+	 * @throws InvalidCredentialException
+	 *             If the trusted identity description isn't valid.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@PUT
+	@Path(ONE_TRUST)
+	@Consumes({ XML, JSON })
+	@Produces({ XML, JSON })
+	@Description("Updates a particular trusted identity.")
+	@Nonnull
+	Trust setParticularTrust(@Nonnull @PathParam("id") String id,
+			@Nonnull Trust t, @Nonnull @Context UriInfo ui)
+			throws InvalidCredentialException, BadStateChangeException;
+
+	/**
+	 * Adds a new trusted identity.
+	 * 
+	 * @param t
+	 *            The details of the trusted identity to create.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return Description of the created trusted identity.
+	 * @throws InvalidCredentialException
+	 *             If the trusted identity description isn't valid.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@POST
+	@Path(TRUSTS)
+	@Consumes({ XML, JSON })
+	@Description("Adds a new trusted identity.")
+	@Nonnull
+	Response addTrust(@Nonnull Trust t, @Nonnull @Context UriInfo ui)
+			throws InvalidCredentialException, BadStateChangeException;
+
+	/**
+	 * Deletes all trusted identities associated with a run.
+	 * 
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return A characterisation of a successful delete.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@DELETE
+	@Path(TRUSTS)
+	@Description("Deletes all trusted identities.")
+	@Nonnull
+	Response deleteAllTrusts(@Nonnull @Context UriInfo ui)
+			throws BadStateChangeException;
+
+	/**
+	 * Deletes one trusted identity associated with a run.
+	 * 
+	 * @param id
+	 *            The identity of the trusted identity to delete.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return A characterisation of a successful delete.
+	 * @throws BadStateChangeException
+	 *             If the workflow run is not in the initialising state.
+	 */
+	@DELETE
+	@Path(ONE_TRUST)
+	@Description("Deletes a particular trusted identity.")
+	@Nonnull
+	Response deleteTrust(@Nonnull @PathParam("id") String id,
+			@Nonnull @Context UriInfo ui) throws BadStateChangeException;
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(PERMS)
+	@Description("Produces the description of the run permissions' operations.")
+	Response permissionsOptions();
+
+	/** Get an outline of the operations supported. */
+	@OPTIONS
+	@Path(ONE_PERM)
+	@Description("Produces the description of one run permission's operations.")
+	Response permissionOptions(@PathParam("id") String id);
+
+	/**
+	 * @return A list of (non-default) permissions associated with this workflow
+	 *         run.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 */
+	@GET
+	@Path(PERMS)
+	@Produces({ XML, JSON })
+	@Description("Gives a list of all non-default permissions associated with "
+			+ "the enclosing workflow run. By default, nobody has any access "
+			+ "at all except for the owner of the run.")
+	@Nonnull
+	PermissionsDescription describePermissions(@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Describe the particular permission granted to a user.
+	 * 
+	 * @param id
+	 *            The name of the user whose permissions are to be described.
+	 * @return The permission they are granted.
+	 */
+	@GET
+	@Path(ONE_PERM)
+	@Produces(TEXT)
+	@Description("Describes the permission granted to a particular user.")
+	@Nonnull
+	Permission describePermission(@Nonnull @PathParam("id") String id);
+
+	/**
+	 * Update the permission granted to a user.
+	 * 
+	 * @param id
+	 *            The name of the user whose permissions are to be updated. Note
+	 *            that the owner always has full permissions.
+	 * @param perm
+	 *            The permission level to set.
+	 * @return The permission level that has actually been set.
+	 */
+	@PUT
+	@Consumes(TEXT)
+	@Produces(TEXT)
+	@Path(ONE_PERM)
+	@Description("Updates the permissions granted to a particular user.")
+	@Nonnull
+	Permission setPermission(@Nonnull @PathParam("id") String id,
+			@Nonnull Permission perm);
+
+	/**
+	 * Delete the permissions associated with a user, which restores them to the
+	 * default (no access unless they are the owner or have admin privileges).
+	 * 
+	 * @param id
+	 *            The name of the user whose permissions are to be revoked.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return An indication that the delete has been successful (or not).
+	 */
+	@DELETE
+	@Path(ONE_PERM)
+	@Description("Deletes (by resetting to default) the permissions "
+			+ "associated with a particular user.")
+	@Nonnull
+	Response deletePermission(@Nonnull @PathParam("id") String id,
+			@Nonnull @Context UriInfo ui);
+
+	/**
+	 * Manufacture a permission setting for a previously-unknown user.
+	 * 
+	 * @param desc
+	 *            A description of the name of the user and the permission level
+	 *            to grant them.
+	 * @param ui
+	 *            Information about the URI used to access this resource.
+	 * @return An indication that the create has been successful (or not).
+	 */
+	@POST
+	@Path(PERMS)
+	@Consumes({ XML, JSON })
+	@Description("Creates a new assignment of permissions to a particular user.")
+	@Nonnull
+	Response makePermission(@Nonnull PermissionDescription desc,
+			@Nonnull @Context UriInfo ui);
+
+	/**
+	 * A description of the security resources associated with a workflow run.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "securityDescriptor")
+	@XmlType(name = "SecurityDescriptor")
+	public static final class Descriptor extends VersionedElement {
+		/** The identity of the owner of the enclosing workflow run. */
+		@XmlElement
+		public String owner;
+		/** Where to get the permissions on the run. */
+		@XmlElement
+		public Uri permissions;
+
+		/** Characterisation of the credentials attached to the run. */
+		@XmlElement
+		public Credentials credentials;
+		/** Characterisation of the trusted certificates attached to the run. */
+		@XmlElement
+		public Trusts trusts;
+
+		public Descriptor() {
+		}
+
+		/**
+		 * Initialise a description of the security context.
+		 * 
+		 * @param ub
+		 *            How to build URIs.
+		 * @param owner
+		 *            Who owns the context.
+		 * @param credential
+		 *            The credentials associated with the context.
+		 * @param trust
+		 *            The trusted certificates associated with the context.
+		 */
+		public Descriptor(@Nonnull UriBuilder ub, @Nonnull String owner,
+				@Nonnull Credential[] credential, @Nonnull Trust[] trust) {
+			super(true);
+			this.owner = owner;
+			this.permissions = new Uri(ub, PERMS);
+			this.credentials = new Credentials(new Uri(ub, CREDS).ref,
+					credential);
+			this.trusts = new Trusts(new Uri(ub, TRUSTS).ref, trust);
+		}
+
+		/**
+		 * A description of credentials associated with a workflow run.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "CredentialCollection")
+		public static final class Credentials {
+			/** Reference to the collection of credentials */
+			@XmlAttribute(name = "href", namespace = XLINK)
+			@XmlSchemaType(name = "anyURI")
+			public URI href;
+			/** Descriptions of the credentials themselves. */
+			@XmlElement
+			public List<CredentialHolder> credential = new ArrayList<>();
+
+			public Credentials() {
+			}
+
+			/**
+			 * Initialise a description of the credentials.
+			 * 
+			 * @param uri
+			 *            the URI of the collection.
+			 * @param credential
+			 *            The credentials in the collection.
+			 */
+			public Credentials(@Nonnull URI uri,
+					@Nonnull Credential[] credential) {
+				this.href = uri;
+				for (Credential c : credential)
+					this.credential.add(new CredentialHolder(c));
+			}
+		}
+
+		/**
+		 * A description of trusted certificates associated with a workflow run.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlType(name = "TrustCollection")
+		public static final class Trusts {
+			/** Reference to the collection of trusted certs */
+			@XmlAttribute(name = "href", namespace = XLINK)
+			@XmlSchemaType(name = "anyURI")
+			public URI href;
+			/** Descriptions of the trusted certs themselves. */
+			@XmlElement
+			public Trust[] trust;
+
+			public Trusts() {
+			}
+
+			/**
+			 * Initialise a description of the trusted certificates.
+			 * 
+			 * @param uri
+			 *            the URI of the collection.
+			 * @param trust
+			 *            The trusted certificates in the collection.
+			 */
+			public Trusts(@Nonnull URI uri, @Nonnull Trust[] trust) {
+				this.href = uri;
+				this.trust = trust.clone();
+			}
+		}
+	}
+
+	/**
+	 * A container for a credential, used to work around issues with type
+	 * inference in CXF's REST service handling and JAXB.
+	 * 
+	 * @see Credential.KeyPair
+	 * @see Credential.Password
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "credential")
+	@XmlType(name = "Credential")
+	public static final class CredentialHolder {
+		/**
+		 * The credential inside this holder.
+		 */
+		@XmlElements({
+				@XmlElement(name = "keypair", namespace = SERVER, type = Credential.KeyPair.class, required = true),
+				@XmlElement(name = "userpass", namespace = SERVER, type = Credential.Password.class, required = true) })
+		public Credential credential;
+
+		public CredentialHolder() {
+		}
+
+		public CredentialHolder(Credential credential) {
+			this.credential = credential;
+		}
+
+		/**
+		 * Convenience accessor function.
+		 * 
+		 * @return The keypair credential held in this holder.
+		 */
+		@XmlTransient
+		public Credential.KeyPair getKeypair() {
+			return (Credential.KeyPair) this.credential;
+		}
+
+		/**
+		 * Convenience accessor function.
+		 * 
+		 * @return The userpass credential held in this holder.
+		 */
+		@XmlTransient
+		public Credential.Password getUserpass() {
+			return (Credential.Password) this.credential;
+		}
+	}
+
+	/**
+	 * A simple list of credential descriptions.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "credentials")
+	public static final class CredentialList extends VersionedElement {
+		/** The descriptions of the credentials */
+		@XmlElement
+		@Nonnull
+		public List<CredentialHolder> credential = new ArrayList<>();
+
+		public CredentialList() {
+		}
+
+		/**
+		 * Initialise the list of credentials.
+		 * 
+		 * @param credential
+		 *            The descriptions of individual credentials.
+		 */
+		public CredentialList(@Nonnull Credential[] credential) {
+			super(true);
+			for (Credential c : credential)
+				this.credential.add(new CredentialHolder(c));
+		}
+	}
+
+	/**
+	 * A simple list of trusted certificate descriptions.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "trustedIdentities")
+	public static final class TrustList extends VersionedElement {
+		/** The descriptions of the trusted certificates */
+		@XmlElement
+		public Trust[] trust;
+
+		public TrustList() {
+		}
+
+		/**
+		 * Initialise the list of trusted certificates.
+		 * 
+		 * @param trust
+		 *            The descriptions of individual certificates.
+		 */
+		public TrustList(@Nonnull Trust[] trust) {
+			super(true);
+			this.trust = trust.clone();
+		}
+	}
+
+	/**
+	 * A description of the permissions granted to others by the owner of a
+	 * workflow run.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "permissionsDescriptor")
+	public static class PermissionsDescription extends VersionedElement {
+		/**
+		 * A description of the permissions granted to one user by the owner of
+		 * a workflow run.
+		 * 
+		 * @author Donal Fellows
+		 */
+		@XmlRootElement(name = "userPermission")
+		public static class LinkedPermissionDescription extends Uri {
+			/** Who is this granted to? */
+			@XmlElement
+			public String userName;
+			/** What are they granted? */
+			@XmlElement
+			public Permission permission;
+
+			public LinkedPermissionDescription() {
+			}
+
+			/**
+			 * Initialise a description of one user's permissions.
+			 * 
+			 * @param ub
+			 *            How to build the URI to this permission. Already
+			 *            secured.
+			 * @param userName
+			 *            Who this relates to.
+			 * @param permission
+			 *            What permission is granted.
+			 * @param strings
+			 *            Parameters to the URI builder.
+			 */
+			LinkedPermissionDescription(@Nonnull UriBuilder ub,
+					@Nonnull String userName, @Nonnull Permission permission,
+					String... strings) {
+				super(ub, strings);
+				this.userName = userName;
+				this.permission = permission;
+			}
+		}
+
+		/** List of descriptions of permissions. */
+		@XmlElement
+		public List<LinkedPermissionDescription> permission;
+
+		public PermissionsDescription() {
+			permission = emptyList();
+		}
+
+		/**
+		 * Initialise the description of a collection of permissions.
+		 * 
+		 * @param ub
+		 *            How to build URIs to this collection. Must have already
+		 *            been secured.
+		 * @param permissionMap
+		 *            The permissions to describe.
+		 */
+		public PermissionsDescription(@Nonnull UriBuilder ub,
+				@Nonnull Map<String, Permission> permissionMap) {
+			permission = new ArrayList<>();
+			List<String> userNames = new ArrayList<>(permissionMap.keySet());
+			Collections.sort(userNames);
+			for (String user : userNames)
+				permission.add(new LinkedPermissionDescription(ub, user,
+						permissionMap.get(user), user));
+		}
+	}
+
+	/**
+	 * An instruction to update the permissions for a user.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlRootElement(name = "permissionUpdate")
+	public static class PermissionDescription {
+		/** Who to set the permission for? */
+		@XmlElement
+		public String userName;
+		/** What permission to grant them? */
+		@XmlElement
+		public Permission permission;
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/AccessDeniedHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/AccessDeniedHandler.java
new file mode 100644
index 0000000..8b9c75f
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/AccessDeniedHandler.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+import org.springframework.security.access.AccessDeniedException;
+
+public class AccessDeniedHandler extends HandlerCore implements
+		ExceptionMapper<AccessDeniedException> {
+	@Override
+	public Response toResponse(AccessDeniedException exception) {
+		return respond(FORBIDDEN, exception);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadInputPortNameHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadInputPortNameHandler.java
new file mode 100644
index 0000000..254382b
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadInputPortNameHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.BadInputPortNameException;
+
+@Provider
+public class BadInputPortNameHandler extends HandlerCore implements
+		ExceptionMapper<BadInputPortNameException> {
+	@Override
+	public Response toResponse(BadInputPortNameException exn) {
+		return respond(NOT_FOUND, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadPropertyValueHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadPropertyValueHandler.java
new file mode 100644
index 0000000..6ded568
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadPropertyValueHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+
+@Provider
+public class BadPropertyValueHandler extends HandlerCore implements
+		ExceptionMapper<BadPropertyValueException> {
+	@Override
+	public Response toResponse(BadPropertyValueException exn) {
+		return respond(BAD_REQUEST, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadStateChangeHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadStateChangeHandler.java
new file mode 100644
index 0000000..9501923
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/BadStateChangeHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.BadStateChangeException;
+
+@Provider
+public class BadStateChangeHandler extends HandlerCore implements
+		ExceptionMapper<BadStateChangeException> {
+	@Override
+	public Response toResponse(BadStateChangeException exn) {
+		return respond(BAD_REQUEST, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/EntryHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/EntryHandler.java
new file mode 100644
index 0000000..20d9f56
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/EntryHandler.java
@@ -0,0 +1,131 @@
+package org.taverna.server.master.rest.handler;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonMap;
+import static javax.ws.rs.core.Response.notAcceptable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Variant;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.model.Document;
+import org.apache.abdera.model.Entry;
+import org.apache.abdera.parser.Parser;
+import org.apache.abdera.writer.Writer;
+import org.springframework.beans.factory.annotation.Required;
+
+@Provider
+@Produces({ "application/atom+xml", "application/atom+xml;type=entry" })
+@Consumes({ "application/atom+xml", "application/atom+xml;type=entry" })
+public class EntryHandler implements MessageBodyWriter<Entry>,
+		MessageBodyReader<Entry> {
+	private static final String ENC = "UTF-8";
+	private static final MediaType ENTRY = new MediaType("application",
+			"atom+xml", singletonMap("type", "entry"));
+	private static final Variant VARIANT = new Variant(ENTRY, (String) null,
+			ENC);
+	private static final Charset UTF8 = Charset.forName(ENC);
+
+	@Required
+	public void setAbdera(Abdera abdera) {
+		parser = abdera.getParser();
+		writer = abdera.getWriterFactory().getWriter("prettyxml");
+	}
+
+	private Parser parser;
+	private Writer writer;
+
+	@Override
+	public boolean isReadable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (!Entry.class.isAssignableFrom(type))
+			return false;
+		if (!ENTRY.isCompatible(mediaType))
+			return false;
+		if (mediaType.getParameters().containsKey("type"))
+			return "entry".equalsIgnoreCase(mediaType.getParameters().get(
+					"type"));
+		return true;
+	}
+
+	@Override
+	public Entry readFrom(Class<Entry> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+			throws IOException, WebApplicationException {
+		Charset cs = UTF8;
+		try {
+			String charset = mediaType.getParameters().get("charset");
+			if (charset != null)
+				cs = Charset.forName(charset);
+		} catch (IllegalCharsetNameException e) {
+			throw new WebApplicationException(notAcceptable(asList(VARIANT))
+					.entity("bad charset name").build());
+		} catch (UnsupportedCharsetException e) {
+			throw new WebApplicationException(notAcceptable(asList(VARIANT))
+					.entity("unsupportd charset name").build());
+		}
+		try {
+			Document<Entry> doc = parser.parse(new InputStreamReader(
+					entityStream, cs));
+			if (!Entry.class.isAssignableFrom(doc.getRoot().getClass())) {
+				throw new WebApplicationException(
+						notAcceptable(asList(VARIANT)).entity(
+								"not really a feed entry").build());
+			}
+			return doc.getRoot();
+		} catch (ClassCastException e) {
+			throw new WebApplicationException(notAcceptable(asList(VARIANT))
+					.entity("not really a feed entry").build());
+
+		}
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (!Entry.class.isAssignableFrom(type))
+			return false;
+		if (!ENTRY.isCompatible(mediaType))
+			return false;
+		if (mediaType.getParameters().containsKey("type"))
+			return "entry".equalsIgnoreCase(mediaType.getParameters().get(
+					"type"));
+		return true;
+	}
+
+	@Override
+	public long getSize(Entry t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return -1;
+	}
+
+	@Override
+	public void writeTo(Entry t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		httpHeaders.putSingle("Content-Type", ENTRY.toString() + ";charset="
+				+ ENC);
+		writer.writeTo(t, new OutputStreamWriter(entityStream, UTF8));
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FeedHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FeedHandler.java
new file mode 100644
index 0000000..7c17f5c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FeedHandler.java
@@ -0,0 +1,66 @@
+package org.taverna.server.master.rest.handler;
+
+import static java.util.Collections.singletonMap;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.abdera.Abdera;
+import org.apache.abdera.model.Feed;
+import org.apache.abdera.writer.Writer;
+import org.springframework.beans.factory.annotation.Required;
+
+@Provider
+@Produces({ "application/atom+xml", "application/atom+xml;type=feed" })
+public class FeedHandler implements MessageBodyWriter<Feed> {
+	private static final MediaType FEED = new MediaType("application",
+			"atom+xml", singletonMap("type", "feed"));
+	private static final String ENC = "UTF-8";
+
+	@Required
+	public void setAbdera(Abdera abdera) {
+		writer = abdera.getWriterFactory().getWriter("prettyxml");
+	}
+
+	private Writer writer;
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (!Feed.class.isAssignableFrom(type))
+			return false;
+		if (!FEED.isCompatible(mediaType))
+			return false;
+		if (mediaType.getParameters().containsKey("type"))
+			return "feed".equalsIgnoreCase(mediaType.getParameters()
+					.get("type"));
+		return true;
+	}
+
+	@Override
+	public long getSize(Feed t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return -1;
+	}
+
+	@Override
+	public void writeTo(Feed t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		httpHeaders.putSingle("Content-Type", FEED.toString() + ";charset="
+				+ ENC);
+		writer.writeTo(t, new OutputStreamWriter(entityStream, ENC));
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileConcatenationHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileConcatenationHandler.java
new file mode 100644
index 0000000..ab18d73
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileConcatenationHandler.java
@@ -0,0 +1,61 @@
+package org.taverna.server.master.rest.handler;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.FileConcatenation;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+public class FileConcatenationHandler implements
+		MessageBodyWriter<FileConcatenation> {
+	/** How much to pull from the worker in one read. */
+	private int maxChunkSize;
+
+	/**
+	 * @param maxChunkSize
+	 *            How much to pull from the worker in one read.
+	 */
+	@Required
+	public void setMaxChunkSize(int maxChunkSize) {
+		this.maxChunkSize = maxChunkSize;
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return type.isAssignableFrom(FileConcatenation.class);
+	}
+
+	@Override
+	public long getSize(FileConcatenation fc, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return fc.size();
+	}
+
+	@Override
+	public void writeTo(FileConcatenation fc, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException {
+		for (File f : fc)
+			try {
+				byte[] buffer;
+				for (int off = 0; true ; off += buffer.length) {
+					buffer = f.getContents(off, maxChunkSize);
+					if (buffer == null || buffer.length == 0)
+						break;
+					entityStream.write(buffer);
+				}
+			} catch (FilesystemAccessException e) {
+				// Ignore/skip to next file
+			}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileMessageHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileMessageHandler.java
new file mode 100644
index 0000000..0aeb816
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileMessageHandler.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.commons.logging.Log;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+/**
+ * How to write out a File object with JAX-RS.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+public class FileMessageHandler implements MessageBodyWriter<File> {
+	private Log log = getLog("Taverna.Server.Webapp");
+	/** How much to pull from the worker in one read. */
+	private int maxChunkSize;
+
+	/**
+	 * @param maxChunkSize
+	 *            How much to pull from the worker in one read.
+	 */
+	public void setMaxChunkSize(int maxChunkSize) {
+		this.maxChunkSize = maxChunkSize;
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return File.class.isAssignableFrom(type);
+	}
+
+	@Override
+	public long getSize(File t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		try {
+			return t.getSize(); // Is it really raw bytes?
+		} catch (FilesystemAccessException e) {
+			log.info("failed to get file length", e);
+			return -1;
+		}
+	}
+
+	@Override
+	public void writeTo(File t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		try {
+			int off = 0;
+			while (true) {
+				byte[] buffer = t.getContents(off, maxChunkSize);
+				if (buffer == null || buffer.length == 0)
+					break;
+				entityStream.write(buffer);
+				off += buffer.length;
+			}
+		} catch (FilesystemAccessException e) {
+			throw new IOException("problem when reading file", e);
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileSegmentHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileSegmentHandler.java
new file mode 100644
index 0000000..f387cf6
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FileSegmentHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static java.lang.Math.min;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.rest.FileSegment;
+
+/**
+ * How to write out a segment of a file with JAX-RS.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+public class FileSegmentHandler implements MessageBodyWriter<FileSegment> {
+	/** How much to pull from the worker in one read. */
+	private int maxChunkSize;
+
+	/**
+	 * @param maxChunkSize
+	 *            How much to pull from the worker in one read.
+	 */
+	public void setMaxChunkSize(int maxChunkSize) {
+		this.maxChunkSize = maxChunkSize;
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return FileSegment.class.isAssignableFrom(type);
+	}
+
+	@Override
+	public long getSize(FileSegment t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return t.to - t.from;
+	}
+
+	@Override
+	public void writeTo(FileSegment t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		try {
+			int off = t.from;
+			while (off < t.to) {
+				byte[] buffer = t.file.getContents(off,
+						min(maxChunkSize, t.to - off));
+				if (buffer == null || buffer.length == 0)
+					break;
+				entityStream.write(buffer);
+				off += buffer.length;
+			}
+		} catch (FilesystemAccessException e) {
+			throw new IOException("problem when reading file", e);
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FilesystemAccessHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FilesystemAccessHandler.java
new file mode 100644
index 0000000..12c137e
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/FilesystemAccessHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+
+@Provider
+public class FilesystemAccessHandler extends HandlerCore implements
+		ExceptionMapper<FilesystemAccessException> {
+	@Override
+	public Response toResponse(FilesystemAccessException exn) {
+		return respond(FORBIDDEN, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/GeneralFailureHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/GeneralFailureHandler.java
new file mode 100644
index 0000000..775805b
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/GeneralFailureHandler.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+import org.taverna.server.master.exceptions.GeneralFailureException;
+
+public class GeneralFailureHandler extends HandlerCore implements
+		ExceptionMapper<GeneralFailureException> {
+	@Override
+	public Response toResponse(GeneralFailureException exception) {
+		return respond(INTERNAL_SERVER_ERROR, exception);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/HandlerCore.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/HandlerCore.java
new file mode 100644
index 0000000..0e3fb51
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/HandlerCore.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
+import static javax.ws.rs.core.Response.status;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import javax.ws.rs.core.Response;
+
+import org.apache.commons.logging.Log;
+import org.taverna.server.master.api.ManagementModel;
+
+/**
+ * Base class for handlers that grants Spring-enabled access to the management
+ * model.
+ * 
+ * @author Donal Fellows
+ */
+public class HandlerCore {
+	private Log log = getLog("Taverna.Server.Webapp");
+	private ManagementModel managementModel;
+
+	/**
+	 * @param managementModel
+	 *            the managementModel to set
+	 */
+	public void setManagementModel(ManagementModel managementModel) {
+		this.managementModel = managementModel;
+	}
+
+	/**
+	 * Simplified interface for building responses.
+	 * 
+	 * @param status
+	 *            What status code to use?
+	 * @param exception
+	 *            What exception to report on?
+	 * @return The build response.
+	 */
+	protected Response respond(Response.Status status, Exception exception) {
+		if (managementModel.getLogOutgoingExceptions()
+				|| status.getStatusCode() >= 500)
+			log.info("converting exception to response", exception);
+		return status(status).type(TEXT_PLAIN_TYPE)
+				.entity(exception.getMessage()).build();
+	}
+
+	/**
+	 * Simplified interface for building responses.
+	 * 
+	 * @param status
+	 *            What status code to use?
+	 * @param partialMessage
+	 *            The prefix to the message.
+	 * @param exception
+	 *            What exception to report on?
+	 * @return The build response.
+	 */
+	protected Response respond(Response.Status status, String partialMessage,
+			Exception exception) {
+		if (managementModel.getLogOutgoingExceptions()
+				|| status.getStatusCode() >= 500)
+			log.info("converting exception to response", exception);
+		return status(status).type(TEXT_PLAIN_TYPE)
+				.entity(partialMessage + "\n" + exception.getMessage()).build();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/IllegalArgumentHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/IllegalArgumentHandler.java
new file mode 100644
index 0000000..ac02015
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/IllegalArgumentHandler.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Provider
+public class IllegalArgumentHandler extends HandlerCore implements
+		ExceptionMapper<IllegalArgumentException> {
+	@Override
+	public Response toResponse(IllegalArgumentException exn) {
+		return respond(UNSUPPORTED_MEDIA_TYPE, exn);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/ImplementationProblemHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/ImplementationProblemHandler.java
new file mode 100644
index 0000000..1458667
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/ImplementationProblemHandler.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+import org.taverna.server.localworker.remote.ImplementationException;
+
+public class ImplementationProblemHandler extends HandlerCore implements
+		ExceptionMapper<ImplementationException> {
+	@Override
+	public Response toResponse(ImplementationException exception) {
+		return respond(INTERNAL_SERVER_ERROR, exception);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/InputStreamMessageHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/InputStreamMessageHandler.java
new file mode 100644
index 0000000..2c0c092
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/InputStreamMessageHandler.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static java.lang.Long.parseLong;
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.commons.logging.Log;
+
+/**
+ * Maps a stream from a client into a bounded ordinary input stream that the
+ * webapp can work with more easily.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+@Consumes(APPLICATION_OCTET_STREAM)
+public class InputStreamMessageHandler implements
+		MessageBodyReader<InputStream> {
+	@Override
+	public boolean isReadable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return InputStream.class.isAssignableFrom(type);
+	}
+
+	@Override
+	public InputStream readFrom(Class<InputStream> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+			throws IOException, WebApplicationException {
+		return new TransferStream(entityStream,
+				httpHeaders.get("Content-Length"));
+	}
+}
+
+/**
+ * The actual transfer thunk.
+ * 
+ * @author Donal Fellows
+ */
+class TransferStream extends InputStream {
+	private Log log = getLog("Taverna.Server.Handlers");
+
+	public TransferStream(InputStream entityStream, List<String> contentLength) {
+		this.entityStream = new BufferedInputStream(entityStream);
+		if (contentLength != null && contentLength.size() > 0) {
+			this.limit = parseLong(contentLength.get(0));
+			if (log.isDebugEnabled())
+				log.debug("will attempt to transfer " + this.limit + " bytes");
+		} else {
+			this.limit = -1;
+			if (log.isDebugEnabled())
+				log.debug("will attempt to transfer until EOF");
+		}
+	}
+
+	InputStream entityStream;
+	long limit;
+	long doneBytes = 0;
+
+	@Override
+	public int read() throws IOException {
+		if (limit >= 0 && doneBytes >= limit)
+			return -1;
+		int result = entityStream.read();
+		if (result >= 0)
+			doneBytes++;
+		return result;
+	}
+
+	@Override
+	public int read(byte[] ary, int off, int len) throws IOException {
+		if (limit >= 0) {
+			if (doneBytes >= limit)
+				return -1;
+			if (doneBytes + len > limit)
+				len = (int) (limit - doneBytes);
+		}
+		int readBytes = entityStream.read(ary, off, len);
+		if (readBytes >= 0)
+			doneBytes += readBytes;
+		return readBytes;
+	}
+
+	@Override
+	public void close() throws IOException {
+		entityStream.close();
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/InvalidCredentialHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/InvalidCredentialHandler.java
new file mode 100644
index 0000000..fe11de8
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/InvalidCredentialHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+
+@Provider
+public class InvalidCredentialHandler extends HandlerCore implements
+		ExceptionMapper<InvalidCredentialException> {
+	@Override
+	public Response toResponse(InvalidCredentialException exn) {
+		return respond(BAD_REQUEST, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/JAXBExceptionHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/JAXBExceptionHandler.java
new file mode 100644
index 0000000..33ac6a0
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/JAXBExceptionHandler.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import javax.xml.bind.JAXBException;
+
+@Provider
+public class JAXBExceptionHandler extends HandlerCore implements
+		ExceptionMapper<JAXBException> {
+	@Override
+	public Response toResponse(JAXBException exn) {
+		return respond(BAD_REQUEST, "APIEpicFail: " + exn.getErrorCode(), exn);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NegotiationFailedHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NegotiationFailedHandler.java
new file mode 100644
index 0000000..47153e7
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NegotiationFailedHandler.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN;
+import static javax.ws.rs.core.Response.notAcceptable;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.rest.TavernaServerDirectoryREST.NegotiationFailedException;
+
+@Provider
+public class NegotiationFailedHandler implements
+		ExceptionMapper<NegotiationFailedException> {
+	@Override
+	public Response toResponse(NegotiationFailedException exn) {
+		return notAcceptable(exn.accepted).type(TEXT_PLAIN)
+				.entity(exn.getMessage()).build();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoCreateHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoCreateHandler.java
new file mode 100644
index 0000000..e4215a1
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoCreateHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.NoCreateException;
+
+@Provider
+public class NoCreateHandler extends HandlerCore implements
+		ExceptionMapper<NoCreateException> {
+	@Override
+	public Response toResponse(NoCreateException exn) {
+		return respond(SERVICE_UNAVAILABLE, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoCredentialHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoCredentialHandler.java
new file mode 100644
index 0000000..d81f6ba
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoCredentialHandler.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+import org.taverna.server.master.exceptions.NoCredentialException;
+
+public class NoCredentialHandler extends HandlerCore implements
+		ExceptionMapper<NoCredentialException> {
+	@Override
+	public Response toResponse(NoCredentialException exn) {
+		return respond(NOT_FOUND, exn);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoDestroyHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoDestroyHandler.java
new file mode 100644
index 0000000..927af4b
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoDestroyHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.NoDestroyException;
+
+@Provider
+public class NoDestroyHandler extends HandlerCore implements
+		ExceptionMapper<NoDestroyException> {
+	@Override
+	public Response toResponse(NoDestroyException exn) {
+		return respond(FORBIDDEN, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoDirectoryEntryHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoDirectoryEntryHandler.java
new file mode 100644
index 0000000..ab2e54d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoDirectoryEntryHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+
+@Provider
+public class NoDirectoryEntryHandler extends HandlerCore implements
+		ExceptionMapper<NoDirectoryEntryException> {
+	@Override
+	public Response toResponse(NoDirectoryEntryException exn) {
+		return respond(NOT_FOUND, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoListenerHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoListenerHandler.java
new file mode 100644
index 0000000..36e3053
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoListenerHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.NoListenerException;
+
+@Provider
+public class NoListenerHandler extends HandlerCore implements
+		ExceptionMapper<NoListenerException> {
+	@Override
+	public Response toResponse(NoListenerException exn) {
+		return respond(BAD_REQUEST, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoUpdateHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoUpdateHandler.java
new file mode 100644
index 0000000..61a89d4
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NoUpdateHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.NoUpdateException;
+
+@Provider
+public class NoUpdateHandler extends HandlerCore implements
+		ExceptionMapper<NoUpdateException> {
+	@Override
+	public Response toResponse(NoUpdateException exn) {
+		return respond(FORBIDDEN, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NotOwnerHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NotOwnerHandler.java
new file mode 100644
index 0000000..44de871
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/NotOwnerHandler.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+
+import org.taverna.server.master.exceptions.NotOwnerException;
+
+public class NotOwnerHandler extends HandlerCore implements
+		ExceptionMapper<NotOwnerException> {
+	@Override
+	public Response toResponse(NotOwnerException exn) {
+		return respond(FORBIDDEN, exn);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/OverloadedHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/OverloadedHandler.java
new file mode 100644
index 0000000..21e5e68
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/OverloadedHandler.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.SERVICE_UNAVAILABLE;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.OverloadedException;
+
+@Provider
+public class OverloadedHandler extends HandlerCore implements
+		ExceptionMapper<OverloadedException> {
+	@Override
+	public Response toResponse(OverloadedException exn) {
+		return respond(SERVICE_UNAVAILABLE, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/PermissionHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/PermissionHandler.java
new file mode 100644
index 0000000..03a4dd4
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/PermissionHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+
+import org.taverna.server.master.common.Permission;
+
+/**
+ * Handler that allows CXF to send and receive {@linkplain Permission
+ * permissions} as plain text directly.
+ * 
+ * @author Donal Fellows
+ */
+public class PermissionHandler implements MessageBodyReader<Permission>,
+		MessageBodyWriter<Permission> {
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return type.isAssignableFrom(Permission.class)
+				&& mediaType.isCompatible(TEXT_PLAIN_TYPE);
+	}
+
+	@Override
+	public long getSize(Permission t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return t.toString().length();
+	}
+
+	@Override
+	public void writeTo(Permission t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		new OutputStreamWriter(entityStream).write(t.toString());
+	}
+
+	@Override
+	public boolean isReadable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return type.isAssignableFrom(Permission.class)
+				&& mediaType.isCompatible(TEXT_PLAIN_TYPE);
+	}
+
+	@Override
+	public Permission readFrom(Class<Permission> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+			throws IOException, WebApplicationException {
+		char[] cbuf = new char[7];
+		int len = new InputStreamReader(entityStream).read(cbuf);
+		if (len < 0)
+			throw new IllegalArgumentException("no entity supplied");
+		return Permission.valueOf(new String(cbuf, 0, len));
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/Scufl2DocumentHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/Scufl2DocumentHandler.java
new file mode 100644
index 0000000..edeac63
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/Scufl2DocumentHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE.txt" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.common.Workflow;
+
+import uk.org.taverna.scufl2.api.io.ReaderException;
+import uk.org.taverna.scufl2.api.io.WorkflowBundleIO;
+import uk.org.taverna.scufl2.api.io.WriterException;
+
+/**
+ * Handler that allows a .scufl2 document to be read from and written to a REST
+ * message directly.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+public class Scufl2DocumentHandler implements MessageBodyReader<Workflow>,
+		MessageBodyWriter<Workflow> {
+	private static final MediaType SCUFL2_TYPE = new MediaType("application",
+			"vnd.taverna.scufl2.workflow-bundle");
+	public static final String SCUFL2 = "application/vnd.taverna.scufl2.workflow-bundle";
+	private WorkflowBundleIO io = new WorkflowBundleIO();
+
+	@Override
+	public boolean isReadable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (type.isAssignableFrom(Workflow.class))
+			return mediaType.isCompatible(SCUFL2_TYPE);
+		return false;
+	}
+
+	@Override
+	public Workflow readFrom(Class<Workflow> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+			throws IOException, WebApplicationException {
+		try {
+			return new Workflow(io.readBundle(entityStream, SCUFL2));
+		} catch (ReaderException e) {
+			throw new WebApplicationException(e, 403);
+		}
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (Workflow.class.isAssignableFrom(type))
+			return mediaType.isCompatible(SCUFL2_TYPE);
+		return false;
+	}
+
+	@Override
+	public long getSize(Workflow workflow, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return -1;
+	}
+
+	@Override
+	public void writeTo(Workflow workflow, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		try {
+			io.writeBundle(workflow.getScufl2Workflow(), entityStream, SCUFL2);
+		} catch (WriterException e) {
+			throw new WebApplicationException(e);
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/T2FlowDocumentHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/T2FlowDocumentHandler.java
new file mode 100644
index 0000000..4227d80
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/T2FlowDocumentHandler.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.taverna.server.master.common.Workflow;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+/**
+ * Handler that allows a .t2flow document to be read from and written to a REST
+ * message directly.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+public class T2FlowDocumentHandler implements MessageBodyReader<Workflow>,
+		MessageBodyWriter<Workflow> {
+	private static final MediaType T2FLOW_TYPE = new MediaType("application",
+			"vnd.taverna.t2flow+xml");
+	public static final String T2FLOW = "application/vnd.taverna.t2flow+xml";
+	public static final String T2FLOW_ROOTNAME = "workflow";
+	public static final String T2FLOW_NS = "http://taverna.sf.net/2008/xml/t2flow";
+	private DocumentBuilderFactory db;
+	private TransformerFactory transformer;
+
+	public T2FlowDocumentHandler() throws ParserConfigurationException,
+			TransformerConfigurationException {
+		db = DocumentBuilderFactory.newInstance();
+		db.setNamespaceAware(true);
+		transformer = TransformerFactory.newInstance();
+	}
+
+	@Override
+	public boolean isReadable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (type.isAssignableFrom(Workflow.class))
+			return mediaType.isCompatible(T2FLOW_TYPE);
+		return false;
+	}
+
+	@Override
+	public Workflow readFrom(Class<Workflow> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+			throws IOException, WebApplicationException {
+		Document doc;
+		try {
+			doc = db.newDocumentBuilder().parse(entityStream);
+		} catch (SAXException e) {
+			throw new WebApplicationException(e, 403);
+		} catch (ParserConfigurationException e) {
+			throw new WebApplicationException(e);
+		}
+		Workflow workflow = new Workflow(doc.getDocumentElement());
+		if (doc.getDocumentElement().getNamespaceURI().equals(T2FLOW_NS)
+				&& doc.getDocumentElement().getNodeName()
+						.equals(T2FLOW_ROOTNAME))
+			return workflow;
+		throw new WebApplicationException(Response.status(403)
+				.entity("invalid T2flow document; bad root element")
+				.type("text/plain").build());
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		if (Workflow.class.isAssignableFrom(type))
+			return mediaType.isCompatible(T2FLOW_TYPE);
+		return false;
+	}
+
+	@Override
+	public long getSize(Workflow workflow, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return -1;
+	}
+
+	@Override
+	public void writeTo(Workflow workflow, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		try {
+			transformer.newTransformer().transform(
+					new DOMSource(workflow.getT2flowWorkflow()),
+					new StreamResult(entityStream));
+		} catch (TransformerException e) {
+			if (e.getCause() != null && e.getCause() instanceof IOException)
+				throw (IOException) e.getCause();
+			throw new WebApplicationException(e);
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/URIListHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/URIListHandler.java
new file mode 100644
index 0000000..a90a229
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/URIListHandler.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.status;
+import static org.taverna.server.master.rest.handler.URIListHandler.URI_LIST;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Deserialization and serialization engine for the <tt>{@value #URI_LIST}</tt>
+ * content type.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+@Consumes(URI_LIST)
+public class URIListHandler implements MessageBodyReader<List<URI>>,
+		MessageBodyWriter<List<URI>> {
+	/** The content type we handle. */
+	public static final String URI_LIST = "text/uri-list";
+	private static final MediaType URILIST = new MediaType("text", "uri-list");
+
+	@Override
+	public boolean isReadable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return type.isAssignableFrom(ArrayList.class)
+				&& genericType instanceof ParameterizedType
+				&& ((Class<?>) ((ParameterizedType) genericType)
+						.getActualTypeArguments()[0])
+						.isAssignableFrom(URI.class)
+				&& URILIST.isCompatible(mediaType);
+	}
+
+	@Override
+	public List<URI> readFrom(Class<List<URI>> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
+			throws IOException, WebApplicationException {
+		String enc = mediaType.getParameters().get("encoding");
+		Charset c = (enc == null) ? Charset.defaultCharset() : Charset
+				.forName(enc);
+		BufferedReader br = new BufferedReader(new InputStreamReader(
+				entityStream, c));
+		ArrayList<URI> uris = new ArrayList<>();
+		String line;
+		while ((line = br.readLine()) != null) {
+			if (line.startsWith("#"))
+				continue;
+			try {
+				uris.add(new URI(line));
+			} catch (URISyntaxException e) {
+				throw new WebApplicationException(e, status(422).entity(
+						"ill-formed URI").build());
+			}
+		}
+		return uris;
+	}
+
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return List.class.isAssignableFrom(type)
+				&& genericType instanceof ParameterizedType
+				&& ((ParameterizedType) genericType).getActualTypeArguments()[0] == URI.class
+				&& URILIST.isCompatible(mediaType);
+	}
+
+	@Override
+	public long getSize(List<URI> list, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return -1;
+	}
+
+	private static final String PREFERRED_ENCODING = "UTF-8";
+
+	@Override
+	public void writeTo(List<URI> list, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException {
+		String encoding = mediaType.getParameters().get("encoding");
+		if (encoding == null) {
+			encoding = PREFERRED_ENCODING;
+			httpHeaders.putSingle("Content-Type", URI_LIST + ";encoding="
+					+ encoding);
+		}
+		BufferedWriter w = new BufferedWriter(new OutputStreamWriter(
+				entityStream, encoding));
+		for (URI uri : list) {
+			w.write(uri.toString());
+			w.newLine();
+		}
+		w.flush();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/UnknownRunHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/UnknownRunHandler.java
new file mode 100644
index 0000000..4542237
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/UnknownRunHandler.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.exceptions.UnknownRunException;
+
+@Provider
+public class UnknownRunHandler extends HandlerCore implements
+		ExceptionMapper<UnknownRunException> {
+	@Override
+	public Response toResponse(UnknownRunException exn) {
+		return respond(NOT_FOUND, exn);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/ZipStreamHandler.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/ZipStreamHandler.java
new file mode 100644
index 0000000..2241220
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/ZipStreamHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.rest.handler;
+
+import static org.apache.commons.io.IOUtils.copy;
+import static org.taverna.server.master.api.ContentTypes.APPLICATION_ZIP_TYPE;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+
+import org.taverna.server.master.interfaces.Directory.ZipStream;
+
+/**
+ * How to write a ZIP file as the result entity of a request.
+ * 
+ * @author Donal Fellows
+ */
+@Provider
+@Produces("application/zip")
+public class ZipStreamHandler implements MessageBodyWriter<ZipStream> {
+	@Override
+	public boolean isWriteable(Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return ZipStream.class.isAssignableFrom(type)
+				&& mediaType.equals(APPLICATION_ZIP_TYPE);
+	}
+
+	@Override
+	public long getSize(ZipStream t, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType) {
+		return -1;
+	}
+
+	@Override
+	public void writeTo(ZipStream zipStream, Class<?> type, Type genericType,
+			Annotation[] annotations, MediaType mediaType,
+			MultivaluedMap<String, Object> httpHeaders,
+			OutputStream entityStream) throws IOException,
+			WebApplicationException {
+		copy(zipStream, entityStream);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/handler/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/package-info.java
new file mode 100644
index 0000000..e72af22
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/handler/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains type handlers for the RESTful interface to Taverna Server.
+ * @author Donal Fellows
+ */
+@XmlSchema(namespace = SERVER_REST, elementFormDefault = QUALIFIED, attributeFormDefault = QUALIFIED, xmlns = {
+		@XmlNs(prefix = "xlink", namespaceURI = XLINK),
+		@XmlNs(prefix = "ts", namespaceURI = SERVER),
+		@XmlNs(prefix = "ts-rest", namespaceURI = SERVER_REST),
+		@XmlNs(prefix = "ts-soap", namespaceURI = SERVER_SOAP),
+		@XmlNs(prefix = "port", namespaceURI = DATA),
+		@XmlNs(prefix = "feed", namespaceURI = FEED),
+		@XmlNs(prefix = "admin", namespaceURI = ADMIN) })
+package org.taverna.server.master.rest.handler;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.master.common.Namespaces.ADMIN;
+import static org.taverna.server.master.common.Namespaces.FEED;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.SERVER_REST;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+import static org.taverna.server.port_description.Namespaces.DATA;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+
diff --git a/server-webapp/src/main/java/org/taverna/server/master/rest/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/rest/package-info.java
new file mode 100644
index 0000000..0a0b069
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/rest/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the RESTful interface to Taverna Server.
+ * @author Donal Fellows
+ */
+@XmlSchema(namespace = SERVER_REST, elementFormDefault = QUALIFIED, attributeFormDefault = QUALIFIED, xmlns = {
+		@XmlNs(prefix = "xlink", namespaceURI = XLINK),
+		@XmlNs(prefix = "ts", namespaceURI = SERVER),
+		@XmlNs(prefix = "ts-rest", namespaceURI = SERVER_REST),
+		@XmlNs(prefix = "ts-soap", namespaceURI = SERVER_SOAP),
+		@XmlNs(prefix = "port", namespaceURI = DATA),
+		@XmlNs(prefix = "feed", namespaceURI = FEED),
+		@XmlNs(prefix = "admin", namespaceURI = ADMIN) })
+package org.taverna.server.master.rest;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.master.common.Namespaces.ADMIN;
+import static org.taverna.server.master.common.Namespaces.FEED;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.SERVER_REST;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+import static org.taverna.server.port_description.Namespaces.DATA;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+
diff --git a/server-webapp/src/main/java/org/taverna/server/master/soap/DirEntry.java b/server-webapp/src/main/java/org/taverna/server/master/soap/DirEntry.java
new file mode 100644
index 0000000..bb8d73f
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/soap/DirEntry.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.soap;
+
+import static org.taverna.server.master.common.Namespaces.XLINK;
+
+import java.net.URI;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlSeeAlso;
+import javax.xml.bind.annotation.XmlType;
+
+import org.taverna.server.master.common.DirEntryReference;
+
+/**
+ * A more Taverna-friendly version of the directory entry descriptor classes.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "DirectoryEntry")
+@XmlRootElement(name = "entry")
+@XmlSeeAlso({ DirEntry.File.class, DirEntry.Directory.class })
+public class DirEntry {
+	/** A link to the entry. Ignored on input. */
+	@XmlAttribute(name = "href", namespace = XLINK)
+	@XmlSchemaType(name = "anyURI")
+	public URI link;
+	@XmlAttribute
+	public String name;
+	@XmlElement(required = true)
+	public String path;
+
+	/**
+	 * A file in a directory.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlType(name = "FileDirEntry")
+	@XmlRootElement(name = "file")
+	public static class File extends DirEntry {
+	}
+
+	/**
+	 * A directory in a directory. That is, a sub-directory.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlType(name = "DirectoryDirEntry")
+	@XmlRootElement(name = "dir")
+	public static class Directory extends DirEntry {
+	}
+
+	/**
+	 * Converts from the "common" format to the subclasses of this class.
+	 * 
+	 * @param deref
+	 *            The "common" format handle to convert.
+	 * @return The converted handle
+	 */
+	public static DirEntry convert(DirEntryReference deref) {
+		DirEntry result;
+		if (deref instanceof DirEntryReference.DirectoryReference)
+			result = new Directory();
+		else if (deref instanceof DirEntryReference.FileReference)
+			result = new File();
+		else
+			result = new DirEntry();
+		result.link = deref.link;
+		result.name = deref.name;
+		result.path = deref.path;
+		return result;
+	}
+
+	/**
+	 * Converts to the "common" format from the subclasses of this class.
+	 * 
+	 * @param deref
+	 *            The subclass of this class to convert.
+	 * @return The converted reference.
+	 */
+	public static DirEntryReference convert(DirEntry de) {
+		DirEntryReference result;
+		if (de instanceof Directory)
+			result = new DirEntryReference.DirectoryReference();
+		else if (de instanceof File)
+			result = new DirEntryReference.FileReference();
+		else
+			result = new DirEntryReference();
+		result.link = de.link;
+		result.name = de.name;
+		result.path = de.path;
+		return result;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/soap/FileContents.java b/server-webapp/src/main/java/org/taverna/server/master/soap/FileContents.java
new file mode 100644
index 0000000..7ebc991
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/soap/FileContents.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.soap;
+
+import static java.lang.Math.min;
+import static java.lang.System.arraycopy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.activation.DataHandler;
+import javax.activation.DataSource;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlMimeType;
+import javax.xml.bind.annotation.XmlType;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.File;
+
+/**
+ * An MTOM-capable description of how to transfer the contents of a file.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "FileContents")
+public class FileContents {
+	@XmlElement
+	public String name;
+	@XmlMimeType("application/octet-stream") // JAXB bug: must be this
+	public DataHandler fileData;
+
+	/**
+	 * Initialize the contents of this descriptor from the given file and
+	 * content type.
+	 * 
+	 * @param file
+	 *            The file that is to be reported.
+	 * @param contentType
+	 *            The estimated content type of the file.
+	 */
+	public void setFile(File file, String contentType) {
+		name = file.getFullName();
+		fileData = new DataHandler(new TavernaFileSource(file, contentType));
+	}
+
+	/**
+	 * Write the content described by this class to the specified file.
+	 * @param file The file to write to; must already exist.
+	 * @throws IOException
+	 * @throws FilesystemAccessException
+	 */
+	public void writeToFile(File file) throws IOException,
+			FilesystemAccessException {
+		try (InputStream is = fileData.getInputStream()) {
+			byte[] buf = new byte[65536];
+			file.setContents(new byte[0]);
+			while (true) {
+				int len = is.read(buf);
+				if (len <= 0)
+					return;
+				if (len == buf.length)
+					file.appendContents(buf);
+				else {
+					byte[] shortbuf = new byte[len];
+					arraycopy(buf, 0, shortbuf, 0, len);
+					file.appendContents(shortbuf);
+				}
+			}
+		}
+	}
+}
+
+/**
+ * A data source that knows how to communicate with the Taverna Server back-end.
+ * 
+ * @author Donal Fellows
+ */
+class TavernaFileSource implements DataSource {
+	TavernaFileSource(File f, String type) {
+		this.f = f;
+		this.type = type;
+	}
+
+	private final File f;
+	private final String type;
+
+	@Override
+	public String getContentType() {
+		return type;
+	}
+
+	@Override
+	public String getName() {
+		return f.getName();
+	}
+
+	@Override
+	public InputStream getInputStream() throws IOException {
+		final File f = this.f;
+		return new InputStream() {
+			private int idx;
+
+			@Override
+			public int read(byte[] b, int off, int len) throws IOException {
+				byte[] r;
+				try {
+					r = f.getContents(idx, len);
+				} catch (FilesystemAccessException e) {
+					throw new IOException(e);
+				}
+				if (r == null)
+					return -1;
+				len = min(len, r.length);
+				arraycopy(r, 0, b, off, len);
+				idx += len;
+				return len;
+			}
+
+			@Override
+			public int read() throws IOException {
+				byte[] r;
+				try {
+					r = f.getContents(idx, 1);
+				} catch (FilesystemAccessException e) {
+					throw new IOException(e);
+				}
+				if (r == null)
+					return -1;
+				idx++;
+				return r[0];
+			}
+		};
+	}
+
+	@Override
+	public OutputStream getOutputStream() throws IOException {
+		final File f = this.f;
+		return new OutputStream() {
+			private boolean append = false;
+
+			@Override
+			public void write(int b) throws IOException {
+				write(new byte[] { (byte) b });
+			}
+
+			@Override
+			public void write(byte[] b) throws IOException {
+				try {
+					if (append)
+						f.appendContents(b);
+					else
+						f.setContents(b);
+					append = true;
+				} catch (FilesystemAccessException e) {
+					throw new IOException(e);
+				}
+			}
+
+			@Override
+			public void write(byte[] b, int off, int len) throws IOException {
+				byte[] ary = new byte[len];
+				arraycopy(b, off, ary, 0, len);
+				write(ary);
+			}
+		};
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/soap/PermissionList.java b/server-webapp/src/main/java/org/taverna/server/master/soap/PermissionList.java
new file mode 100644
index 0000000..3ab6d0c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/soap/PermissionList.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.soap;
+
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+import org.taverna.server.master.common.Permission;
+
+/**
+ * The list of permissions to access a workflow run of users <i>other than the
+ * owner</i>. This class exists to support the JAXB mapping.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "PermissionList")
+@XmlRootElement(name = "permissionList")
+public class PermissionList {
+	/**
+	 * The type of a single mapped permission. This class exists to support the
+	 * JAXB mapping.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@XmlType(name = "")
+	public static class SinglePermissionMapping {
+		public SinglePermissionMapping() {
+		}
+
+		public SinglePermissionMapping(String user, Permission permission) {
+			this.userName = user;
+			this.permission = permission;
+		}
+
+		/** The name of the user that this talks about. */
+		public String userName;
+		/** The permission level that the user is granted. */
+		public Permission permission;
+	}
+
+	/** The list of (non-default) permissions granted. */
+	@XmlElement
+	public List<SinglePermissionMapping> permission;
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/soap/TavernaServerSOAP.java b/server-webapp/src/main/java/org/taverna/server/master/soap/TavernaServerSOAP.java
new file mode 100644
index 0000000..e175bf8
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/soap/TavernaServerSOAP.java
@@ -0,0 +1,1553 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.soap;
+
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Roles.USER;
+
+import java.net.URI;
+import java.util.Date;
+import java.util.List;
+
+import javax.annotation.security.RolesAllowed;
+import javax.jws.WebMethod;
+import javax.jws.WebParam;
+import javax.jws.WebResult;
+import javax.jws.WebService;
+import javax.xml.bind.annotation.XmlElement;
+
+import org.apache.cxf.annotations.WSDLDocumentation;
+import org.ogf.usage.JobUsageRecord;
+import org.taverna.server.master.common.Capability;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.DirEntryReference;
+import org.taverna.server.master.common.InputDescription;
+import org.taverna.server.master.common.Permission;
+import org.taverna.server.master.common.ProfileList;
+import org.taverna.server.master.common.RunReference;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.common.version.Version;
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoCredentialException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.NotOwnerException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.rest.TavernaServerREST;
+import org.taverna.server.port_description.OutputDescription;
+
+/**
+ * The SOAP service interface to Taverna 3 Server.
+ * 
+ * @author Donal Fellows
+ * @see TavernaServerREST
+ */
+@RolesAllowed(USER)
+@WebService(name = "tavernaService", targetNamespace = SERVER_SOAP)
+@WSDLDocumentation("The SOAP service interface to Taverna " + Version.JAVA
+		+ " Server.")
+public interface TavernaServerSOAP {
+	/**
+	 * Make a run for a particular workflow.
+	 * 
+	 * @param workflow
+	 *            The workflow to instantiate.
+	 * @return Annotated handle for created run.
+	 * @throws NoUpdateException
+	 * @throws NoCreateException
+	 */
+	@WebResult(name = "Run")
+	@WSDLDocumentation("Make a run for a particular workflow.")
+	RunReference submitWorkflow(
+			@WebParam(name = "workflow") @XmlElement(required = true) Workflow workflow)
+					throws NoUpdateException, NoCreateException;
+
+	/**
+	 * Make a run for a particular workflow.
+	 * 
+	 * @param workflow
+	 *            The workflow to instantiate.
+	 * @return Annotated handle for created run.
+	 * @throws NoUpdateException
+	 * @throws NoCreateException
+	 */
+	@WebResult(name = "Run")
+	@WSDLDocumentation("Make a run for a particular workflow.")
+	RunReference submitWorkflowMTOM(
+			@WebParam(name = "workflow") @XmlElement(required = true) WrappedWorkflow workflow)
+			throws NoUpdateException;
+
+	/**
+	 * Make a run for a particular workflow, where that workflow will be
+	 * downloaded from elsewhere. The URI <i>must</i> be publicly readable.
+	 * 
+	 * @param workflowURI
+	 *            The URI to the workflow to instantiate.
+	 * @return Annotated handle for created run.
+	 * @throws NoUpdateException
+	 * @throws NoCreateException
+	 */
+	@WebResult(name = "Run")
+	@WSDLDocumentation("Make a run for a particular workflow where that "
+			+ "workflow is given by publicly readable URI.")
+	RunReference submitWorkflowByURI(
+			@WebParam(name = "workflowURI") @XmlElement(required = true) URI workflowURI)
+			throws NoCreateException, NoUpdateException;
+
+	/**
+	 * Get the list of existing runs owned by the user.
+	 * 
+	 * @return Annotated handle list.
+	 */
+	@WebResult(name = "Run")
+	@WSDLDocumentation("Get the list of existing runs owned by the user.")
+	RunReference[] listRuns();
+
+	/**
+	 * Get the upper limit on the number of runs that the user may create at
+	 * once.
+	 * 
+	 * @return The limit. <b>NB:</b> the number currently operating may be
+	 *         larger, but in that case no further runs can be made until some
+	 *         of the old ones are destroyed.
+	 */
+	@WebResult(name = "MaxSimultaneousRuns")
+	@WSDLDocumentation("Get the upper limit on the number of runs that the user may create at once.")
+	int getServerMaxRuns();
+
+	/**
+	 * Get the list of allowed workflows. If the list is empty, <i>any</i>
+	 * workflow may be used.
+	 * 
+	 * @return A list of workflow documents.
+	 */
+	@WebMethod(operationName = "getPermittedWorkflowURIs")
+	@WebResult(name = "PermittedWorkflowURI")
+	@WSDLDocumentation("Get the list of URIs to allowed workflows. If the list is empty, any workflow may be used including those not submitted via URI.")
+	URI[] getServerWorkflows();
+
+	/**
+	 * Get the list of allowed event listeners.
+	 * 
+	 * @return A list of listener names.
+	 */
+	@WebMethod(operationName = "getPermittedListenerTypes")
+	@WebResult(name = "PermittedListenerType")
+	@WSDLDocumentation("Get the list of allowed types of event listeners.")
+	String[] getServerListeners();
+
+	/**
+	 * Get the list of notification fabrics.
+	 * 
+	 * @return A list of listener names.
+	 */
+	@WebMethod(operationName = "getEnabledNotificationFabrics")
+	@WebResult(name = "EnabledNotifierFabric")
+	@WSDLDocumentation("Get the list of notification fabrics. Each is a URI scheme.")
+	String[] getServerNotifiers();
+
+	@WebMethod(operationName = "getCapabilities")
+	@WebResult(name = "Capabilities")
+	@WSDLDocumentation("Get the workflow execution capabilities of this "
+			+ "Taverna Server instance.")
+	List<Capability> getServerCapabilities();
+
+	/**
+	 * Destroy a run immediately. This might or might not actually relinquish
+	 * resources; that's up to the service implementation and deployment.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the lifetime of the
+	 *             run.
+	 */
+	@WSDLDocumentation("Destroy a run immediately.")
+	void destroyRun(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException, NoUpdateException;
+
+	/**
+	 * Get the workflow document used to create the given run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The workflow document.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "CreationWorkflow")
+	@WSDLDocumentation("Get the workflow document used to create the given run.")
+	Workflow getRunWorkflow(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Get the workflow document used to create the given run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The workflow document.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "CreationWorkflow")
+	@WSDLDocumentation("Get the workflow document used to create the given run.")
+	WrappedWorkflow getRunWorkflowMTOM(
+			@WebParam(name = "runName") String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Get a description of the profiles supported by the workflow document used
+	 * to create the given run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return A description of the supported profiles.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "Profiles")
+	@WSDLDocumentation("Get a description of the profiles supported by the workflow document used to create the given run.")
+	ProfileList getRunWorkflowProfiles(
+			@WebParam(name = "runName") String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Get the descriptive name of the workflow run. The descriptive name
+	 * carries no deep information.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The descriptive name of the run.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "DescriptiveName")
+	@WSDLDocumentation("Get the descriptive name of the workflow run. Carries no deep information.")
+	String getRunDescriptiveName(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Set the descriptive name of the workflow run. The descriptive name
+	 * carries no deep information.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param descriptiveName
+	 *            The new descriptive name to set. Note that the implementation
+	 *            is allowed to arbitrarily truncate this value.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user is not permitted to update this run.
+	 */
+	@WSDLDocumentation("Set the descriptive name of the workflow run. Carries no deep information.")
+	void setRunDescriptiveName(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "descriptiveName") @XmlElement(required = true) String descriptiveName)
+			throws UnknownRunException, NoUpdateException;
+
+	/**
+	 * Get the description of the inputs to the workflow run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The input description
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "RunInputDescription")
+	@WSDLDocumentation("Get the description of the inputs currently set up for the given workflow run.")
+	InputDescription getRunInputs(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Get a description of what inputs the workflow run <i>expects</i> to
+	 * receive.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The description document.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "RunInputDescriptor")
+	@WSDLDocumentation("Get a description of what inputs the given workflow run expects to receive.")
+	org.taverna.server.port_description.InputDescription getRunInputDescriptor(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Tells the run to use the given Baclava file for all inputs.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param fileName
+	 *            The name of the file to use. Must not start with a <tt>/</tt>
+	 *            or contain a <tt>..</tt> element.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the run.
+	 * @throws FilesystemAccessException
+	 *             If the filename is illegal.
+	 * @throws BadStateChangeException
+	 *             If the run is not in the {@link Status#Initialized
+	 *             Initialized} state
+	 */
+	@WSDLDocumentation("Tells the given run to use the given already-uploaded Baclava file for all inputs.")
+	void setRunInputBaclavaFile(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "baclavaFileName") String fileName)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, BadStateChangeException;
+
+	/**
+	 * Tells the run to use the given file for input on the given port. This
+	 * overrides any previously set file or value on the port and causes the
+	 * server to forget about using a Baclava file for all inputs.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param portName
+	 *            The port to use the file for.
+	 * @param portFilename
+	 *            The file to use on the port. Must not start with a <tt>/</tt>
+	 *            or contain a <tt>..</tt> element.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the run.
+	 * @throws FilesystemAccessException
+	 *             If the filename is illegal.
+	 * @throws BadStateChangeException
+	 *             If the run is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 * @throws BadPropertyValueException
+	 *             If the input port may not be changed to the contents of the
+	 *             given file.
+	 */
+	@WSDLDocumentation("Tells the given run to use the given file for input on the given port.")
+	void setRunInputPortFile(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "portName") @XmlElement(required = true) String portName,
+			@WebParam(name = "portFileName") @XmlElement(required = true) String portFilename)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, BadStateChangeException,
+			BadPropertyValueException;
+
+	/**
+	 * Tells the run to use the given value for input on the given port. This
+	 * overrides any previously set file or value on the port and causes the
+	 * server to forget about using a Baclava file for all inputs. Note that
+	 * this is wholly unsuitable for use with binary data.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param portName
+	 *            The port to use the file for.
+	 * @param portValue
+	 *            The literal value to use on the port.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the run.
+	 * @throws BadStateChangeException
+	 *             If the run is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 * @throws BadPropertyValueException
+	 *             If the input port may not be changed to the given literal
+	 *             value.
+	 */
+	@WSDLDocumentation("Tells the given run to use the given literal string value for input on the given port.")
+	void setRunInputPortValue(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "portName") @XmlElement(required = true) String portName,
+			@WebParam(name = "portValue") @XmlElement(required = true) String portValue)
+			throws UnknownRunException, NoUpdateException,
+			BadStateChangeException, BadPropertyValueException;
+
+	/**
+	 * Tells the given run to use the given list delimiter (a single-character
+	 * string value) for splitting the input on the given port. Note that
+	 * nullability of the delimiter is supported here.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param portName
+	 *            The port to set the list delimiter for.
+	 * @param delimiter
+	 *            The single-character value (in range U+00001..U+0007F) to use
+	 *            as the delimiter, or <tt>null</tt> for no delimiter at all.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the run.
+	 * @throws BadStateChangeException
+	 *             If the run is not in the {@link Status#Initialized
+	 *             Initialized} state.
+	 * @throws BadPropertyValueException
+	 *             If the delimiter may not be changed to the given literal
+	 *             value.
+	 */
+	@WSDLDocumentation("Tells the given run to use the given list delimiter (a single-character string value) for splitting the input on the given port. Note that nullability of the delimiter is supported here.")
+	void setRunInputPortListDelimiter(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "portName") @XmlElement(required = true) String portName,
+			@WebParam(name = "delimiter") String delimiter)
+			throws UnknownRunException, NoUpdateException,
+			BadStateChangeException, BadPropertyValueException;
+
+	/**
+	 * Get the Baclava file where the output of the run will be written.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The filename, or <tt>null</tt> if the results will be written to
+	 *         a subdirectory <tt>out</tt> of the run's working directory.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "OutputBaclavaFile")
+	@WSDLDocumentation("Get the Baclava file where the output of the run will be written.")
+	String getRunOutputBaclavaFile(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Set the Baclava file where the output of the run will be written.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param outputFile
+	 *            The filename for the Baclava file, or <tt>null</tt> or the
+	 *            empty string to indicate that the results are to be written to
+	 *            the subdirectory <tt>out</tt> of the run's working directory.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the run.
+	 * @throws FilesystemAccessException
+	 *             If the filename is illegal (starts with a <tt>/</tt> or
+	 *             contains a <tt>..</tt> element.
+	 * @throws BadStateChangeException
+	 *             If the run is not in the {@link Status#Initialized
+	 *             Initialized} state
+	 */
+	@WSDLDocumentation("Set the Baclava file where the output of the run will be written.")
+	void setRunOutputBaclavaFile(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "baclavaFileName") String outputFile)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, BadStateChangeException;
+
+	/**
+	 * Return a description of the outputs of a run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return Description document (higher level than filesystem traverse).
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws BadStateChangeException
+	 *             If the run is in the {@link Status#Initialized Initialized}
+	 *             state
+	 * @throws FilesystemAccessException
+	 *             If there is an exception when accessing the filesystem.
+	 * @throws NoDirectoryEntryException
+	 *             If things are odd in the filesystem.
+	 */
+	@WebResult(name = "OutputDescription")
+	@WSDLDocumentation("Return a description of the outputs of a run. Only known during/after the run.")
+	OutputDescription getRunOutputDescription(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException, BadStateChangeException,
+			FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Get the time when the run will be eligible to be automatically deleted.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return A date at which the expiry will be scheduled. The actual deletion
+	 *         will happen an arbitrary amount of time later (depending on
+	 *         system policy).
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "Expiry")
+	@WSDLDocumentation("Get the time when the run will be eligible to be automatically deleted.")
+	Date getRunExpiry(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Set when the run will be eligible to be automatically deleted.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param expiry
+	 *            A date at which the expiry will be scheduled. The actual
+	 *            deletion will happen an arbitrary amount of time later
+	 *            (depending on system policy).
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the lifetime of the
+	 *             run.
+	 */
+	@WSDLDocumentation("Set when the run will be eligible to be automatically deleted.")
+	void setRunExpiry(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "expiry") @XmlElement(required = true) Date expiry)
+			throws UnknownRunException, NoUpdateException;
+
+	/**
+	 * Get the time when the run was created.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The moment when the run was created (modulo some internal
+	 *         overhead).
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "CreationTime")
+	@WSDLDocumentation("Get the time when the run was created.")
+	Date getRunCreationTime(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Get the time when the run was started.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The moment when the run was started (modulo some internal
+	 *         overhead) or <tt>null</tt> to indicate that it has never started.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "StartTime")
+	@WSDLDocumentation("Get the time when the run was started.")
+	Date getRunStartTime(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Get the time when the run was detected as having finished.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The moment when the run was believed stopped. Note that this may
+	 *         not be when the run <i>actually</i> finished; promptness of
+	 *         detection depends on many factors.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "FinishTime")
+	@WSDLDocumentation("Get the time when the run was detected as having finished.")
+	Date getRunFinishTime(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Get the current status of the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The status code.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "Status")
+	@WSDLDocumentation("Get the current status of the given workflow run.")
+	Status getRunStatus(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Set the status of a run. This is used to start it executing, make it stop
+	 * executing, etc. Note that changing the status of a run can <i>never</i>
+	 * cause the run to be destroyed.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param status
+	 *            The status to change to. Changing to the current status will
+	 *            always have no effect.
+	 * @return An empty string if the state change was completed, or a
+	 *         description (never empty) of why the state change is ongoing.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the run.
+	 * @throws BadStateChangeException
+	 *             If the state change requested is impossible.
+	 */
+	@WebResult(name = "PartialityReason")
+	@WSDLDocumentation("Set the status of a given workflow run.")
+	String setRunStatus(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "status") @XmlElement(required = true) Status status)
+			throws UnknownRunException, NoUpdateException,
+			BadStateChangeException;
+
+	/**
+	 * Get the names of the event listeners attached to the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The listener names.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "ListenerName")
+	@WSDLDocumentation("Get the names of the event listeners attached to the run.")
+	String[] getRunListeners(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Adds an event listener to the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param listenerType
+	 *            The type of event listener to add. Must be one of the names
+	 *            returned by the {@link #getServerListeners()} operation.
+	 * @param configuration
+	 *            The configuration document for the event listener; the
+	 *            interpretation of the configuration is up to the listener.
+	 * @return The actual name of the listener.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user isn't allowed to manipulate the run.
+	 * @throws NoListenerException
+	 *             If the listener construction fails (<i>e.g.</i>, due to an
+	 *             unsupported <b>listenerType</b> or a problem with the
+	 *             <b>configuration</b>).
+	 */
+	@WebResult(name = "ListenerName")
+	@WSDLDocumentation("Adds an event listener to the run.")
+	String addRunListener(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "listenerType") @XmlElement(required = true) String listenerType,
+			@WebParam(name = "configuration") @XmlElement(required = true) String configuration)
+			throws UnknownRunException, NoUpdateException, NoListenerException;
+
+	/**
+	 * Returns the standard output of the workflow run. Unstarted runs return
+	 * the empty string.
+	 * <p>
+	 * The equivalent thing can also be fetched from the relevant listener
+	 * property (i.e., io/stdout).
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return Whatever the run engine printed on its stdout.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "StandardOutput")
+	@WSDLDocumentation("Returns the stdout from the run engine.")
+	String getRunStdout(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Returns the standard error of the workflow run. Unstarted runs return the
+	 * empty string.
+	 * <p>
+	 * The equivalent thing can also be fetched from the relevant listener
+	 * property (i.e., io/stderr).
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return Whatever the run engine printed on its stderr.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "StandardError")
+	@WSDLDocumentation("Returns the stderr from the run engine.")
+	String getRunStderr(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Returns the usage record for the workflow run. Unfinished runs return
+	 * <tt>null</tt>.
+	 * <p>
+	 * The equivalent thing can also be fetched from the relevant listener
+	 * property (i.e., io/usage).
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The usage record, or <tt>null</tt>.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "ResourceUsage")
+	@WSDLDocumentation("Returns the resource usage from the run engine.")
+	JobUsageRecord getRunUsageRecord(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Returns the log of the workflow run. Unstarted runs return the empty
+	 * string.
+	 * <p>
+	 * This can also be fetched from the appropriate file (i.e.,
+	 * <tt>logs/detail.log</tt>).
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return Whatever the run engine wrote to its log.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "Log")
+	@WSDLDocumentation("Returns the detailed log from the run engine.")
+	String getRunLog(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Returns the run bundle of a run. The run must be <i>finished</i> for this
+	 * to be guaranteed to be present, and must <i>not</i> have had its output
+	 * generated as Baclava.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The contents of the run bundle.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If there was a problem reading the bundle.
+	 * @throws NoDirectoryEntryException
+	 *             If the bundle doesn't exist currently.
+	 */
+	@WebResult(name = "RunBundle")
+	@WSDLDocumentation("Gets the run bundle of a finished run. MTOM support recommended!")
+	FileContents getRunBundle(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Gets whether to generate provenance (in a run bundle) for a run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return Whether provenance will be generated.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "GenerateProvenance")
+	@WSDLDocumentation("Gets whether a run generates provenance.")
+	boolean getRunGenerateProvenance(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Sets whether to generate provenance (in a run bundle) for a run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param generateProvenance
+	 *            Whether to generate provenance.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user is not allowed to manipulate the run.
+	 */
+	@WSDLDocumentation("Sets whether a run generates provenance. "
+			+ "Only usefully settable before the run is started.")
+	void setRunGenerateProvenance(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "generateProvenance") @XmlElement(required = true) boolean generateProvenance)
+			throws UnknownRunException, NoUpdateException;
+
+	/**
+	 * Get the owner of the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The status code.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 */
+	@WebResult(name = "Owner")
+	@WSDLDocumentation("Get the owner of the given workflow run.")
+	String getRunOwner(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException;
+
+	/**
+	 * Get the list of permissions associated with a workflow run.
+	 * 
+	 * @param runName
+	 *            The name of the run whose permissions are to be obtained.
+	 * @return A description of the non-<tt>none</tt> permissions.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the current
+	 *             user is not permitted to see it.
+	 * @throws NotOwnerException
+	 *             If asked to provide this information about a run that the
+	 *             current user may see but where they are not the owner of it.
+	 */
+	@WebResult(name = "PermissionList")
+	@WSDLDocumentation("Get the list of permissions associated with a given workflow run.")
+	PermissionList listRunPermissions(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException, NotOwnerException;
+
+	/**
+	 * Set the permission for a user to access and update a particular workflow
+	 * run.
+	 * 
+	 * @param runName
+	 *            The name of the run whose permissions are to be updated.
+	 * @param userName
+	 *            The name of the user about whom this call is talking.
+	 * @param permission
+	 *            The permission level to set.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the current
+	 *             user is not permitted to see it.
+	 * @throws NotOwnerException
+	 *             If asked to provide this information about a run that the
+	 *             current user may see but where they are not the owner of it.
+	 */
+	@WSDLDocumentation("Set the permission for a user to access and update a given workflow run.")
+	void setRunPermission(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "userName") @XmlElement(required = true) String userName,
+			@WebParam(name = "permission") @XmlElement(required = true) Permission permission)
+			throws UnknownRunException, NotOwnerException;
+
+	/**
+	 * Get the credentials associated with the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The collection of credentials.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NotOwnerException
+	 *             If the user is permitted to see the run, but isn't the owner;
+	 *             only the owner may see the credentials.
+	 */
+	@WebResult(name = "Credentials")
+	@WSDLDocumentation("Get the credentials (passwords, private keys) associated with the given workflow run. Only the owner may do this.")
+	Credential[] getRunCredentials(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException, NotOwnerException;
+
+	/**
+	 * Set a credential associated with the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param credentialID
+	 *            The handle of the credential to set. If empty, a new
+	 *            credential will be created.
+	 * @param credential
+	 *            The credential to set.
+	 * @return The handle of the credential that was created or updated.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NotOwnerException
+	 *             If the user is permitted to see the run, but isn't the owner;
+	 *             only the owner may manipulate the credentials.
+	 * @throws InvalidCredentialException
+	 *             If the <b>credential</b> fails its checks.
+	 * @throws NoCredentialException
+	 *             If the <b>credentialID</b> is not empty but does not
+	 *             correspond to an existing credential.
+	 * @throws BadStateChangeException
+	 *             If an attempt to manipulate the credentials is done after the
+	 *             workflow has started running.
+	 */
+	@WebResult(name = "credentialID")
+	@WSDLDocumentation("Set a credential (password, private key, etc.) associated with the given run. Only the owner may do this.")
+	String setRunCredential(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "credentialID") @XmlElement(required = true) String credentialID,
+			@WebParam(name = "credential") @XmlElement(required = true) Credential credential)
+			throws UnknownRunException, NotOwnerException,
+			InvalidCredentialException, NoCredentialException,
+			BadStateChangeException;
+
+	/**
+	 * Delete a credential associated with the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param credentialID
+	 *            The handle of the credential to delete. If empty, a new
+	 *            credential will be created.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NotOwnerException
+	 *             If the user is permitted to see the run, but isn't the owner;
+	 *             only the owner may manipulate the credentials.
+	 * @throws NoCredentialException
+	 *             If the given credentialID does not exist.
+	 * @throws BadStateChangeException
+	 *             If an attempt to manipulate the credentials is done after the
+	 *             workflow has started running.
+	 */
+	@WSDLDocumentation("Delete a credential associated with the given run. Only the owner may do this.")
+	void deleteRunCredential(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "credentialID") @XmlElement(required = true) String credentialID)
+			throws UnknownRunException, NotOwnerException,
+			NoCredentialException, BadStateChangeException;
+
+	/**
+	 * Get the certificate collections associated with the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @return The collection of credentials.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NotOwnerException
+	 *             If the user is permitted to see the run, but isn't the owner;
+	 *             only the owner may see the credentials.
+	 */
+	@WebResult(name = "CertificateCollections")
+	@WSDLDocumentation("Get the trusted (server or CA) certificates associated with the run. Only the owner may do this.")
+	Trust[] getRunCertificates(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName)
+			throws UnknownRunException, NotOwnerException;
+
+	/**
+	 * Set a certificate collection associated with the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param certificateID
+	 *            The handle of the certificate collection to set. If empty, a
+	 *            new certificate collection will be created.
+	 * @param certificate
+	 *            The certificate collection to set.
+	 * @return The handle of the certificate set that was created or updated.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NotOwnerException
+	 *             If the user is permitted to see the run, but isn't the owner;
+	 *             only the owner may manipulate the certificates.
+	 * @throws InvalidCredentialException
+	 *             If the <b>certificate</b> fails its checks.
+	 * @throws NoCredentialException
+	 *             If the <b>credentialID</b> is not empty but does not
+	 *             correspond to an existing certificate collection.
+	 * @throws BadStateChangeException
+	 *             If an attempt to manipulate the credentials is done after the
+	 *             workflow has started running.
+	 */
+	@WebResult(name = "certificateID")
+	@WSDLDocumentation("Set a trusted (server or CA) certificate associated with the run. Only the owner may do this.")
+	String setRunCertificates(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "certificateID") String certificateID,
+			@WebParam(name = "certificate") @XmlElement(required = true) Trust certificate)
+			throws UnknownRunException, NotOwnerException,
+			InvalidCredentialException, NoCredentialException,
+			BadStateChangeException;
+
+	/**
+	 * Delete a certificate collection associated with the run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param certificateID
+	 *            The handle of the credential to delete.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NotOwnerException
+	 *             If the user is permitted to see the run, but isn't the owner;
+	 *             only the owner may manipulate the certificates.
+	 * @throws NoCredentialException
+	 *             If the given certificateID does not exist.
+	 * @throws BadStateChangeException
+	 *             If an attempt to manipulate the credentials is done after the
+	 *             workflow has started running.
+	 */
+	@WSDLDocumentation("Delete a trusted (server or CA) certificate associated with the run. Only the owner may do this.")
+	void deleteRunCertificates(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "certificateID") @XmlElement(required = true) String certificateID)
+			throws UnknownRunException, NotOwnerException,
+			NoCredentialException, BadStateChangeException;
+
+	/**
+	 * Get the contents of any directory at/under the run's working directory.
+	 * Runs do not share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param directory
+	 *            The name of the directory to fetch; the main working directory
+	 *            is <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @return A list of entries. They are assumed to be all directories or
+	 *         files.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., reading the contents of
+	 *             a file).
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the directory can't be looked up.
+	 */
+	@WebResult(name = "DirectoryEntry")
+	@WSDLDocumentation("Get the contents of any directory at/under the run's working directory.")
+	DirEntry[] getRunDirectoryContents(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "directory") @XmlElement(required = true) DirEntry directory)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Get the contents of any directory (and its subdirectories) at/under the
+	 * run's working directory, returning it as a compressed ZIP file. Runs do
+	 * not share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param directory
+	 *            The name of the directory to fetch; the main working directory
+	 *            is <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @return A serialized ZIP file.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., reading the contents of
+	 *             a file).
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the directory can't be looked up.
+	 */
+	@WebResult(name = "ZipFile")
+	@WSDLDocumentation("Get the contents of any directory (and its subdirectories) at/under the run's working directory, returning it as a compressed ZIP file.")
+	byte[] getRunDirectoryAsZip(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "directory") @XmlElement(required = true) DirEntry directory)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Get the contents of any directory (and its subdirectories) at/under the
+	 * run's working directory, returning it as a compressed ZIP file that is
+	 * streamed via MTOM. Runs do not share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param directory
+	 *            The name of the directory to fetch; the main working directory
+	 *            is <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @return An MTOM-streamable ZIP file reference.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., reading the contents of
+	 *             a file).
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the directory can't be looked up.
+	 */
+	@WebResult(name = "ZipStream")
+	@WSDLDocumentation("Get the contents of any directory (and its subdirectories) at/under the run's working directory, returning it as a compressed ZIP file that is streamed by MTOM.")
+	ZippedDirectory getRunDirectoryAsZipMTOM(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "directory") @XmlElement(required = true) DirEntry directory)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Make a new empty directory beneath an existing one, which must be the
+	 * run's working directory or a directory beneath it. Runs do not share
+	 * working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param parent
+	 *            The parent directory that will have the new directory added
+	 *            beneath it.
+	 * @param name
+	 *            The name of the directory to create. Must not be the same as
+	 *            any other file or directory in the <i>parent</i> directory.
+	 *            The name <i>must not</i> consist of <tt>..</tt> or have a
+	 *            <tt>/</tt> in it.
+	 * @return A reference to the created directory.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user is not allowed to make modifications to the run.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., making something with
+	 *             the same name as something that already exists).
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the containing directory can't be looked up.
+	 */
+	@WebResult(name = "CreatedDirectory")
+	@WSDLDocumentation("Make a new empty directory beneath an existing one, all relative to the given run's main working directory.")
+	DirEntry makeRunDirectory(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "parentDirectory") @XmlElement(required = true) DirEntry parent,
+			@WebParam(name = "directoryName") @XmlElement(required = true) String name)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Make a new empty file in an existing directory, which may be the run's
+	 * working directory or any directory beneath it. Runs do not share working
+	 * directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param parent
+	 *            The parent directory that will have the new file added to it.
+	 * @param name
+	 *            The name of the file to create. Must not be the same as any
+	 *            other file or directory in the <i>parent</i> directory. The
+	 *            name <i>must not</i> consist of <tt>..</tt> or have a
+	 *            <tt>/</tt> in it.
+	 * @return A reference to the created file. The file will be empty.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user is not allowed to make modifications to the run.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., making something with
+	 *             the same name as something that already exists).
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the containing directory can't be looked up.
+	 */
+	@WebResult(name = "CreatedFile")
+	@WSDLDocumentation("Make a new empty file in an existing directory, which may be the run's working directory or any directory beneath it.")
+	DirEntry makeRunFile(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "parentDirectory") @XmlElement(required = true) DirEntry parent,
+			@WebParam(name = "fileNameTail") @XmlElement(required = true) String name)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Destroy an entry (file or directory) in or beneath a run's working
+	 * directory. Runs do not share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param dirEntry
+	 *            Reference to an existing item in a directory that will be
+	 *            destroyed. May be a reference to either a file or a directory.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user is not allowed to make modifications to the run.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., deleting something
+	 *             which doesn't exist or attempting to delete the main working
+	 *             directory).
+	 * @throws NoDirectoryEntryException
+	 *             If the name of the file or directory can't be looked up.
+	 */
+	@WSDLDocumentation("Destroy an entry (file or directory) in or beneath a run's working directory.")
+	void destroyRunDirectoryEntry(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "directoryEntry") @XmlElement(required = true) DirEntry dirEntry)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Get the contents of a file under the run's working directory. Runs do not
+	 * share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param file
+	 *            The name of the file to fetch; the main working directory is
+	 *            <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @return The literal byte contents of the file.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., reading the contents of
+	 *             a directory).
+	 * @throws NoDirectoryEntryException
+	 *             If the file doesn't exist.
+	 */
+	@WebResult(name = "FileContents")
+	@WSDLDocumentation("Get the contents of a file under the run's working directory.")
+	byte[] getRunFileContents(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "fileName") @XmlElement(required = true) DirEntry file)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Get the contents of a file under the run's working directory via MTOM.
+	 * Runs do not share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param file
+	 *            The name of the file to fetch; the main working directory is
+	 *            <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @return The contents, described for transfer via MTOM.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., reading the contents of
+	 *             a directory).
+	 * @throws NoDirectoryEntryException
+	 *             If the file doesn't exist.
+	 */
+	@WebResult(name = "FileContentsMTOM")
+	@WSDLDocumentation("Get the contents of a file via MTOM.")
+	FileContents getRunFileContentsMTOM(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "fileName") @XmlElement(required = true) DirEntry file)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Set the contents of a file under the run's working directory. Runs do not
+	 * share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param file
+	 *            The name of the file to update; the main working directory is
+	 *            <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @param newContents
+	 *            The literal bytes to set the file contents to.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user is not allowed to make modifications to the run.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., writing the contents of
+	 *             a directory).
+	 * @throws NoDirectoryEntryException
+	 *             If the file doesn't exist.
+	 */
+	@WSDLDocumentation("Set the contents of a file under the run's working directory.")
+	void setRunFileContents(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "fileName") @XmlElement(required = true) DirEntry file,
+			@WebParam(name = "contents") @XmlElement(required = true) byte[] newContents)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Set the contents of a file under the run's working directory. Runs do not
+	 * share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param newContents
+	 *            The description of what file to set, and what to the file
+	 *            contents should be set to.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user is not allowed to make modifications to the run.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., writing the contents of
+	 *             a directory).
+	 * @throws NoDirectoryEntryException
+	 *             If the file doesn't exist.
+	 */
+	@WSDLDocumentation("Set the contents of a file under the run's working directory.")
+	void setRunFileContentsMTOM(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "contents") @XmlElement(required = true) FileContents newContents)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Set the contents of a file under the run's working directory to the
+	 * contents of a publicly readable URI. Runs do not share working
+	 * directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param file
+	 *            The name of the file to update; the main working directory is
+	 *            <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @param reference
+	 *            The publicly readable URI whose contents are to become the
+	 *            literal bytes of the file's contents.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoUpdateException
+	 *             If the user is not allowed to make modifications to the run.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., writing the contents of
+	 *             a directory).
+	 * @throws NoDirectoryEntryException
+	 *             If the file doesn't exist.
+	 */
+	@WSDLDocumentation("Set the contents of a file under the run's working directory from the contents of a publicly readable URI.")
+	void setRunFileContentsFromURI(@WebParam(name = "runName") String runName,
+			@WebParam(name = "fileName") DirEntryReference file,
+			@WebParam(name = "contents") URI reference)
+			throws UnknownRunException, NoUpdateException,
+			FilesystemAccessException, NoDirectoryEntryException;
+
+	/**
+	 * Get the length of any file (in bytes) at/under the run's working
+	 * directory. Runs do not share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param file
+	 *            The name of the file to get the length of; the main working
+	 *            directory is <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @return The number of bytes in the file.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., reading the length of a
+	 *             directory).
+	 * @throws NoDirectoryEntryException
+	 *             If the file doesn't exist.
+	 */
+	@WebResult(name = "FileLength")
+	@WSDLDocumentation("Get the length of any file (in bytes) at/under the run's working directory.")
+	long getRunFileLength(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "fileName") @XmlElement(required = true) DirEntry file)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Get the time that the file or directory (at/under the run's working
+	 * directory) was last modified. Runs do not share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param file
+	 *            The name of the file to get the modification date of; the main
+	 *            working directory is <tt>/</tt> and <tt>..</tt> is always
+	 *            disallowed.
+	 * @return The modification date of the file or directory, as understood by
+	 *         the underlying operating system.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated.
+	 * @throws NoDirectoryEntryException
+	 *             If the file or directory doesn't exist.
+	 */
+	@WebResult(name = "FileModified")
+	@WSDLDocumentation("Get the length of any file (in bytes) at/under the run's working directory.")
+	Date getRunFileModified(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "fileName") @XmlElement(required = true) DirEntry file)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Get the content type of any file at/under the run's working directory.
+	 * Runs do not share working directories.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param file
+	 *            The name of the file to get the length of; the main working
+	 *            directory is <tt>/</tt> and <tt>..</tt> is always disallowed.
+	 * @return The content type of the file.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws FilesystemAccessException
+	 *             If some assumption is violated (e.g., reading the length of a
+	 *             directory).
+	 * @throws NoDirectoryEntryException
+	 *             If the file doesn't exist.
+	 */
+	@WebResult(name = "FileContentType")
+	@WSDLDocumentation("Get the content type of any file at/under the run's working directory.")
+	String getRunFileType(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "fileName") @XmlElement(required = true) DirEntry file)
+			throws UnknownRunException, FilesystemAccessException,
+			NoDirectoryEntryException;
+
+	/**
+	 * Get the configuration document for an event listener attached to a run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param listenerName
+	 *            The name of the listener attached.
+	 * @return The configuration document.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoListenerException
+	 *             If no such listener exists.
+	 */
+	@WebResult(name = "ListenerConfiguration")
+	@WSDLDocumentation("Get the configuration document for an event listener attached to a run.")
+	String getRunListenerConfiguration(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "listenerName") @XmlElement(required = true) String listenerName)
+			throws UnknownRunException, NoListenerException;
+
+	/**
+	 * Get the list of properties supported by an event listener attached to a
+	 * run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param listenerName
+	 *            The name of the listener attached.
+	 * @return The list of property names.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoListenerException
+	 *             If no such listener exists.
+	 */
+	@WebResult(name = "ListenerPropertyName")
+	@WSDLDocumentation("Get the list of properties supported by an event listener attached to a run.")
+	String[] getRunListenerProperties(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "listenerName") @XmlElement(required = true) String listenerName)
+			throws UnknownRunException, NoListenerException;
+
+	/**
+	 * Get the value of a property for an event listener attached to a run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param listenerName
+	 *            The name of the listener attached.
+	 * @param propertyName
+	 *            The name of the property to read.
+	 * @return The configuration document.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoListenerException
+	 *             If no such listener exists or if the listener has no such
+	 *             property.
+	 */
+	@WebResult(name = "ListenerPropertyValue")
+	@WSDLDocumentation("Get the value of a property for an event listener attached to a run.")
+	String getRunListenerProperty(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "listenerName") @XmlElement(required = true) String listenerName,
+			@WebParam(name = "propertyName") @XmlElement(required = true) String propertyName)
+			throws UnknownRunException, NoListenerException;
+
+	/**
+	 * Set the value of a property for an event listener attached to a run.
+	 * 
+	 * @param runName
+	 *            The handle of the run.
+	 * @param listenerName
+	 *            The name of the listener attached.
+	 * @param propertyName
+	 *            The name of the property to write.
+	 * @param value
+	 *            The value to set the property to.
+	 * @throws UnknownRunException
+	 *             If the server doesn't know about the run or if the user is
+	 *             not permitted to see it.
+	 * @throws NoListenerException
+	 *             If no such listener exists, the listener has no such
+	 *             property, or the value is considered "unacceptable" in some
+	 *             way.
+	 * @throws NoUpdateException
+	 *             If the user is not allowed to make modifications to the run.
+	 */
+	@WSDLDocumentation("Set the value of a property for an event listener attached to a run.")
+	void setRunListenerProperty(
+			@WebParam(name = "runName") @XmlElement(required = true) String runName,
+			@WebParam(name = "listenerName") @XmlElement(required = true) String listenerName,
+			@WebParam(name = "propertyName") @XmlElement(required = true) String propertyName,
+			@WebParam(name = "propertyValue") @XmlElement(required = true) String value)
+			throws UnknownRunException, NoUpdateException, NoListenerException;
+
+	/**
+	 * Gets the status of the server. Follows the HELIO Monitoring Service
+	 * protocol.
+	 * 
+	 * @return A status string.
+	 */
+	@WSDLDocumentation("A simple way to get the status of the overall server.")
+	@WebResult(name = "ServerStatus")
+	String getServerStatus();
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/soap/WrappedWorkflow.java b/server-webapp/src/main/java/org/taverna/server/master/soap/WrappedWorkflow.java
new file mode 100644
index 0000000..cd06115
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/soap/WrappedWorkflow.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE.txt" for license terms.
+ */
+package org.taverna.server.master.soap;
+
+import static javax.xml.bind.annotation.XmlAccessType.NONE;
+import static org.apache.commons.io.IOUtils.closeQuietly;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+import javax.activation.DataHandler;
+import javax.activation.DataSource;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlMimeType;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+
+import org.taverna.server.master.common.Workflow;
+
+import uk.org.taverna.scufl2.api.io.ReaderException;
+import uk.org.taverna.scufl2.api.io.WorkflowBundleIO;
+import uk.org.taverna.scufl2.api.io.WriterException;
+
+/**
+ * An MTOM-capable description of how to transfer the contents of a file.
+ * 
+ * @author Donal Fellows
+ */
+@XmlType(name = "WorkflowReference")
+@XmlAccessorType(NONE)
+public class WrappedWorkflow {
+	@XmlMimeType("application/octet-stream")
+	// JAXB bug: must be this
+	public DataHandler workflowData;
+	Workflow workflow;
+
+	/**
+	 * Initialize the contents of this descriptor from the given file and
+	 * content type.
+	 * 
+	 * @param workflow
+	 *            The workflow that is to be reported.
+	 */
+	public void setWorkflow(Workflow workflow) {
+		workflowData = new DataHandler(new WorkflowSource(workflow));
+	}
+
+	@XmlTransient
+	public Workflow getWorkflow() throws IOException {
+		if (workflow != null)
+			return workflow;
+		try {
+			return new Workflow(new WorkflowBundleIO().readBundle(
+					workflowData.getInputStream(), null));
+		} catch (ReaderException e) {
+			throw new IOException("problem converting to scufl2 bundle", e);
+		}
+	}
+}
+
+/**
+ * A data source that knows how to deliver a workflow.
+ * 
+ * @author Donal Fellows
+ */
+class WorkflowSource implements DataSource {
+	WorkflowSource(Workflow workflow) {
+		this.wf = workflow;
+		this.io = new WorkflowBundleIO();
+	}
+
+	Workflow wf;
+	final WorkflowBundleIO io;
+
+	@Override
+	public String getContentType() {
+		return wf.getPreferredContentType().getContentType();
+	}
+
+	@Override
+	public String getName() {
+		switch (wf.getPreferredContentType()) {
+		case SCUFL2:
+			return "workflow.scufl2";
+		case T2FLOW:
+			return "workflow.t2flow";
+		default:
+			return "workflow";
+		}
+	}
+
+	@Override
+	public InputStream getInputStream() throws IOException {
+		PipedInputStream is = new PipedInputStream();
+		final OutputStream os = new PipedOutputStream(is);
+		new Worker() {
+			@Override
+			public void doWork() throws WriterException, IOException {
+				io.writeBundle(wf.getScufl2Workflow(), os, wf
+						.getPreferredContentType().getContentType());
+			}
+
+			@Override
+			public void doneWork() {
+				closeQuietly(os);
+			}
+		};
+		return is;
+	}
+
+	@Override
+	public OutputStream getOutputStream() throws IOException {
+		final PipedInputStream is = new PipedInputStream();
+		OutputStream os = new PipedOutputStream(is);
+		new Worker() {
+			@Override
+			public void doWork() throws IOException, ReaderException {
+				wf = new Workflow(io.readBundle(is, null));
+			}
+
+			@Override
+			public void doneWork() {
+				closeQuietly(is);
+			}
+		};
+		return os;
+	}
+
+	static abstract class Worker extends Thread {
+		public Worker() {
+			setDaemon(true);
+			start();
+		}
+
+		public abstract void doWork() throws Exception;
+
+		public abstract void doneWork();
+
+		@Override
+		public void run() {
+			try {
+				doWork();
+			} catch (Exception e) {
+				// do nothing.
+			} finally {
+				doneWork();
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/soap/ZippedDirectory.java b/server-webapp/src/main/java/org/taverna/server/master/soap/ZippedDirectory.java
new file mode 100644
index 0000000..90fe51b
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/soap/ZippedDirectory.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.soap;
+
+import static org.taverna.server.master.api.ContentTypes.APPLICATION_ZIP_TYPE;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.activation.DataHandler;
+import javax.activation.DataSource;
+import javax.activation.UnsupportedDataTypeException;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlMimeType;
+import javax.xml.bind.annotation.XmlType;
+
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.interfaces.Directory;
+
+/**
+ * An MTOM-capable description of how to transfer the zipped contents of a
+ * directory.
+ * 
+ * @author Donal Fellows
+ * @see Directory#getContentsAsZip()
+ */
+@XmlType(name = "ZippedDirectory")
+public class ZippedDirectory {
+	@XmlElement
+	public String name;
+	@XmlMimeType("application/octet-stream")
+	// JAXB bug: must be this
+	public DataHandler fileData;
+
+	public ZippedDirectory() {
+	}
+
+	/**
+	 * Initialise the contents of this descriptor from the given directory.
+	 * 
+	 * @param dir
+	 *            The directory that is to be reported.
+	 */
+	public ZippedDirectory(Directory dir) {
+		name = dir.getFullName();
+		fileData = new DataHandler(new ZipSource(dir));
+	}
+}
+
+/**
+ * A data source that knows how to communicate with the Taverna Server back-end.
+ * 
+ * @author Donal Fellows
+ */
+class ZipSource implements DataSource {
+	ZipSource(Directory d) {
+		this.d = d;
+	}
+
+	private final Directory d;
+
+	@Override
+	public String getContentType() {
+		return APPLICATION_ZIP_TYPE.toString();
+	}
+
+	@Override
+	public String getName() {
+		return d.getName();
+	}
+
+	@Override
+	public InputStream getInputStream() throws IOException {
+		try {
+			return d.getContentsAsZip();
+		} catch (FilesystemAccessException e) {
+			throw new IOException(e);
+		}
+	}
+
+	@Override
+	public OutputStream getOutputStream() throws IOException {
+		throw new UnsupportedDataTypeException();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/soap/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/soap/package-info.java
new file mode 100644
index 0000000..51d9b69
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/soap/package-info.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * This package contains the SOAP interface to Taverna Server.
+ * @author Donal Fellows
+ */
+@XmlSchema(namespace = SERVER_SOAP, elementFormDefault = QUALIFIED, attributeFormDefault = QUALIFIED, xmlns = {
+		@XmlNs(prefix = "xlink", namespaceURI = XLINK),
+		@XmlNs(prefix = "ts", namespaceURI = SERVER),
+		@XmlNs(prefix = "ts-rest", namespaceURI = SERVER_REST),
+		@XmlNs(prefix = "ts-soap", namespaceURI = SERVER_SOAP),
+		@XmlNs(prefix = "port", namespaceURI = DATA),
+		@XmlNs(prefix = "feed", namespaceURI = FEED),
+		@XmlNs(prefix = "admin", namespaceURI = ADMIN) })
+package org.taverna.server.master.soap;
+
+import static javax.xml.bind.annotation.XmlNsForm.QUALIFIED;
+import static org.taverna.server.master.common.Namespaces.ADMIN;
+import static org.taverna.server.master.common.Namespaces.FEED;
+import static org.taverna.server.master.common.Namespaces.SERVER;
+import static org.taverna.server.master.common.Namespaces.SERVER_REST;
+import static org.taverna.server.master.common.Namespaces.SERVER_SOAP;
+import static org.taverna.server.master.common.Namespaces.XLINK;
+import static org.taverna.server.port_description.Namespaces.DATA;
+
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlSchema;
+
diff --git a/server-webapp/src/main/java/org/taverna/server/master/usage/UsageRecord.java b/server-webapp/src/main/java/org/taverna/server/master/usage/UsageRecord.java
new file mode 100644
index 0000000..487dbd0
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/usage/UsageRecord.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.usage;
+
+import java.util.Date;
+
+import javax.jdo.annotations.Column;
+import javax.jdo.annotations.Index;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.PrimaryKey;
+import javax.jdo.annotations.Queries;
+import javax.jdo.annotations.Query;
+import javax.xml.bind.JAXBException;
+
+import org.ogf.usage.JobUsageRecord;
+
+/**
+ * A usage record as recorded in the database.
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceCapable(table = "USAGE_RECORD_LOG", schema = "UR", cacheable = "true")
+@Queries({ @Query(name = "allByDate", value = "SELECT USAGE_RECORD FROM UR.USAGE_RECORD_LOG ORDER BY CREATE_DATE", resultClass = String.class, unmodifiable = "true", unique = "false", language = "SQL") })
+public class UsageRecord {
+	/**
+	 * Create an empty usage record database entry.
+	 */
+	public UsageRecord() {
+	}
+
+	/**
+	 * Create a usage record database entry that is populated from the given UR.
+	 * 
+	 * @param usageRecord
+	 *            The originating usage record.
+	 * @throws JAXBException
+	 *             If deserialization of the record fails.
+	 */
+	public UsageRecord(String usageRecord) throws JAXBException {
+		JobUsageRecord jur = JobUsageRecord.unmarshal(usageRecord);
+		setUsageRecord(usageRecord);
+		setCreateDate(jur.getRecordIdentity().getCreateTime()
+				.toGregorianCalendar().getTime());
+		setId(jur.getRecordIdentity().getRecordId());
+		setUserid(jur.getUserIdentity().get(0).getLocalUserId());
+	}
+
+	/**
+	 * Create a usage record database entry that is populated from the given UR.
+	 * 
+	 * @param usageRecord
+	 *            The originating usage record.
+	 * @throws JAXBException
+	 *             If serialization of the record fails.
+	 */
+	public UsageRecord(JobUsageRecord usageRecord) throws JAXBException {
+		setUsageRecord(usageRecord.marshal());
+		setCreateDate(usageRecord.getRecordIdentity().getCreateTime()
+				.toGregorianCalendar().getTime());
+		setId(usageRecord.getRecordIdentity().getRecordId());
+		setUserid(usageRecord.getUserIdentity().get(0).getLocalUserId());
+	}
+
+	@PrimaryKey
+	@Column(name = "ID", length = 40)
+	private String id;
+
+	@Persistent
+	@Index(name = "USERID_IDX")
+	@Column(name = "USERID", length = 24)
+	private String userid;
+
+	@Persistent
+	@Index(name = "CREATE_IDX")
+	@Column(name = "CREATE_DATE")
+	private Date createDate;
+
+	@Persistent
+	@Column(name = "USAGE_RECORD", length = 32000)
+	// TODO Consider moving to BLOB (CLOB?) type
+	private String usageRecord;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getUserid() {
+		return userid;
+	}
+
+	public void setUserid(String userid) {
+		this.userid = userid;
+	}
+
+	public Date getCreateDate() {
+		return createDate;
+	}
+
+	public void setCreateDate(Date createDate) {
+		this.createDate = createDate;
+	}
+
+	public String getUsageRecord() {
+		return usageRecord;
+	}
+
+	public void setUsageRecord(String usageRecord) {
+		this.usageRecord = usageRecord;
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/usage/UsageRecordRecorder.java b/server-webapp/src/main/java/org/taverna/server/master/usage/UsageRecordRecorder.java
new file mode 100644
index 0000000..12de304
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/usage/UsageRecordRecorder.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.usage;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.PreDestroy;
+import javax.xml.bind.JAXBException;
+
+import org.apache.commons.logging.Log;
+import org.ogf.usage.JobUsageRecord;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.api.ManagementModel;
+import org.taverna.server.master.utils.Contextualizer;
+import org.taverna.server.master.utils.JDOSupport;
+
+/**
+ * A simple state-aware writer of usage records. It just appends them, one by
+ * one, to the file whose name is stored in the state.
+ * 
+ * @author Donal Fellows
+ */
+public class UsageRecordRecorder extends JDOSupport<UsageRecord> {
+	private Log log = getLog("Taverna.Server.Webapp");
+	public UsageRecordRecorder() {
+		super(UsageRecord.class);
+	}
+
+	private String logFile = null;
+	private boolean disableDB = false;
+	private ManagementModel state;
+	private Contextualizer contextualizer;
+	private String logDestination;
+	private PrintWriter writer;
+	private Object lock = new Object();
+	private UsageRecordRecorder self;
+
+	/**
+	 * @param state
+	 *            the state to set
+	 */
+	@Required
+	public void setState(ManagementModel state) {
+		this.state = state;
+	}
+
+	@Required
+	public void setSelf(UsageRecordRecorder self) {
+		this.self = self;
+	}
+
+	public void setLogFile(String logFile) {
+		this.logFile = (logFile == null || logFile.equals("none")) ? null : logFile;
+	}
+
+	public void setDisableDB(String disable) {
+		disableDB = "yes".equalsIgnoreCase(disable);
+	}
+
+	/**
+	 * @param contextualizer
+	 *            the system's contextualizer, used to allow making the UR dump
+	 *            file be placed relative to the webapp.
+	 */
+	@Required
+	public void setContextualizer(Contextualizer contextualizer) {
+		this.contextualizer = contextualizer;
+	}
+
+	/**
+	 * Accept a usage record for recording.
+	 * 
+	 * @param usageRecord
+	 *            The serialized usage record to record.
+	 */
+	public void storeUsageRecord(String usageRecord) {
+		String logfile = state.getUsageRecordLogFile();
+		if (logfile == null)
+			logfile = this.logFile;
+		if (logfile != null) {
+			logfile = contextualizer.contextualize(logfile);
+			synchronized (lock) {
+				if (!logfile.equals(logDestination)) {
+					if (writer != null) {
+						writer.close();
+						writer = null;
+					}
+					try {
+						writer = new PrintWriter(new FileWriter(logfile));
+						logDestination = logfile;
+					} catch (IOException e) {
+						log.warn("failed to open usage record log file", e);
+					}
+				}
+				if (writer != null) {
+					writer.println(usageRecord);
+					writer.flush();
+				}
+			}
+		}
+
+		if (!disableDB)
+			saveURtoDB(usageRecord);
+	}
+
+	/**
+	 * How to save a usage record to the database.
+	 * 
+	 * @param usageRecord
+	 *            The serialized usage record to save.
+	 */
+	protected void saveURtoDB(String usageRecord) {
+		UsageRecord ur;
+		try {
+			ur = new UsageRecord(usageRecord);
+		} catch (JAXBException e) {
+			log.warn("failed to deserialize usage record", e);
+			return;
+		}
+
+		try {
+			self.saveURtoDB(ur);
+		} catch (RuntimeException e) {
+			log.warn("failed to save UR to database", e);
+		}
+	}
+
+	@WithinSingleTransaction
+	public void saveURtoDB(UsageRecord ur) {
+		persist(ur);
+	}
+
+	@WithinSingleTransaction
+	public List<JobUsageRecord> getUsageRecords() {
+		@SuppressWarnings("unchecked")
+		Collection<String> urs = (Collection<String>) namedQuery("allByDate")
+				.execute();
+		List<JobUsageRecord> result = new ArrayList<>();
+		for (String ur : urs)
+			try {
+				result.add(JobUsageRecord.unmarshal(ur));
+			} catch (JAXBException e) {
+				log.warn("failed to unmarshal UR", e);
+			}
+		return result;
+	}
+
+	@PreDestroy
+	public void close() {
+		if (writer != null)
+			writer.close();
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/usage/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/usage/package-info.java
new file mode 100644
index 0000000..13e14cd
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/usage/package-info.java
@@ -0,0 +1,9 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Resource usage recording mechanism.
+ */
+package org.taverna.server.master.usage;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/CallTimeLogger.java b/server-webapp/src/main/java/org/taverna/server/master/utils/CallTimeLogger.java
new file mode 100644
index 0000000..a1ac04a
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/CallTimeLogger.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import static java.lang.String.format;
+import static java.lang.System.nanoTime;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.apache.commons.logging.Log;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.taverna.server.master.common.version.Version;
+
+/**
+ * This class is responsible for timing all invocations of publicly-exposed
+ * methods of the webapp. It's connected to the webapp through an AspectJ-style
+ * pointcut that targets a custom annotation.
+ * 
+ * @author Donal Fellows
+ */
+@Aspect
+@ManagedResource(objectName = JMX_ROOT + "PerformanceMonitor", description = "The performance monitor for Taverna Server "
+		+ Version.JAVA
+		+ ". Writes to application log using the category 'Taverna.Server.Performance'.")
+public class CallTimeLogger {
+	private long threshold = 4000000;
+	private Log log = getLog("Taverna.Server.Performance");
+
+	@ManagedAttribute(description = "Threshold beneath which monitored call times are not logged. In nanoseconds.")
+	public long getThreshold() {
+		return threshold;
+	}
+
+	@ManagedAttribute(description = "Threshold beneath which monitored call times are not logged. In nanoseconds.")
+	public void setThreshold(long threshold) {
+		this.threshold = threshold;
+	}
+
+	/**
+	 * The timer for this aspect. The wrapped invocation will be timed, and a
+	 * log message written if the configured threshold is exceeded.
+	 * 
+	 * @param call
+	 *            The call being wrapped.
+	 * @return The result of the call.
+	 * @throws Throwable
+	 *             If anything goes wrong with the wrapped call.
+	 * @see System#nanoTime()
+	 */
+	@Around("@annotation(org.taverna.server.master.utils.CallTimeLogger.PerfLogged)")
+	public Object time(ProceedingJoinPoint call) throws Throwable {
+		long fore = nanoTime();
+		try {
+			return call.proceed();
+		} finally {
+			long aft = nanoTime();
+			long elapsed = aft - fore;
+			if (elapsed > threshold)
+				log.info(format("call to %s took %.3fms", call.toShortString(),
+						elapsed / 1000000.0));
+		}
+	}
+
+	/**
+	 * Mark methods that should be counted by the invocation counter.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@Retention(RUNTIME)
+	@Documented
+	@Target(METHOD)
+	public static @interface PerfLogged {
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/CallTimingFilter.java b/server-webapp/src/main/java/org/taverna/server/master/utils/CallTimingFilter.java
new file mode 100644
index 0000000..aead6fa
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/CallTimingFilter.java
@@ -0,0 +1,65 @@
+/**
+ * 
+ */
+package org.taverna.server.master.utils;
+
+import static java.lang.String.format;
+import static java.lang.System.nanoTime;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+
+/**
+ * Logs the time it takes to service HTTP calls into Taverna Server.
+ * <p>
+ * This class is currently not used.
+ * 
+ * @author Donal Fellows
+ */
+public class CallTimingFilter implements Filter {
+	private Log log;
+	private String name;
+
+	@Override
+	public void init(FilterConfig filterConfig) throws ServletException {
+		log = getLog("Taverna.Server.Performance");
+		name = filterConfig.getInitParameter("name");
+	}
+
+	@Override
+	public void doFilter(ServletRequest request, ServletResponse response,
+			FilterChain chain) throws IOException, ServletException {
+		if (request instanceof HttpServletRequest)
+			doFilter((HttpServletRequest) request,
+					(HttpServletResponse) response, chain);
+		else
+			chain.doFilter(request, response);
+	}
+
+	public void doFilter(HttpServletRequest request,
+			HttpServletResponse response, FilterChain chain)
+			throws IOException, ServletException {
+		long start = nanoTime();
+		chain.doFilter(request, response);
+		long elapsedTime = nanoTime() - start;
+		log.info(format("%s call to %s %s took %.3fms", name,
+				request.getMethod(), request.getRequestURI(),
+				elapsedTime / 1000000.0));
+	}
+
+	@Override
+	public void destroy() {
+		log = null;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/CapabilityLister.java b/server-webapp/src/main/java/org/taverna/server/master/utils/CapabilityLister.java
new file mode 100644
index 0000000..54b0420
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/CapabilityLister.java
@@ -0,0 +1,44 @@
+package org.taverna.server.master.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Properties;
+
+import javax.annotation.PostConstruct;
+
+import org.taverna.server.master.common.Capability;
+
+/**
+ * Utility for listing the capabilities supported by this Taverna Server
+ * installation.
+ * 
+ * @author Donal Fellows
+ */
+public class CapabilityLister {
+	public static final String CAPABILITY_RESOURCE_FILE = "/capabilities.properties";
+	private Properties properties = new Properties();
+
+	@PostConstruct
+	void loadCapabilities() throws IOException {
+		try (InputStream is = getClass().getResourceAsStream(
+				CAPABILITY_RESOURCE_FILE)) {
+			if (is != null)
+				properties.load(is);
+		}
+	}
+
+	public List<Capability> getCapabilities() {
+		List<Capability> caps = new ArrayList<>();
+		for (Entry<Object, Object> entry : properties.entrySet()) {
+			Capability c = new Capability();
+			c.capability = URI.create(entry.getKey().toString());
+			c.version = entry.getValue().toString();
+			caps.add(c);
+		}
+		return caps;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/CertificateChainFetcher.java b/server-webapp/src/main/java/org/taverna/server/master/utils/CertificateChainFetcher.java
new file mode 100644
index 0000000..c27502f
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/CertificateChainFetcher.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableList;
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+import javax.xml.ws.Holder;
+
+/**
+ * Obtains the certificate chain for an arbitrary SSL service. Maintains a
+ * cache.
+ * 
+ * @author Donal Fellows
+ */
+public class CertificateChainFetcher {
+	public String getProtocol() {
+		return protocol;
+	}
+
+	public void setProtocol(String protocol) {
+		this.protocol = protocol;
+	}
+
+	public String getKeystoreType() {
+		return keystoreType;
+	}
+
+	public void setKeystoreType(String keystoreType) {
+		this.keystoreType = keystoreType;
+	}
+
+	public String getAlgorithm() {
+		return algorithm;
+	}
+
+	public void setAlgorithm(String algorithm) {
+		this.algorithm = algorithm;
+	}
+
+	public int getTimeout() {
+		return timeout;
+	}
+
+	public void setTimeout(int timeout) {
+		this.timeout = timeout;
+	}
+
+	public void setSecure(boolean secure) {
+		this.secure = secure;
+	}
+
+	private boolean secure = true;
+	private String protocol = "TLS";
+	private String keystoreType = KeyStore.getDefaultType();
+	private String algorithm = TrustManagerFactory.getDefaultAlgorithm();
+	private int timeout = 10000;
+
+	/**
+	 * Get the certificate chain for a service.
+	 * 
+	 * @param host
+	 *            The host (name or IP address) to contact the service on.
+	 * @param port
+	 *            The port to contact the service on.
+	 * @return The certificate chain, or <tt>null</tt> if no credentials are
+	 *         available.
+	 * @throws NoSuchAlgorithmException
+	 *             If the trust manager cannot be set up because of algorithm
+	 *             problems.
+	 * @throws KeyStoreException
+	 *             If the trust manager cannot be set up because of problems
+	 *             with the keystore type.
+	 * @throws CertificateException
+	 *             If a bad certificate is present in the default keystore;
+	 *             <i>should be impossible</i>.
+	 * @throws IOException
+	 *             If problems happen when trying to contact the service.
+	 * @throws KeyManagementException
+	 *             If the SSL context can't have its special context manager
+	 *             installed.
+	 */
+	private X509Certificate[] getCertificateChainForService(String host,
+			int port) throws NoSuchAlgorithmException, KeyStoreException,
+			CertificateException, IOException, KeyManagementException {
+		KeyStore ks = KeyStore.getInstance(keystoreType);
+		SSLContext context = SSLContext.getInstance(protocol);
+		TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
+		ks.load(null, null);
+		tmf.init(ks);
+		final Holder<X509Certificate[]> chain = new Holder<>();
+		final X509TrustManager defaultTrustManager = (X509TrustManager) tmf
+				.getTrustManagers()[0];
+		context.init(null, new TrustManager[] { new X509TrustManager() {
+			@Override
+			public void checkClientTrusted(X509Certificate[] clientChain,
+					String authType) throws CertificateException {
+				throw new UnsupportedOperationException();
+			}
+
+			@Override
+			public void checkServerTrusted(X509Certificate[] serverChain,
+					String authType) throws CertificateException {
+				chain.value = serverChain;
+				defaultTrustManager.checkServerTrusted(serverChain, authType);
+			}
+
+			@Override
+			public X509Certificate[] getAcceptedIssuers() {
+				throw new UnsupportedOperationException();
+			}
+		} }, null);
+		SSLSocketFactory factory = context.getSocketFactory();
+		try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
+			socket.setSoTimeout(timeout);
+			socket.startHandshake();
+		} catch (SSLException e) {
+			// Ignore
+		}
+		return chain.value;
+	}
+
+	private Map<URI, List<X509Certificate>> cache = new HashMap<>();
+
+	/**
+	 * Gets the certificate chain for a service identified by URI.
+	 * 
+	 * @param uri
+	 *            The URI of the (secure) service to identify.
+	 * @return The certificate chain. Will be <tt>null</tt> if the service is
+	 *         not secure.
+	 * @throws IOException
+	 *             If the service is unreachable or other connection problems
+	 *             occur.
+	 * @throws GeneralSecurityException
+	 *             If any of a number of security-related problems occur, such
+	 *             as an inability to match detailed security protocols.
+	 */
+	public List<X509Certificate> getTrustsForURI(URI uri) throws IOException,
+			GeneralSecurityException {
+		if (!secure)
+			return null;
+		synchronized (this) {
+			if (!cache.containsKey(uri)) {
+				int port = uri.getPort();
+				if (port == -1)
+					switch (uri.getScheme()) {
+					case "http":
+						port = 80;
+						break;
+					case "https":
+						port = 443;
+						break;
+					default:
+						return null;
+					}
+				X509Certificate[] chain = getCertificateChainForService(
+						uri.getHost(), port);
+				if (chain != null)
+					cache.put(uri, unmodifiableList(asList(chain)));
+				else
+					cache.put(uri, null);
+			}
+			return cache.get(uri);
+		}
+	}
+
+	/**
+	 * Flushes the cache.
+	 */
+	public void flushCache() {
+		synchronized (this) {
+			cache.clear();
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/Contextualizer.java b/server-webapp/src/main/java/org/taverna/server/master/utils/Contextualizer.java
new file mode 100644
index 0000000..884ff94
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/Contextualizer.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.UriInfo;
+
+import org.springframework.web.context.ServletContextAware;
+import org.taverna.server.master.common.version.Version;
+
+/**
+ * Convert a string (URL, etc) to a version that is contextualized to the
+ * web-application.
+ * 
+ * @author Donal Fellows
+ */
+public class Contextualizer implements ServletContextAware {
+	static final String ROOT_PLACEHOLDER = "%{WEBAPPROOT}";
+	static final String VERSION_PLACEHOLDER = "%{VERSION}";
+	static final String BASE_PLACEHOLDER = "%{BASEURL}";
+
+	/**
+	 * Apply the contextualization operation. This consists of replacing the
+	 * string <tt>{@value #ROOT_PLACEHOLDER}</tt> with the real root of the webapp.
+	 * 
+	 * @param input
+	 *            the string to contextualize
+	 * @return the contextualized string
+	 */
+	public String contextualize(String input) {
+		// Hack to work around bizarre CXF bug
+		String path = context.getRealPath("/").replace("%2D", "-");
+		return input.replace(ROOT_PLACEHOLDER, path).replace(
+				VERSION_PLACEHOLDER, Version.JAVA);
+	}
+
+	/**
+	 * Apply the contextualization operation. This consists of replacing the
+	 * string <tt>{@value #ROOT_PLACEHOLDER}</tt> with the real root of the
+	 * webapp.
+	 * 
+	 * @param ui
+	 *            Where to get information about the URL used to access the
+	 *            webapp.
+	 * @param input
+	 *            the string to contextualize
+	 * @return the contextualized string
+	 */
+	public String contextualize(UriInfo ui, String input) {
+		// Hack to work around bizarre CXF bug
+		String baseuri = ui.getBaseUri().toString().replace("%2D", "-");
+		if (baseuri.endsWith("/"))
+			baseuri = baseuri.substring(0, baseuri.length() - 1);
+		return contextualize(input).replace(BASE_PLACEHOLDER, baseuri);
+	}
+
+	private ServletContext context;
+
+	@Override
+	public void setServletContext(ServletContext servletContext) {
+		context = servletContext;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/DerbyUtils.java b/server-webapp/src/main/java/org/taverna/server/master/utils/DerbyUtils.java
new file mode 100644
index 0000000..f8b39e3
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/DerbyUtils.java
@@ -0,0 +1,68 @@
+package org.taverna.server.master.utils;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Utility class, used to make Derby less broken.
+ * 
+ * @see <a
+ *      href="http://stackoverflow.com/questions/1004327/getting-rid-of-derby-log">
+ *      Getting rid of derby.log </a>
+ * @see <a
+ *      href="http://stackoverflow.com/questions/3339736/set-system-property-with-spring-configuration-file">
+ *      Set system property with Spring configuration file </a>
+ */
+public class DerbyUtils {
+	/**
+	 * A writer that channels things on to the log.
+	 */
+	public static final Writer TO_LOG = new DBLog();
+	// Hack
+	public static final Writer DEV_NULL = TO_LOG;
+}
+
+class DBLog extends Writer {
+	private Log log = LogFactory.getLog("Taverna.Server.Database");
+	private StringBuilder sb = new StringBuilder();
+	private boolean closed = false;
+
+	@Override
+	public void write(char[] cbuf, int off, int len) throws IOException {
+		if (closed)
+			throw new EOFException();
+		if (!log.isInfoEnabled())
+			return;
+		sb.append(cbuf, off, len);
+		while (!closed) {
+			int idx = sb.indexOf("\n"), realIdx = idx;
+			if (idx < 0)
+				break;
+			char ch;
+			while (idx > 0 && ((ch = sb.charAt(idx - 1)) == '\r' || ch == ' ' || ch == '\t'))
+				idx--;
+			if (idx > 0)
+				log.info(sb.substring(0, idx));
+			sb.delete(0, realIdx + 1);
+		}
+	}
+
+	@Override
+	public void flush() throws IOException {
+		if (sb.length() > 0) {
+			log.info(sb.toString());
+			sb = new StringBuilder();
+		}
+	}
+
+	@Override
+	public void close() throws IOException {
+		flush();
+		closed = true;
+		sb = null;
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/FilenameUtils.java b/server-webapp/src/main/java/org/taverna/server/master/utils/FilenameUtils.java
new file mode 100644
index 0000000..19299e2
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/FilenameUtils.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import java.util.List;
+
+import javax.ws.rs.core.PathSegment;
+
+import org.taverna.server.master.common.DirEntryReference;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * Utility functions for getting entries from directories.
+ * 
+ * @author Donal Fellows
+ */
+public class FilenameUtils {
+	private static final String TYPE_ERROR = "trying to take subdirectory of file";
+	private static final String NO_FILE = "no such directory entry";
+	private static final String NOT_A_FILE = "not a file";
+	private static final String NOT_A_DIR = "not a directory";
+
+	/**
+	 * Get a named directory entry from a workflow run.
+	 * 
+	 * @param run
+	 *            The run whose working directory is to be used as the root of
+	 *            the search.
+	 * @param name
+	 *            The name of the directory entry to look up.
+	 * @return The directory entry whose name is equal to the last part of the
+	 *         path; an empty path will retrieve the working directory handle
+	 *         itself.
+	 * @throws NoDirectoryEntryException
+	 *             If there is no such entry.
+	 * @throws FilesystemAccessException
+	 *             If the directory isn't specified or isn't readable.
+	 */
+	public DirectoryEntry getDirEntry(TavernaRun run, String name)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		Directory dir = run.getWorkingDirectory();
+		if (name == null || name.isEmpty())
+			return dir;
+		DirectoryEntry found = dir;
+		boolean mustBeLast = false;
+
+		// Must be nested loops; avoids problems with %-encoded "/" chars
+		for (String bit : name.split("/")) {
+			if (mustBeLast)
+				throw new FilesystemAccessException(TYPE_ERROR);
+			found = getEntryFromDir(bit, dir);
+			dir = null;
+			if (found instanceof Directory) {
+				dir = (Directory) found;
+				mustBeLast = false;
+			} else
+				mustBeLast = true;
+		}
+		return found;
+	}
+
+	/**
+	 * Get a named directory entry from a workflow run.
+	 * 
+	 * @param run
+	 *            The run whose working directory is to be used as the root of
+	 *            the search.
+	 * @param d
+	 *            The path segments describing what to look up.
+	 * @return The directory entry whose name is equal to the last part of the
+	 *         path; an empty path will retrieve the working directory handle
+	 *         itself.
+	 * @throws NoDirectoryEntryException
+	 *             If there is no such entry.
+	 * @throws FilesystemAccessException
+	 *             If the directory isn't specified or isn't readable.
+	 */
+	public DirectoryEntry getDirEntry(TavernaRun run, List<PathSegment> d)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		Directory dir = run.getWorkingDirectory();
+		if (d == null || d.isEmpty())
+			return dir;
+		DirectoryEntry found = dir;
+		boolean mustBeLast = false;
+
+		// Must be nested loops; avoids problems with %-encoded "/" chars
+		for (PathSegment segment : d)
+			for (String bit : segment.getPath().split("/")) {
+				if (mustBeLast)
+					throw new FilesystemAccessException(TYPE_ERROR);
+				found = getEntryFromDir(bit, dir);
+				dir = null;
+				if (found instanceof Directory) {
+					dir = (Directory) found;
+					mustBeLast = false;
+				} else
+					mustBeLast = true;
+			}
+		return found;
+	}
+
+	/**
+	 * Get a named directory entry from a workflow run.
+	 * 
+	 * @param run
+	 *            The run whose working directory is to be used as the root of
+	 *            the search.
+	 * @param d
+	 *            The directory reference describing what to look up.
+	 * @return The directory entry whose name is equal to the last part of the
+	 *         path in the directory reference; an empty path will retrieve the
+	 *         working directory handle itself.
+	 * @throws FilesystemAccessException
+	 *             If the directory isn't specified or isn't readable.
+	 * @throws NoDirectoryEntryException
+	 *             If there is no such entry.
+	 */
+	public DirectoryEntry getDirEntry(TavernaRun run, DirEntryReference d)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		Directory dir = run.getWorkingDirectory();
+		if (d == null || d.path == null || d.path.isEmpty())
+			return dir;
+		DirectoryEntry found = dir;
+		boolean mustBeLast = false;
+
+		for (String bit : d.path.split("/")) {
+			if (mustBeLast)
+				throw new FilesystemAccessException(TYPE_ERROR);
+			found = getEntryFromDir(bit, dir);
+			dir = null;
+			if (found instanceof Directory) {
+				dir = (Directory) found;
+				mustBeLast = false;
+			} else
+				mustBeLast = true;
+		}
+		return found;
+	}
+
+	/**
+	 * Get a named directory entry from a directory.
+	 * 
+	 * @param name
+	 *            The name of the entry; must be "<tt>/</tt>"-free.
+	 * @param dir
+	 *            The directory to look in.
+	 * @return The directory entry whose name is equal to the given name.
+	 * @throws NoDirectoryEntryException
+	 *             If there is no such entry.
+	 * @throws FilesystemAccessException
+	 *             If the directory isn't specified or isn't readable.
+	 */
+	private DirectoryEntry getEntryFromDir(String name, Directory dir)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		if (dir == null)
+			throw new FilesystemAccessException(NO_FILE);
+		for (DirectoryEntry entry : dir.getContents())
+			if (entry.getName().equals(name))
+				return entry;
+		throw new NoDirectoryEntryException(NO_FILE);
+	}
+
+	/**
+	 * Get a named directory from a workflow run.
+	 * 
+	 * @param run
+	 *            The run whose working directory is to be used as the root of
+	 *            the search.
+	 * @param d
+	 *            The directory reference describing what to look up.
+	 * @return The directory whose name is equal to the last part of the path in
+	 *         the directory reference; an empty path will retrieve the working
+	 *         directory handle itself.
+	 * @throws FilesystemAccessException
+	 *             If the directory isn't specified or isn't readable, or if the
+	 *             name doesn't refer to a directory.
+	 * @throws NoDirectoryEntryException
+	 *             If there is no such entry.
+	 */
+	public Directory getDirectory(TavernaRun run, DirEntryReference d)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		DirectoryEntry dirEntry = getDirEntry(run, d);
+		if (dirEntry instanceof Directory)
+			return (Directory) dirEntry;
+		throw new FilesystemAccessException(NOT_A_DIR);
+	}
+
+	/**
+	 * Get a named directory from a workflow run.
+	 * 
+	 * @param run
+	 *            The run whose working directory is to be used as the root of
+	 *            the search.
+	 * @param name
+	 *            The name of the directory to look up.
+	 * @return The directory.
+	 * @throws FilesystemAccessException
+	 *             If the directory isn't specified or isn't readable, or if the
+	 *             name doesn't refer to a directory.
+	 * @throws NoDirectoryEntryException
+	 *             If there is no such entry.
+	 */
+	public Directory getDirectory(TavernaRun run, String name)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		DirectoryEntry dirEntry = getDirEntry(run, name);
+		if (dirEntry instanceof Directory)
+			return (Directory) dirEntry;
+		throw new FilesystemAccessException(NOT_A_DIR);
+	}
+
+	/**
+	 * Get a named file from a workflow run.
+	 * 
+	 * @param run
+	 *            The run whose working directory is to be used as the root of
+	 *            the search.
+	 * @param d
+	 *            The directory reference describing what to look up.
+	 * @return The file whose name is equal to the last part of the path in the
+	 *         directory reference; an empty path will retrieve the working
+	 *         directory handle itself.
+	 * @throws FilesystemAccessException
+	 *             If the file isn't specified or isn't readable, or if the name
+	 *             doesn't refer to a file.
+	 * @throws NoDirectoryEntryException
+	 *             If there is no such entry.
+	 */
+	public File getFile(TavernaRun run, DirEntryReference d)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		DirectoryEntry dirEntry = getDirEntry(run, d);
+		if (dirEntry instanceof File)
+			return (File) dirEntry;
+		throw new FilesystemAccessException(NOT_A_FILE);
+	}
+
+	/**
+	 * Get a named file from a workflow run.
+	 * 
+	 * @param run
+	 *            The run whose working directory is to be used as the root of
+	 *            the search.
+	 * @param name
+	 *            The name of the file to look up.
+	 * @return The file whose name is equal to the last part of the path in the
+	 *         directory reference; an empty path will retrieve the working
+	 *         directory handle itself.
+	 * @throws FilesystemAccessException
+	 *             If the file isn't specified or isn't readable, or if the name
+	 *             doesn't refer to a file.
+	 * @throws NoDirectoryEntryException
+	 *             If there is no such entry.
+	 */
+	public File getFile(TavernaRun run, String name)
+			throws FilesystemAccessException, NoDirectoryEntryException {
+		DirectoryEntry dirEntry = getDirEntry(run, name);
+		if (dirEntry instanceof File)
+			return (File) dirEntry;
+		throw new FilesystemAccessException(NOT_A_FILE);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/FlushThreadLocalCacheInterceptor.java b/server-webapp/src/main/java/org/taverna/server/master/utils/FlushThreadLocalCacheInterceptor.java
new file mode 100644
index 0000000..ff52e81
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/FlushThreadLocalCacheInterceptor.java
@@ -0,0 +1,18 @@
+package org.taverna.server.master.utils;
+
+import org.apache.cxf.jaxrs.provider.ProviderFactory;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.AbstractPhaseInterceptor;
+import org.apache.cxf.phase.Phase;
+
+public class FlushThreadLocalCacheInterceptor extends
+		AbstractPhaseInterceptor<Message> {
+	public FlushThreadLocalCacheInterceptor() {
+		super(Phase.USER_LOGICAL_ENDING);
+	}
+
+	@Override
+	public void handleMessage(Message message) {
+		ProviderFactory.getInstance(message).clearThreadLocalProxies();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/InvocationCounter.java b/server-webapp/src/main/java/org/taverna/server/master/utils/InvocationCounter.java
new file mode 100644
index 0000000..55a260b
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/InvocationCounter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+
+/**
+ * This class is responsible for counting all invocations of publicly-exposed
+ * methods of the webapp. It's connected to the webapp primarily through an
+ * AspectJ-style pointcut.
+ * 
+ * @author Donal Fellows
+ */
+@Aspect
+public class InvocationCounter {
+	private int count;
+
+	@Before("@annotation(org.taverna.server.master.utils.InvocationCounter.CallCounted)")
+	public synchronized void count() {
+		count++;
+	}
+
+	public synchronized int getCount() {
+		return count;
+	}
+
+	/**
+	 * Mark methods that should be counted by the invocation counter.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@Retention(RUNTIME)
+	@Documented
+	@Target(METHOD)
+	public static @interface CallCounted {
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/JCECheck.java b/server-webapp/src/main/java/org/taverna/server/master/utils/JCECheck.java
new file mode 100644
index 0000000..252e316
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/JCECheck.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import static java.lang.Integer.MAX_VALUE;
+import static javax.crypto.Cipher.getMaxAllowedKeyLength;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.annotation.PostConstruct;
+
+import org.apache.commons.logging.Log;
+
+/**
+ * Trivial bean that checks for whether the JCE policy files that allow
+ * unlimited strength security are present, and warns in the log if not.
+ * 
+ * @author Donal Fellows
+ */
+public class JCECheck {
+	/**
+	 * Write a message to the log that says whether an unlimited strength
+	 * {@linkplain #Cipher cipher} is present. This is the official proxy for
+	 * whether the unlimited strength JCE policy files have been installed; if
+	 * absent, the message is logged as a warning, otherwise it is just
+	 * informational.
+	 */
+	@PostConstruct
+	public void checkForUnlimitedJCE() {
+		Log log = getLog("Taverna.Server.Utils");
+
+		try {
+			if (getMaxAllowedKeyLength("AES") < MAX_VALUE)
+				log.warn("maximum key length very short; unlimited "
+						+ "strength JCE policy files maybe missing");
+			else
+				log.info("unlimited strength JCE policy in place");
+		} catch (GeneralSecurityException e) {
+			log.warn("problem computing key length limits!", e);
+		}
+	}
+
+	/**
+	 * @return Whether the unlimited strength JCE policy files are present (or
+	 *         rather whether an unlimited strength {@linkplain #Cipher cipher}
+	 *         is permitted).
+	 */
+	public boolean isUnlimitedStrength() {
+		try {
+			return getMaxAllowedKeyLength("AES") == MAX_VALUE;
+		} catch (NoSuchAlgorithmException e) {
+			return false;
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/JDOSupport.java b/server-webapp/src/main/java/org/taverna/server/master/utils/JDOSupport.java
new file mode 100644
index 0000000..ba9ec81
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/JDOSupport.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.WeakHashMap;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.PreDestroy;
+import javax.jdo.JDOException;
+import javax.jdo.PersistenceManager;
+import javax.jdo.PersistenceManagerFactory;
+import javax.jdo.Query;
+import javax.jdo.Transaction;
+
+import org.apache.commons.logging.Log;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.beans.factory.annotation.Required;
+
+/**
+ * Simple support class that wraps up and provides access to the correct parts
+ * of JDO.
+ * 
+ * @author Donal Fellows
+ * 
+ * @param &lt;T&gt; The context class that the subclass will be working with.
+ */
+public abstract class JDOSupport<T> {
+	private Class<T> contextClass;
+	private PersistenceManagerBuilder pmb;
+
+	/**
+	 * Instantiate this class, supplying it a handle to the class that will be
+	 * used to provide context for queries and accesses.
+	 * 
+	 * @param contextClass
+	 *            Must match the type parameter to the class itself.
+	 */
+	protected JDOSupport(@Nonnull Class<T> contextClass) {
+		this.contextClass = contextClass;
+	}
+
+	/**
+	 * @param persistenceManagerBuilder
+	 *            The JDO engine to use for managing persistence.
+	 */
+	@Required
+	public void setPersistenceManagerBuilder(
+			PersistenceManagerBuilder persistenceManagerBuilder) {
+		pmb = persistenceManagerBuilder;
+	}
+
+	private PersistenceManager pm() {
+		if (isPersistent())
+			return pmb.getPersistenceManager();
+		return null;
+	}
+
+	/**
+	 * Has this class actually been configured with a persistence manager by
+	 * Spring?
+	 * 
+	 * @return Whether there is a persistence manager installed.
+	 */
+	protected boolean isPersistent() {
+		return pmb != null;
+	}
+
+	/**
+	 * Get an instance of a query in JDOQL.
+	 * 
+	 * @param filter
+	 *            The filter part of the query.
+	 * @return The query, which should be executed to retrieve the results.
+	 */
+	@Nonnull
+	protected Query query(@Nonnull String filter) {
+		return pm().newQuery(contextClass, filter);
+	}
+
+	/**
+	 * Get an instance of a named query attached to the context class (as an
+	 * annotation).
+	 * 
+	 * @param name
+	 *            The name of the query.
+	 * @return The query, which should be executed to retrieve the results.
+	 * @see javax.jdo.annotations.Query
+	 */
+	@Nonnull
+	protected Query namedQuery(@Nonnull String name) {
+		return pm().newNamedQuery(contextClass, name);
+	}
+
+	/**
+	 * Make an instance of the context class persist in the database. It's
+	 * identity must not already exist.
+	 * 
+	 * @param value
+	 *            The instance to persist.
+	 * @return The persistence-coupled instance.
+	 */
+	@Nullable
+	protected T persist(@Nullable T value) {
+		if (value == null)
+			return null;
+		return pm().makePersistent(value);
+	}
+
+	/**
+	 * Make a non-persistent (i.e., will hold its value past the end of the
+	 * transaction) copy of a persistence-coupled instance of the context class.
+	 * 
+	 * @param value
+	 *            The value to decouple.
+	 * @return The non-persistent copy.
+	 */
+	@Nullable
+	protected T detach(@Nullable T value) {
+		if (value == null)
+			return null;
+		return pm().detachCopy(value);
+	}
+
+	/**
+	 * Look up an instance of the context class by its identity.
+	 * 
+	 * @param id
+	 *            The identity of the object.
+	 * @return The instance, which is persistence-coupled.
+	 */
+	@Nullable
+	protected T getById(Object id) {
+		try {
+			return pm().getObjectById(contextClass, id);
+		} catch (Exception e) {
+			return null;
+		}
+	}
+
+	/**
+	 * Delete a persistence-coupled instance of the context class.
+	 * 
+	 * @param value
+	 *            The value to delete.
+	 */
+	protected void delete(@Nullable T value) {
+		if (value != null)
+			pm().deletePersistent(value);
+	}
+
+	/**
+	 * Manages integration of JDO transactions with Spring.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@Aspect
+	public static class TransactionAspect {
+		private Object lock = new Object();
+		private Log log = getLog("Taverna.Server.Utils");
+		private volatile int txid;
+
+		@Around(value = "@annotation(org.taverna.server.master.utils.JDOSupport.WithinSingleTransaction) && target(support)", argNames = "support")
+		Object applyTransaction(ProceedingJoinPoint pjp, JDOSupport<?> support)
+				throws Throwable {
+			synchronized (lock) {
+				PersistenceManager pm = support.pm();
+				int id = ++txid;
+				Transaction tx = (pm == null) ? null : pm.currentTransaction();
+				if (tx != null && tx.isActive())
+					tx = null;
+				if (tx != null) {
+					if (log.isDebugEnabled())
+						log.debug("starting transaction #" + id);
+					tx.begin();
+				}
+				try {
+					Object result = pjp.proceed();
+					if (tx != null) {
+						tx.commit();
+						if (log.isDebugEnabled())
+							log.debug("committed transaction #" + id);
+					}
+					tx = null;
+					return result;
+				} catch (Throwable t) {
+					try {
+						if (tx != null) {
+							tx.rollback();
+							if (log.isDebugEnabled())
+								log.debug("rolled back transaction #" + id);
+						}
+					} catch (JDOException e) {
+						log.warn("rollback failed unexpectedly", e);
+					}
+					throw t;
+				}
+			}
+		}
+	}
+
+	/**
+	 * Mark a method (of a subclass of {@link JDOSupport}) as having a
+	 * transaction wrapped around it. The transactions are managed correctly in
+	 * the multi-threaded case.
+	 * 
+	 * @author Donal Fellows
+	 */
+	@Target(METHOD)
+	@Retention(RUNTIME)
+	@Documented
+	public @interface WithinSingleTransaction {
+	}
+
+	/**
+	 * Manages {@linkplain PersistenceManager persistence managers} in a way
+	 * that doesn't cause problems when the web application is unloaded.
+	 * 
+	 * @author Donal Fellows
+	 */
+	public static class PersistenceManagerBuilder {
+		private PersistenceManagerFactory pmf;
+		private WeakHashMap<Thread, PersistenceManager> cache = new WeakHashMap<>();
+
+		/**
+		 * @param persistenceManagerFactory
+		 *            The JDO engine to use for managing persistence.
+		 */
+		@Required
+		public void setPersistenceManagerFactory(
+				PersistenceManagerFactory persistenceManagerFactory) {
+			pmf = persistenceManagerFactory;
+		}
+
+		@Nonnull
+		public PersistenceManager getPersistenceManager() {
+			if (cache == null)
+				return pmf.getPersistenceManager();
+			Thread t = Thread.currentThread();
+			PersistenceManager pm = cache.get(t);
+			if (pm == null && pmf != null) {
+				pm = pmf.getPersistenceManager();
+				cache.put(t, pm);
+			}
+			return pm;
+		}
+
+		@PreDestroy
+		void clearThreadCache() {
+			WeakHashMap<Thread, PersistenceManager> cache = this.cache;
+			this.cache = null;
+			for (PersistenceManager pm : cache.values())
+				if (pm != null)
+					pm.close();
+			cache.clear();
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/LoggingDerbyAdapter.java b/server-webapp/src/main/java/org/taverna/server/master/utils/LoggingDerbyAdapter.java
new file mode 100644
index 0000000..a8aa937
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/LoggingDerbyAdapter.java
@@ -0,0 +1,138 @@
+package org.taverna.server.master.utils;
+
+import static java.lang.System.currentTimeMillis;
+import static java.lang.Thread.sleep;
+import static org.apache.commons.logging.LogFactory.getLog;
+
+import java.sql.DatabaseMetaData;
+import java.util.Properties;
+
+import org.apache.commons.logging.Log;
+import org.datanucleus.store.rdbms.adapter.DerbyAdapter;
+import org.datanucleus.store.rdbms.identifier.IdentifierFactory;
+import org.datanucleus.store.rdbms.key.CandidateKey;
+import org.datanucleus.store.rdbms.key.ForeignKey;
+import org.datanucleus.store.rdbms.key.Index;
+import org.datanucleus.store.rdbms.key.PrimaryKey;
+import org.datanucleus.store.rdbms.sql.SQLTable;
+import org.datanucleus.store.rdbms.table.Column;
+import org.datanucleus.store.rdbms.table.Table;
+import org.datanucleus.store.rdbms.table.TableImpl;
+import org.datanucleus.store.rdbms.table.ViewImpl;
+
+/**
+ * Evil hack to allow logging of the DDL spat out to Derby.
+ * 
+ * @author Donal Fellows
+ */
+public class LoggingDerbyAdapter extends DerbyAdapter {
+	Log log = getLog("Taverna.Server.SQL");
+
+	private StringBuilder ddl = new StringBuilder();
+	private volatile long timeout;
+	private Thread timer;
+
+	private synchronized void logDDL() {
+		if (ddl.length() > 0) {
+			log.info("Data definition language:\n" + ddl);
+			ddl.setLength(0);
+		}
+		timer = null;
+	}
+
+	private synchronized void doLog(String item) {
+		ddl.append(item);
+		if (!item.endsWith("\n"))
+			ddl.append('\n');
+		timeout = currentTimeMillis() + 5000;
+		if (timer == null)
+			timer = new OneShotThread("DDL logger timeout", new Runnable() {
+				@Override
+				public void run() {
+					try {
+						while (timeout > currentTimeMillis())
+							sleep(1000);
+					} catch (InterruptedException e) {
+						// Ignore
+					}
+					logDDL();
+				}
+			});
+	}
+
+	/**
+	 * Creates an Apache Derby adapter based on the given metadata which logs
+	 * the DDL it creates.
+	 */
+	public LoggingDerbyAdapter(DatabaseMetaData metadata) {
+		super(metadata);
+	}
+
+	@Override
+	public String getCreateTableStatement(TableImpl table, Column[] columns,
+			Properties props, IdentifierFactory factory) {
+		String statement = super.getCreateTableStatement(table, columns, props,
+				factory);
+		doLog(statement);
+		return statement;
+	}
+
+	@Override
+	public String getCreateIndexStatement(Index index, IdentifierFactory factory) {
+		String statement = super.getCreateIndexStatement(index, factory);
+		doLog(statement);
+		return statement;
+	}
+
+	@Override
+	public String getAddCandidateKeyStatement(CandidateKey ck,
+			IdentifierFactory factory) {
+		String statement = super.getAddCandidateKeyStatement(ck, factory);
+		doLog(statement);
+		return statement;
+	}
+
+	@Override
+	public String getAddPrimaryKeyStatement(PrimaryKey pk,
+			IdentifierFactory factory) {
+		String statement = super.getAddPrimaryKeyStatement(pk, factory);
+		doLog(statement);
+		return statement;
+	}
+
+	@Override
+	public String getAddColumnStatement(Table table, Column col) {
+		String statement = super.getAddColumnStatement(table, col);
+		doLog(statement);
+		return statement;
+	}
+
+	@Override
+	public String getAddForeignKeyStatement(ForeignKey fk,
+			IdentifierFactory factory) {
+		String statement = super.getAddForeignKeyStatement(fk, factory);
+		doLog(statement);
+		return statement;
+	}
+
+	@Override
+	public String getDeleteTableStatement(SQLTable tbl) {
+		String statement = super.getDeleteTableStatement(tbl);
+		doLog(statement);
+		return statement;
+	}
+
+	@Override
+	public String getDropTableStatement(Table table) {
+		String statement = super.getDropTableStatement(table);
+		doLog(statement);
+		return statement;
+	}
+
+	@Override
+	public String getDropViewStatement(ViewImpl view) {
+		String statement = super.getDropViewStatement(view);
+		doLog(statement);
+		return statement;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/OneShotThread.java b/server-webapp/src/main/java/org/taverna/server/master/utils/OneShotThread.java
new file mode 100644
index 0000000..68b813d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/OneShotThread.java
@@ -0,0 +1,10 @@
+package org.taverna.server.master.utils;
+
+public class OneShotThread extends Thread {
+	public OneShotThread(String name, Runnable target) {
+		super(target, name);
+		setContextClassLoader(null);
+		setDaemon(true);
+		start();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/RestUtils.java b/server-webapp/src/main/java/org/taverna/server/master/utils/RestUtils.java
new file mode 100644
index 0000000..a892b52
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/RestUtils.java
@@ -0,0 +1,32 @@
+/*

+ * Copyright (C) 2013 The University of Manchester

+ * 

+ * See the file "LICENSE" for license terms.

+ */

+package org.taverna.server.master.utils;

+

+import javax.ws.rs.OPTIONS;

+import javax.ws.rs.core.Response;

+

+/**

+ * Utilities that make it easier to write REST services.

+ * 

+ * @author Donal Fellows

+ */

+public class RestUtils {

+	/**

+	 * Generate a response to an HTTP OPTIONS request.

+	 * 

+	 * @param methods

+	 *            The state-changing methods supported, if any.

+	 * @return the required response

+	 * @see OPTIONS

+	 */

+	public static Response opt(String... methods) {

+		StringBuilder sb = new StringBuilder("GET,");

+		for (String m : methods)

+			sb.append(m).append(",");

+		sb.append("HEAD,OPTIONS");

+		return Response.ok().header("Allow", sb.toString()).entity("").build();

+	}

+}

diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/RuntimeExceptionWrapper.java b/server-webapp/src/main/java/org/taverna/server/master/utils/RuntimeExceptionWrapper.java
new file mode 100644
index 0000000..0b2d4ea
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/RuntimeExceptionWrapper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.taverna.server.master.exceptions.GeneralFailureException;
+
+/**
+ * Aspect used to convert {@linkplain RuntimeException runtime exceptions} into
+ * a form that can be nicely conveyed to the outside world as HTTP errors.
+ * 
+ * @author Donal Fellows
+ */
+@Aspect
+public class RuntimeExceptionWrapper {
+	/**
+	 * Map an unexpected exception to one that can be correctly reported as a
+	 * problem.
+	 * 
+	 * @param exn
+	 *            The runtime exception being trapped.
+	 * @throws GeneralFailureException
+	 *             The known exception type that it is mapped to.
+	 */
+	@AfterThrowing(pointcut = "execution(* org.taverna.server.master.rest..*(..)) && !bean(*Provider.*)", throwing = "exn")
+	public void wrapRuntimeException(RuntimeException exn)
+			throws GeneralFailureException {
+		// Exclude security-related exceptions
+		if (exn.getClass().getName().startsWith("org.springframework.security."))
+			return;
+		throw new GeneralFailureException(exn);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/UsernamePrincipal.java b/server-webapp/src/main/java/org/taverna/server/master/utils/UsernamePrincipal.java
new file mode 100644
index 0000000..9bbcc2f
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/UsernamePrincipal.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import java.io.Serializable;
+import java.security.Principal;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/**
+ * A simple serializable principal that just records the name.
+ * 
+ * @author Donal Fellows
+ */
+public class UsernamePrincipal implements Principal, Serializable {
+	private static final long serialVersionUID = 2703493248562435L;
+	public UsernamePrincipal(String username) {
+		this.name = username;
+	}
+
+	public UsernamePrincipal(Principal other) {
+		this.name = other.getName();
+	}
+
+	public UsernamePrincipal(Authentication auth) {
+		this(auth.getPrincipal());
+	}
+
+	public UsernamePrincipal(Object principal) {
+		if (principal instanceof Principal)
+			this.name = ((Principal) principal).getName();
+		else if (principal instanceof String)
+			this.name = (String) principal;
+		else if (principal instanceof UserDetails)
+			this.name = ((UserDetails) principal).getUsername();
+		else
+			this.name = principal.toString();
+	}
+
+	private String name;
+
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	@Override
+	public String toString() {
+		return "Principal<" + name + ">";
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (o instanceof Principal) {
+			Principal p = (Principal) o;
+			return name.equals(p.getName());
+		}
+		return false;
+	}
+
+	@Override
+	public int hashCode() {
+		return name.hashCode();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/WSDLHeadOptionsInterceptor.java b/server-webapp/src/main/java/org/taverna/server/master/utils/WSDLHeadOptionsInterceptor.java
new file mode 100644
index 0000000..96cdc6d
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/WSDLHeadOptionsInterceptor.java
@@ -0,0 +1,65 @@
+package org.taverna.server.master.utils;
+
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.apache.cxf.common.util.UrlUtils.parseQueryString;
+import static org.apache.cxf.message.Message.HTTP_REQUEST_METHOD;
+import static org.apache.cxf.message.Message.QUERY_STRING;
+import static org.apache.cxf.message.Message.REQUEST_URL;
+import static org.apache.cxf.phase.Phase.READ;
+
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.cxf.binding.soap.interceptor.EndpointSelectionInterceptor;
+import org.apache.cxf.interceptor.Fault;
+import org.apache.cxf.message.Message;
+import org.apache.cxf.phase.AbstractPhaseInterceptor;
+
+
+/**
+ * Thunk for TAVSERV-293.
+ * 
+ * @author Donal Fellows (based on work by Daniel Hagen)
+ */
+public class WSDLHeadOptionsInterceptor extends
+		AbstractPhaseInterceptor<Message> {
+	public static final Log log = getLog("Taverna.Server.Utils");
+
+	public WSDLHeadOptionsInterceptor() {
+		super(READ);
+		getAfter().add(EndpointSelectionInterceptor.class.getName());
+	}
+
+	@Override
+	public void handleMessage(Message message) throws Fault {
+		String method = (String) message.get(HTTP_REQUEST_METHOD);
+		String query = (String) message.get(QUERY_STRING);
+
+		if (("HEAD".equals(method) || "OPTIONS".equals(method))
+				&& query != null && !query.trim().isEmpty()
+				&& isRecognizedQuery(query)) {
+			log.debug("adjusting message request method " + method + " for "
+					+ message.get(REQUEST_URL) + " to GET");
+			message.put(HTTP_REQUEST_METHOD, "GET");
+		}
+	}
+
+	/*
+	 * Stolen from http://permalink.gmane.org/gmane.comp.apache.cxf.user/20037
+	 * which is itself in turn stolen from
+	 * org.apache.cxf.frontend.WSDLGetInterceptor.isRecognizedQuery
+	 */
+	/**
+	 * Is this a query for WSDL or XSD relating to it?
+	 * 
+	 * @param query
+	 *            The query string to check
+	 * @return If the query is one to handle.
+	 * @see org.apache.cxf.frontend.WSDLGetInterceptor#isRecognizedQuery(Map,String,String,org.apache.cxf.service.model.EndpointInfo)
+	 *      WSDLGetInterceptor
+	 */
+	private boolean isRecognizedQuery(String query) {
+		Map<String, String> map = parseQueryString(query);
+		return map.containsKey("wsdl") || map.containsKey("xsd");
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/WebappAwareDataSource.java b/server-webapp/src/main/java/org/taverna/server/master/utils/WebappAwareDataSource.java
new file mode 100644
index 0000000..03fc749
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/WebappAwareDataSource.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import static java.lang.Thread.currentThread;
+import static java.sql.DriverManager.deregisterDriver;
+import static java.sql.DriverManager.getDrivers;
+import static org.taverna.server.master.utils.Contextualizer.ROOT_PLACEHOLDER;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.Driver;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Enumeration;
+
+import javax.annotation.PreDestroy;
+
+import org.apache.commons.dbcp.BasicDataSource;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+
+/**
+ * Add some awareness of the context so that we can locate databases internally
+ * to the webapp.
+ * 
+ * @author Donal Fellows
+ */
+public class WebappAwareDataSource extends BasicDataSource {
+	Log log = LogFactory.getLog("Taverna.Server.Utils");
+	private transient boolean init;
+	private Contextualizer ctxt;
+	private String shutdownUrl;
+
+	@Required
+	public void setContextualizer(Contextualizer ctxt) {
+		this.ctxt = ctxt;
+	}
+
+	/**
+	 * A JDBC connection URL to use on shutting down the database. If not set,
+	 * do nothing special.
+	 * 
+	 * @param url
+	 */
+	public void setShutdownUrl(String url) {
+		shutdownUrl = url;
+	}
+
+	private void doInit() {
+		synchronized (this) {
+			if (!init) {
+				setDriverClassLoader(currentThread().getContextClassLoader());
+				String url = getUrl();
+				if (url.contains(ROOT_PLACEHOLDER)) {
+					String newurl = ctxt.contextualize(url);
+					setUrl(newurl);
+					log.info("mapped " + url + " to " + newurl);
+				} else {
+					log.info("did not find " + ROOT_PLACEHOLDER + " in " + url);
+				}
+				init = true;
+			}
+		}
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=- HOOKS -=-=-=-=-=-=-=-=-=-=-
+
+	@Override
+	public Connection getConnection() throws SQLException {
+		doInit();
+		return super.getConnection();
+	}
+
+	@Override
+	public void setLogWriter(PrintWriter pw) throws SQLException {
+		doInit();
+		super.setLogWriter(pw);
+	}
+
+	@Override
+	public void setLoginTimeout(int num) throws SQLException {
+		doInit();
+		super.setLoginTimeout(num);
+	}
+
+	@Override
+	public PrintWriter getLogWriter() throws SQLException {
+		doInit();
+		return super.getLogWriter();
+	}
+
+	@Override
+	public int getLoginTimeout() throws SQLException {
+		doInit();
+		return super.getLoginTimeout();
+	}
+
+	@PreDestroy
+	void realClose() {
+		try {
+			close();
+		} catch (SQLException e) {
+			log.warn("problem shutting down DB connection", e);
+		}
+		try {
+			if (shutdownUrl != null)
+				DriverManager.getConnection(ctxt.contextualize(shutdownUrl));
+		} catch (SQLException e) {
+			// Expected; ignore it
+		}
+		log = null;
+		dropDriver();
+	}
+
+	private void dropDriver() {
+		Enumeration<Driver> drivers = getDrivers();
+		while (drivers.hasMoreElements()) {
+			Driver d = drivers.nextElement();
+			if (d.getClass().getClassLoader() == getDriverClassLoader()
+					&& d.getClass().getName().equals(getDriverClassName())) {
+				try {
+					deregisterDriver(d);
+				} catch (SQLException e) {
+				}
+				break;
+			}
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/X500Utils.java b/server-webapp/src/main/java/org/taverna/server/master/utils/X500Utils.java
new file mode 100644
index 0000000..da4cff0
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/X500Utils.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.utils;
+
+import static javax.security.auth.x500.X500Principal.RFC2253;
+
+import java.math.BigInteger;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.PreDestroy;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Support class that factors out some of the messier parts of working with
+ * X.500 identities and X.509 certificates.
+ * 
+ * @author Donal Fellows
+ */
+public class X500Utils {
+	private Log log = LogFactory.getLog("Taverna.Server.Utils");
+
+	@PreDestroy
+	void closeLog() {
+		log = null;
+	}
+
+	private static final char DN_SEPARATOR = ',';
+	private static final char DN_ESCAPE = '\\';
+	private static final char DN_QUOTE = '"';
+
+	/**
+	 * Parse the DN from the Principal and extract the CN field.
+	 * 
+	 * @param id
+	 *            The identity to extract the distinguished name from.
+	 * @param fields
+	 *            The names to look at when finding the field to return. Each
+	 *            should be an upper-cased string.
+	 * @return The common-name part of the distinguished name, or the literal
+	 *         string "<tt>none</tt>" if there is no CN.
+	 */
+	public String getName(X500Principal id, String... fields) {
+		String dn = id.getName(RFC2253);
+
+		int i = 0;
+		int startIndex = 0;
+		boolean ignoreThisChar = false;
+		boolean inQuotes = false;
+		Map<String, String> tokenized = new HashMap<>();
+
+		for (i = 0; i < dn.length(); i++)
+			if (ignoreThisChar)
+				ignoreThisChar = false;
+			else if (dn.charAt(i) == DN_QUOTE)
+				inQuotes = !inQuotes;
+			else if (inQuotes)
+				continue;
+			else if (dn.charAt(i) == DN_ESCAPE)
+				ignoreThisChar = true;
+			else if ((dn.charAt(i) == DN_SEPARATOR) && !ignoreThisChar) {
+				storeDNField(tokenized, dn.substring(startIndex, i).trim()
+						.split("=", 2));
+				startIndex = i + 1;
+			}
+		if (inQuotes || ignoreThisChar)
+			log.warn("was parsing invalid DN format");
+		// Add last token - after the last delimiter
+		storeDNField(tokenized, dn.substring(startIndex).trim().split("=", 2));
+
+		for (String field : fields) {
+			String value = tokenized.get(field);
+			if (value != null)
+				return value;
+		}
+		return "none";
+	}
+
+	private void storeDNField(Map<String, String> container, String[] split) {
+		if (split == null || split.length != 2)
+			return;
+		String key = split[0].toUpperCase();
+		if (container.containsKey(key))
+			log.warn("duplicate field in DN: " + key);
+		// LATER: Should the field be de-quoted?
+		container.put(key, split[1]);
+	}
+
+	/**
+	 * Get the serial number from a certificate as a hex string.
+	 * 
+	 * @param cert
+	 *            The certificate to extract from.
+	 * @return A hex string, in upper-case.
+	 */
+	public String getSerial(X509Certificate cert) {
+		return new BigInteger(1, cert.getSerialNumber().toByteArray())
+				.toString(16).toUpperCase();
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/utils/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/utils/package-info.java
new file mode 100644
index 0000000..612e61c
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/utils/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * Miscellaneous utility classes. Includes aspects that might be attached
+ * for purposes such as transaction management and invocation tracking.
+ */
+package org.taverna.server.master.utils;
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/CompletionNotifier.java b/server-webapp/src/main/java/org/taverna/server/master/worker/CompletionNotifier.java
new file mode 100644
index 0000000..10b3830
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/CompletionNotifier.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+
+/**
+ * How to convert a notification about the completion of a job into a message.
+ * 
+ * @author Donal Fellows
+ */
+public interface CompletionNotifier {
+	/**
+	 * @return The name of this notifier.
+	 */
+	String getName();
+
+	/**
+	 * Called to get the content of a message that a workflow run has finished.
+	 * 
+	 * @param name
+	 *            The name of the run.
+	 * @param run
+	 *            What run are we talking about.
+	 * @param code
+	 *            What the exit code was.
+	 * @return The plain-text content of the message.
+	 */
+	String makeCompletionMessage(String name, RemoteRunDelegate run, int code);
+
+	/**
+	 * Called to get the subject of the message to dispatch.
+	 * 
+	 * @param name
+	 *            The name of the run.
+	 * @param run
+	 *            What run are we talking about.
+	 * @param code
+	 *            What the exit code was.
+	 * @return The plain-text subject of the message.
+	 */
+	String makeMessageSubject(String name, RemoteRunDelegate run, int code);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/FactoryBean.java b/server-webapp/src/main/java/org/taverna/server/master/worker/FactoryBean.java
new file mode 100644
index 0000000..5d0f371
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/FactoryBean.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import org.taverna.server.master.notification.atom.EventDAO;
+
+/**
+ * What the remote run really needs of its factory.
+ * 
+ * @author Donal Fellows
+ */
+public interface FactoryBean {
+	/**
+	 * @return Whether a run can actually be started at this time.
+	 */
+	boolean isAllowingRunsToStart();
+
+	/**
+	 * @return a handle to the master Atom event feed (<i>not</i> the per-run
+	 *         feed)
+	 */
+	EventDAO getMasterEventFeed();
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/PasswordIssuer.java b/server-webapp/src/main/java/org/taverna/server/master/worker/PasswordIssuer.java
new file mode 100644
index 0000000..d3c5b8a
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/PasswordIssuer.java
@@ -0,0 +1,57 @@
+package org.taverna.server.master.worker;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * A simple password issuing bean.
+ * 
+ * @author Donal Fellows
+ */
+public class PasswordIssuer {
+	private static final char[] ALPHABET = { 'a', 'b', 'c', 'd', 'e', 'f', 'g',
+			'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+			'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
+			'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+			'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7',
+			'8', '9', '0', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')',
+			',', '.', '<', '>', '/', '?', ':', ';', '-', '_', '+', '[', ']',
+			'{', '}', '`', '~' };
+	private Log log = LogFactory.getLog("Taverna.Server.Worker");
+	private SecureRandom r;
+	private int length;
+
+	public PasswordIssuer() {
+		r = new SecureRandom();
+		log.info("constructing passwords with " + r.getAlgorithm());
+		setLength(8);
+	}
+
+	public PasswordIssuer(String algorithm) throws NoSuchAlgorithmException {
+		r = SecureRandom.getInstance(algorithm);
+		log.info("constructing passwords with " + r.getAlgorithm());
+		setLength(8);
+	}
+
+	public void setLength(int length) {
+		this.length = length;
+		log.info("issued password will be " + this.length
+				+ " symbols chosen from " + ALPHABET.length);
+	}
+
+	/**
+	 * Issue a password.
+	 * 
+	 * @return The new password.
+	 */
+	public String issue() {
+		StringBuilder sb = new StringBuilder();
+		for (int i = 0; i < length; i++)
+			sb.append(ALPHABET[r.nextInt(ALPHABET.length)]);
+		log.info("issued new password of length " + sb.length());
+		return sb.toString();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/PolicyImpl.java b/server-webapp/src/main/java/org/taverna/server/master/worker/PolicyImpl.java
new file mode 100644
index 0000000..f5613c7
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/PolicyImpl.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static org.taverna.server.master.identity.WorkflowInternalAuthProvider.PREFIX;
+
+import java.net.URI;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.taverna.server.master.common.Roles;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Policy;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Basic policy implementation that allows any workflow to be instantiated by
+ * any user, but which does not permit users to access each others workflow
+ * runs. It also imposes a global limit on the number of workflow runs at once.
+ * 
+ * @author Donal Fellows
+ */
+class PolicyImpl implements Policy {
+	Log log = LogFactory.getLog("Taverna.Server.Worker.Policy");
+	private PolicyLimits limits;
+	private RunDBSupport runDB;
+
+	@Required
+	public void setLimits(PolicyLimits limits) {
+		this.limits = limits;
+	}
+
+	@Required
+	public void setRunDB(RunDBSupport runDB) {
+		this.runDB = runDB;
+	}
+
+	@Override
+	public int getMaxRuns() {
+		return limits.getMaxRuns();
+	}
+
+	@Override
+	public Integer getMaxRuns(UsernamePrincipal user) {
+		return null;
+	}
+
+	@Override
+	public int getOperatingLimit() {
+		return limits.getOperatingLimit();
+	}
+
+	@Override
+	public List<URI> listPermittedWorkflowURIs(UsernamePrincipal user) {
+		return limits.getPermittedWorkflowURIs();
+	}
+
+	private boolean isSelfAccess(String runId) {
+		Authentication auth = SecurityContextHolder.getContext()
+				.getAuthentication();
+		boolean self = false;
+		String id = null;
+		for (GrantedAuthority a : auth.getAuthorities()) {
+			String aa = a.getAuthority();
+			if (aa.equals(Roles.SELF)) {
+				self = true;
+				continue;
+			}
+			if (!aa.startsWith(PREFIX))
+				continue;
+			id = aa.substring(PREFIX.length());
+		}
+		return self && runId.equals(id);
+	}
+
+	@Override
+	public boolean permitAccess(UsernamePrincipal user, TavernaRun run) {
+		String username = user.getName();
+		TavernaSecurityContext context = run.getSecurityContext();
+		if (context.getOwner().getName().equals(username)) {
+			if (log.isDebugEnabled())
+				log.debug("granted access by " + user.getName() + " to "
+						+ run.getId());
+			return true;
+		}
+		if (isSelfAccess(run.getId())) {
+			if (log.isDebugEnabled())
+				log.debug("access by workflow to itself: " + run.getId());
+			return true;
+		}
+		if (log.isDebugEnabled())
+			log.debug("considering access by " + user.getName() + " to "
+					+ run.getId());
+		return context.getPermittedReaders().contains(username);
+	}
+
+	@Override
+	public void permitCreate(UsernamePrincipal user, Workflow workflow)
+			throws NoCreateException {
+		if (user == null)
+			throw new NoCreateException(
+					"anonymous workflow creation not allowed");
+		if (runDB.countRuns() >= getMaxRuns())
+			throw new NoCreateException("server load exceeded; please wait");
+	}
+
+	@Override
+	public synchronized void permitDestroy(UsernamePrincipal user, TavernaRun run)
+			throws NoDestroyException {
+		if (user == null)
+			throw new NoDestroyException();
+		String username = user.getName();
+		TavernaSecurityContext context = run.getSecurityContext();
+		if (context.getOwner() == null
+				|| context.getOwner().getName().equals(username))
+			return;
+		if (!context.getPermittedDestroyers().contains(username))
+			throw new NoDestroyException();
+	}
+
+	@Override
+	public void permitUpdate(UsernamePrincipal user, TavernaRun run)
+			throws NoUpdateException {
+		if (user == null)
+			throw new NoUpdateException(
+					"workflow run not owned by you and you're not granted access");
+		TavernaSecurityContext context = run.getSecurityContext();
+		if (context.getOwner().getName().equals(user.getName()))
+			return;
+		if (isSelfAccess(run.getId())) {
+			if (log.isDebugEnabled())
+				log.debug("update access by workflow to itself: " + run.getId());
+			return;
+		}
+		if (!context.getPermittedUpdaters().contains(user.getName()))
+			throw new NoUpdateException(
+					"workflow run not owned by you and you're not granted access");
+	}
+
+	@Override
+	public void setPermittedWorkflowURIs(UsernamePrincipal user,
+			List<URI> permitted) {
+		limits.setPermittedWorkflowURIs(permitted);
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/PolicyLimits.java b/server-webapp/src/main/java/org/taverna/server/master/worker/PolicyLimits.java
new file mode 100644
index 0000000..8cbc7ea
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/PolicyLimits.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import java.net.URI;
+import java.util.List;
+
+import org.taverna.server.master.common.Status;
+
+/**
+ * The worker policy delegates certain limits to the state model of the
+ * particular worker.
+ * 
+ * @author Donal Fellows
+ */
+public interface PolicyLimits {
+	/**
+	 * @return the maximum number of extant workflow runs in any state
+	 */
+	int getMaxRuns();
+
+	/**
+	 * @return the maximum number of workflow runs in the
+	 *         {@linkplain Status#Operating operating} state.
+	 */
+	int getOperatingLimit();
+
+	/**
+	 * @return the list of URIs to workflows that may be used to create workflow
+	 *         runs. If empty or <tt>null</tt>, no restriction is present.
+	 */
+	List<URI> getPermittedWorkflowURIs();
+
+	/**
+	 * @param permitted
+	 *            the list of URIs to workflows that may be used to create
+	 *            workflow runs.
+	 */
+	void setPermittedWorkflowURIs(List<URI> permitted);
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/RemoteRunDelegate.java b/server-webapp/src/main/java/org/taverna/server/master/worker/RemoteRunDelegate.java
new file mode 100644
index 0000000..22158d5
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/RemoteRunDelegate.java
@@ -0,0 +1,967 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static java.lang.System.currentTimeMillis;
+import static java.util.Calendar.MINUTE;
+import static java.util.Collections.sort;
+import static java.util.Collections.unmodifiableSet;
+import static java.util.UUID.randomUUID;
+import static org.apache.commons.io.IOUtils.closeQuietly;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.taverna.server.master.worker.RemoteRunDelegate.checkBadFilename;
+import static org.taverna.server.master.worker.RunConnection.NAME_LENGTH;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.PipedOutputStream;
+import java.rmi.MarshalledObject;
+import java.rmi.RemoteException;
+import java.security.GeneralSecurityException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.logging.Log;
+import org.taverna.server.localworker.remote.IllegalStateTransitionException;
+import org.taverna.server.localworker.remote.ImplementationException;
+import org.taverna.server.localworker.remote.RemoteDirectory;
+import org.taverna.server.localworker.remote.RemoteDirectoryEntry;
+import org.taverna.server.localworker.remote.RemoteFile;
+import org.taverna.server.localworker.remote.RemoteInput;
+import org.taverna.server.localworker.remote.RemoteListener;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.localworker.remote.RemoteStatus;
+import org.taverna.server.localworker.remote.StillWorkingOnItException;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.OverloadedException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.DirectoryEntry;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.SecurityContextFactory;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Bridging shim between the WebApp world and the RMI world.
+ * 
+ * @author Donal Fellows
+ */
+@SuppressWarnings("serial")
+public class RemoteRunDelegate implements TavernaRun {
+	private transient Log log = getLog("Taverna.Server.Worker");
+	transient TavernaSecurityContext secContext;
+	Date creationInstant;
+	Workflow workflow;
+	Date expiry;
+	HashSet<String> readers;
+	HashSet<String> writers;
+	HashSet<String> destroyers;
+	transient String id;
+	transient RemoteSingleRun run;
+	transient RunDBSupport db;
+	transient FactoryBean factory;
+	boolean doneTransitionToFinished;
+	boolean generateProvenance;// FIXME expose
+	String name;
+	private static final String ELLIPSIS = "...";
+
+	public RemoteRunDelegate(Date creationInstant, Workflow workflow,
+			RemoteSingleRun rsr, int defaultLifetime, RunDBSupport db, UUID id,
+			boolean generateProvenance, FactoryBean factory) {
+		if (rsr == null)
+			throw new IllegalArgumentException("remote run must not be null");
+		this.creationInstant = creationInstant;
+		this.workflow = workflow;
+		Calendar c = Calendar.getInstance();
+		c.add(MINUTE, defaultLifetime);
+		this.expiry = c.getTime();
+		this.run = rsr;
+		this.db = db;
+		this.generateProvenance = generateProvenance;
+		this.factory = factory;
+		try {
+			this.name = "";
+			String ci = " " + creationInstant;
+			String n = workflow.getName();
+			if (n.length() > NAME_LENGTH - ci.length())
+				n = n.substring(0,
+						NAME_LENGTH - ci.length() - ELLIPSIS.length())
+						+ ELLIPSIS;
+			this.name = n + ci;
+		} catch (Exception e) {
+			// Ignore; it's just a name, not something important.
+		}
+		if (id != null)
+			this.id = id.toString();
+	}
+
+	RemoteRunDelegate() {
+	}
+
+	/**
+	 * Get the types of listener supported by this run.
+	 * 
+	 * @return A list of listener type names.
+	 * @throws RemoteException
+	 *             If anything goes wrong.
+	 */
+	public List<String> getListenerTypes() throws RemoteException {
+		return run.getListenerTypes();
+	}
+
+	@Override
+	public void addListener(Listener listener) {
+		if (listener instanceof ListenerDelegate)
+			try {
+				run.addListener(((ListenerDelegate) listener).getRemote());
+			} catch (RemoteException e) {
+				log.warn("communication problem adding listener", e);
+			} catch (ImplementationException e) {
+				log.warn("implementation problem adding listener", e);
+			}
+		else
+			log.fatal("bad listener " + listener.getClass()
+					+ "; not applicable remotely!");
+	}
+
+	@Override
+	public String getId() {
+		if (id == null)
+			id = randomUUID().toString();
+		return id;
+	}
+
+	/**
+	 * Attach a listener to a workflow run and return its local delegate.
+	 * 
+	 * @param type
+	 *            The type of listener to create.
+	 * @param config
+	 *            The configuration of the listener.
+	 * @return The local delegate of the listener.
+	 * @throws NoListenerException
+	 *             If anything goes wrong.
+	 */
+	public Listener makeListener(String type, String config)
+			throws NoListenerException {
+		try {
+			return new ListenerDelegate(run.makeListener(type, config));
+		} catch (RemoteException e) {
+			throw new NoListenerException("failed to make listener", e);
+		}
+	}
+
+	@Override
+	public void destroy() {
+		try {
+			run.destroy();
+		} catch (RemoteException | ImplementationException e) {
+			log.warn("failed to destroy run", e);
+		}
+	}
+
+	@Override
+	public Date getExpiry() {
+		return new Date(expiry.getTime());
+	}
+
+	@Override
+	public List<Listener> getListeners() {
+		List<Listener> listeners = new ArrayList<>();
+		try {
+			for (RemoteListener rl : run.getListeners())
+				listeners.add(new ListenerDelegate(rl));
+		} catch (RemoteException e) {
+			log.warn("failed to get listeners", e);
+		}
+		return listeners;
+	}
+
+	@Override
+	public TavernaSecurityContext getSecurityContext() {
+		return secContext;
+	}
+
+	@Override
+	public Status getStatus() {
+		try {
+			switch (run.getStatus()) {
+			case Initialized:
+				return Status.Initialized;
+			case Operating:
+				return Status.Operating;
+			case Stopped:
+				return Status.Stopped;
+			case Finished:
+				return Status.Finished;
+			}
+		} catch (RemoteException e) {
+			log.warn("problem getting remote status", e);
+		}
+		return Status.Finished;
+	}
+
+	@Override
+	public Workflow getWorkflow() {
+		return workflow;
+	}
+
+	@Override
+	public Directory getWorkingDirectory() throws FilesystemAccessException {
+		try {
+			return new DirectoryDelegate(run.getWorkingDirectory());
+		} catch (Throwable e) {
+			if (e.getCause() != null)
+				e = e.getCause();
+			throw new FilesystemAccessException(
+					"problem getting main working directory handle", e);
+		}
+	}
+
+	@Override
+	public void setExpiry(Date d) {
+		if (d.after(new Date()))
+			expiry = new Date(d.getTime());
+		db.flushToDisk(this);
+	}
+
+	@Override
+	public String setStatus(Status s) throws BadStateChangeException {
+		try {
+			log.info("setting status of run " + id + " to " + s);
+			switch (s) {
+			case Initialized:
+				run.setStatus(RemoteStatus.Initialized);
+				break;
+			case Operating:
+				if (run.getStatus() == RemoteStatus.Initialized) {
+					if (!factory.isAllowingRunsToStart())
+						throw new OverloadedException();
+					secContext.conveySecurity();
+				}
+				run.setGenerateProvenance(generateProvenance);
+				run.setStatus(RemoteStatus.Operating);
+				factory.getMasterEventFeed()
+						.started(
+								this,
+								"started run execution",
+								"The execution of run '" + getName()
+										+ "' has started.");
+				break;
+			case Stopped:
+				run.setStatus(RemoteStatus.Stopped);
+				break;
+			case Finished:
+				run.setStatus(RemoteStatus.Finished);
+				break;
+			}
+			return null;
+		} catch (IllegalStateTransitionException e) {
+			throw new BadStateChangeException(e.getMessage());
+		} catch (RemoteException e) {
+			throw new BadStateChangeException(e.getMessage(), e.getCause());
+		} catch (GeneralSecurityException | IOException e) {
+			throw new BadStateChangeException(e.getMessage(), e);
+		} catch (ImplementationException e) {
+			if (e.getCause() != null)
+				throw new BadStateChangeException(e.getMessage(), e.getCause());
+			throw new BadStateChangeException(e.getMessage(), e);
+		} catch (StillWorkingOnItException e) {
+			log.info("still working on setting status of run " + id + " to "
+					+ s, e);
+			return e.getMessage();
+		} catch (InterruptedException e) {
+			throw new BadStateChangeException(
+					"interrupted while waiting to insert notification into database");
+		}
+	}
+
+	static void checkBadFilename(String filename)
+			throws FilesystemAccessException {
+		if (filename.startsWith("/"))
+			throw new FilesystemAccessException("filename may not be absolute");
+		if (Arrays.asList(filename.split("/")).contains(".."))
+			throw new FilesystemAccessException(
+					"filename may not refer to parent");
+	}
+
+	@Override
+	public String getInputBaclavaFile() {
+		try {
+			return run.getInputBaclavaFile();
+		} catch (RemoteException e) {
+			log.warn("problem when fetching input baclava file", e);
+			return null;
+		}
+	}
+
+	@Override
+	public List<Input> getInputs() {
+		ArrayList<Input> inputs = new ArrayList<>();
+		try {
+			for (RemoteInput ri : run.getInputs())
+				inputs.add(new RunInput(ri));
+		} catch (RemoteException e) {
+			log.warn("problem when fetching list of workflow inputs", e);
+		}
+		return inputs;
+	}
+
+	@Override
+	public String getOutputBaclavaFile() {
+		try {
+			return run.getOutputBaclavaFile();
+		} catch (RemoteException e) {
+			log.warn("problem when fetching output baclava file", e);
+			return null;
+		}
+	}
+
+	@Override
+	public Input makeInput(String name) throws BadStateChangeException {
+		try {
+			return new RunInput(run.makeInput(name));
+		} catch (RemoteException e) {
+			throw new BadStateChangeException("failed to make input", e);
+		}
+	}
+
+	@Override
+	public void setInputBaclavaFile(String filename)
+			throws FilesystemAccessException, BadStateChangeException {
+		checkBadFilename(filename);
+		try {
+			run.setInputBaclavaFile(filename);
+		} catch (RemoteException e) {
+			throw new FilesystemAccessException(
+					"cannot set input baclava file name", e);
+		}
+	}
+
+	@Override
+	public void setOutputBaclavaFile(String filename)
+			throws FilesystemAccessException, BadStateChangeException {
+		checkBadFilename(filename);
+		try {
+			run.setOutputBaclavaFile(filename);
+		} catch (RemoteException e) {
+			throw new FilesystemAccessException(
+					"cannot set output baclava file name", e);
+		}
+	}
+
+	@Override
+	public Date getCreationTimestamp() {
+		return creationInstant == null ? null : new Date(
+				creationInstant.getTime());
+	}
+
+	@Override
+	public Date getFinishTimestamp() {
+		try {
+			return run.getFinishTimestamp();
+		} catch (RemoteException e) {
+			log.info("failed to get finish timestamp", e);
+			return null;
+		}
+	}
+
+	@Override
+	public Date getStartTimestamp() {
+		try {
+			return run.getStartTimestamp();
+		} catch (RemoteException e) {
+			log.info("failed to get finish timestamp", e);
+			return null;
+		}
+	}
+
+	/**
+	 * @param readers
+	 *            the readers to set
+	 */
+	public void setReaders(Set<String> readers) {
+		this.readers = new HashSet<>(readers);
+		db.flushToDisk(this);
+	}
+
+	/**
+	 * @return the readers
+	 */
+	public Set<String> getReaders() {
+		return readers == null ? new HashSet<String>()
+				: unmodifiableSet(readers);
+	}
+
+	/**
+	 * @param writers
+	 *            the writers to set
+	 */
+	public void setWriters(Set<String> writers) {
+		this.writers = new HashSet<>(writers);
+		db.flushToDisk(this);
+	}
+
+	/**
+	 * @return the writers
+	 */
+	public Set<String> getWriters() {
+		return writers == null ? new HashSet<String>()
+				: unmodifiableSet(writers);
+	}
+
+	/**
+	 * @param destroyers
+	 *            the destroyers to set
+	 */
+	public void setDestroyers(Set<String> destroyers) {
+		this.destroyers = new HashSet<>(destroyers);
+		db.flushToDisk(this);
+	}
+
+	/**
+	 * @return the destroyers
+	 */
+	public Set<String> getDestroyers() {
+		return destroyers == null ? new HashSet<String>()
+				: unmodifiableSet(destroyers);
+	}
+
+	private void writeObject(ObjectOutputStream out) throws IOException {
+		out.defaultWriteObject();
+		out.writeUTF(secContext.getOwner().getName());
+		out.writeObject(secContext.getFactory());
+		out.writeObject(new MarshalledObject<>(run));
+	}
+
+	@Override
+	public boolean getGenerateProvenance() {
+		return generateProvenance;
+	}
+
+	@Override
+	public void setGenerateProvenance(boolean generateProvenance) {
+		this.generateProvenance = generateProvenance;
+		db.flushToDisk(this);
+	}
+
+	@SuppressWarnings("unchecked")
+	private void readObject(ObjectInputStream in) throws IOException,
+			ClassNotFoundException {
+		in.defaultReadObject();
+		if (log == null)
+			log = getLog("Taverna.Server.LocalWorker");
+		final String creatorName = in.readUTF();
+		SecurityContextFactory factory = (SecurityContextFactory) in
+				.readObject();
+		try {
+			secContext = factory.create(this,
+					new UsernamePrincipal(creatorName));
+		} catch (RuntimeException | IOException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new SecurityContextReconstructionException(e);
+		}
+		run = ((MarshalledObject<RemoteSingleRun>) in.readObject()).get();
+	}
+
+	public void setSecurityContext(TavernaSecurityContext tavernaSecurityContext) {
+		secContext = tavernaSecurityContext;
+	}
+
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	@Override
+	public void setName(@Nonnull String name) {
+		if (name.length() > RunConnection.NAME_LENGTH)
+			this.name = name.substring(0, RunConnection.NAME_LENGTH);
+		else
+			this.name = name;
+		db.flushToDisk(this);
+	}
+
+	@Override
+	public void ping() throws UnknownRunException {
+		try {
+			run.ping();
+		} catch (RemoteException e) {
+			throw new UnknownRunException(e);
+		}
+	}
+}
+
+abstract class DEDelegate implements DirectoryEntry {
+	Log log = getLog("Taverna.Server.Worker");
+	private RemoteDirectoryEntry entry;
+	private String name;
+	private String full;
+	private Date cacheModTime;
+	private long cacheQueryTime = 0L;
+
+	DEDelegate(RemoteDirectoryEntry entry) {
+		this.entry = entry;
+	}
+
+	@Override
+	public void destroy() throws FilesystemAccessException {
+		try {
+			entry.destroy();
+		} catch (IOException e) {
+			throw new FilesystemAccessException(
+					"failed to delete directory entry", e);
+		}
+	}
+
+	@Override
+	public String getFullName() {
+		if (full != null)
+			return full;
+		String n = getName();
+		RemoteDirectoryEntry re = entry;
+		try {
+			while (true) {
+				RemoteDirectory parent = re.getContainingDirectory();
+				if (parent == null)
+					break;
+				n = parent.getName() + "/" + n;
+				re = parent;
+			}
+		} catch (RemoteException e) {
+			log.warn("failed to generate full name", e);
+		}
+		return (full = n);
+	}
+
+	@Override
+	public String getName() {
+		if (name == null)
+			try {
+				name = entry.getName();
+			} catch (RemoteException e) {
+				log.error("failed to get name", e);
+			}
+		return name;
+	}
+
+	@Override
+	public Date getModificationDate() {
+		if (cacheModTime == null || currentTimeMillis() - cacheQueryTime < 5000)
+			try {
+				cacheModTime = entry.getModificationDate();
+				cacheQueryTime = currentTimeMillis();
+			} catch (RemoteException e) {
+				log.error("failed to get modification time", e);
+			}
+		return cacheModTime;
+	}
+
+	@Override
+	public int compareTo(DirectoryEntry de) {
+		return getFullName().compareTo(de.getFullName());
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		return o != null && o instanceof DEDelegate
+				&& getFullName().equals(((DEDelegate) o).getFullName());
+	}
+
+	@Override
+	public int hashCode() {
+		return getFullName().hashCode();
+	}
+}
+
+class DirectoryDelegate extends DEDelegate implements Directory {
+	RemoteDirectory rd;
+
+	DirectoryDelegate(RemoteDirectory dir) {
+		super(dir);
+		rd = dir;
+	}
+
+	@Override
+	public Collection<DirectoryEntry> getContents()
+			throws FilesystemAccessException {
+		ArrayList<DirectoryEntry> result = new ArrayList<>();
+		try {
+			for (RemoteDirectoryEntry rde : rd.getContents()) {
+				if (rde instanceof RemoteDirectory)
+					result.add(new DirectoryDelegate((RemoteDirectory) rde));
+				else
+					result.add(new FileDelegate((RemoteFile) rde));
+			}
+		} catch (IOException e) {
+			throw new FilesystemAccessException(
+					"failed to get directory contents", e);
+		}
+		return result;
+	}
+
+	@Override
+	public Collection<DirectoryEntry> getContentsByDate()
+			throws FilesystemAccessException {
+		ArrayList<DirectoryEntry> result = new ArrayList<>(getContents());
+		sort(result, new DateComparator());
+		return result;
+	}
+
+	static class DateComparator implements Comparator<DirectoryEntry> {
+		@Override
+		public int compare(DirectoryEntry a, DirectoryEntry b) {
+			return a.getModificationDate().compareTo(b.getModificationDate());
+		}
+	}
+
+	@Override
+	public File makeEmptyFile(Principal actor, String name)
+			throws FilesystemAccessException {
+		try {
+			return new FileDelegate(rd.makeEmptyFile(name));
+		} catch (IOException e) {
+			throw new FilesystemAccessException("failed to make empty file", e);
+		}
+	}
+
+	@Override
+	public Directory makeSubdirectory(Principal actor, String name)
+			throws FilesystemAccessException {
+		try {
+			return new DirectoryDelegate(rd.makeSubdirectory(name));
+		} catch (IOException e) {
+			throw new FilesystemAccessException("failed to make subdirectory",
+					e);
+		}
+	}
+
+	@Override
+	public ZipStream getContentsAsZip() throws FilesystemAccessException {
+		ZipStream zs = new ZipStream();
+
+		final ZipOutputStream zos;
+		try {
+			zos = new ZipOutputStream(new PipedOutputStream(zs));
+		} catch (IOException e) {
+			throw new FilesystemAccessException("problem building zip stream",
+					e);
+		}
+		Thread t = new Thread(new Runnable() {
+			@Override
+			public void run() {
+				try {
+					zipDirectory(rd, null, zos);
+				} catch (IOException e) {
+					log.warn("problem when zipping directory", e);
+				} finally {
+					closeQuietly(zos);
+				}
+			}
+		});
+		t.setDaemon(true);
+		t.start();
+		return zs;
+	}
+
+	/**
+	 * Compresses a directory tree into a ZIP.
+	 * 
+	 * @param dir
+	 *            The directory to compress.
+	 * @param base
+	 *            The base name of the directory (or <tt>null</tt> if this is
+	 *            the root directory of the ZIP).
+	 * @param zos
+	 *            Where to write the compressed data.
+	 * @throws RemoteException
+	 *             If some kind of problem happens with the remote delegates.
+	 * @throws IOException
+	 *             If we run into problems with reading or writing data.
+	 */
+	void zipDirectory(RemoteDirectory dir, String base, ZipOutputStream zos)
+			throws RemoteException, IOException {
+		for (RemoteDirectoryEntry rde : dir.getContents()) {
+			String name = rde.getName();
+			if (base != null)
+				name = base + "/" + name;
+			if (rde instanceof RemoteDirectory) {
+				RemoteDirectory rd = (RemoteDirectory) rde;
+				zipDirectory(rd, name, zos);
+			} else {
+				RemoteFile rf = (RemoteFile) rde;
+				zos.putNextEntry(new ZipEntry(name));
+				try {
+					int off = 0;
+					while (true) {
+						byte[] c = rf.getContents(off, 64 * 1024);
+						if (c == null || c.length == 0)
+							break;
+						zos.write(c);
+						off += c.length;
+					}
+				} finally {
+					zos.closeEntry();
+				}
+			}
+		}
+	}
+}
+
+class FileDelegate extends DEDelegate implements File {
+	RemoteFile rf;
+
+	FileDelegate(RemoteFile f) {
+		super(f);
+		this.rf = f;
+	}
+
+	@Override
+	public byte[] getContents(int offset, int length)
+			throws FilesystemAccessException {
+		try {
+			return rf.getContents(offset, length);
+		} catch (IOException e) {
+			throw new FilesystemAccessException("failed to read file contents",
+					e);
+		}
+	}
+
+	@Override
+	public long getSize() throws FilesystemAccessException {
+		try {
+			return rf.getSize();
+		} catch (IOException e) {
+			throw new FilesystemAccessException("failed to get file length", e);
+		}
+	}
+
+	@Override
+	public void setContents(byte[] data) throws FilesystemAccessException {
+		try {
+			rf.setContents(data);
+		} catch (IOException e) {
+			throw new FilesystemAccessException(
+					"failed to write file contents", e);
+		}
+	}
+
+	@Override
+	public void appendContents(byte[] data) throws FilesystemAccessException {
+		try {
+			rf.appendContents(data);
+		} catch (IOException e) {
+			throw new FilesystemAccessException(
+					"failed to write file contents", e);
+		}
+	}
+
+	@Override
+	public void copy(File from) throws FilesystemAccessException {
+		FileDelegate fromFile;
+		try {
+			fromFile = (FileDelegate) from;
+		} catch (ClassCastException e) {
+			throw new FilesystemAccessException("different types of File?!");
+		}
+
+		try {
+			rf.copy(fromFile.rf);
+		} catch (Exception e) {
+			throw new FilesystemAccessException("failed to copy file contents",
+					e);
+		}
+		return;
+	}
+}
+
+class ListenerDelegate implements Listener {
+	private Log log = getLog("Taverna.Server.Worker");
+	private RemoteListener r;
+	String conf;
+
+	ListenerDelegate(RemoteListener l) {
+		r = l;
+	}
+
+	RemoteListener getRemote() {
+		return r;
+	}
+
+	@Override
+	public String getConfiguration() {
+		try {
+			if (conf == null)
+				conf = r.getConfiguration();
+		} catch (RemoteException e) {
+			log.warn("failed to get configuration", e);
+		}
+		return conf;
+	}
+
+	@Override
+	public String getName() {
+		try {
+			return r.getName();
+		} catch (RemoteException e) {
+			log.warn("failed to get name", e);
+			return "UNKNOWN NAME";
+		}
+	}
+
+	@Override
+	public String getProperty(String propName) throws NoListenerException {
+		try {
+			return r.getProperty(propName);
+		} catch (RemoteException e) {
+			throw new NoListenerException("no such property: " + propName, e);
+		}
+	}
+
+	@Override
+	public String getType() {
+		try {
+			return r.getType();
+		} catch (RemoteException e) {
+			log.warn("failed to get type", e);
+			return "UNKNOWN TYPE";
+		}
+	}
+
+	@Override
+	public String[] listProperties() {
+		try {
+			return r.listProperties();
+		} catch (RemoteException e) {
+			log.warn("failed to list properties", e);
+			return new String[0];
+		}
+	}
+
+	@Override
+	public void setProperty(String propName, String value)
+			throws NoListenerException, BadPropertyValueException {
+		try {
+			r.setProperty(propName, value);
+		} catch (RemoteException e) {
+			log.warn("failed to set property", e);
+			if (e.getCause() != null
+					&& e.getCause() instanceof RuntimeException)
+				throw new NoListenerException("failed to set property",
+						e.getCause());
+			if (e.getCause() != null && e.getCause() instanceof Exception)
+				throw new BadPropertyValueException("failed to set property",
+						e.getCause());
+			throw new BadPropertyValueException("failed to set property", e);
+		}
+	}
+}
+
+class RunInput implements Input {
+	private final RemoteInput i;
+
+	RunInput(RemoteInput remote) {
+		this.i = remote;
+	}
+
+	@Override
+	public String getFile() {
+		try {
+			return i.getFile();
+		} catch (RemoteException e) {
+			return null;
+		}
+	}
+
+	@Override
+	public String getName() {
+		try {
+			return i.getName();
+		} catch (RemoteException e) {
+			return null;
+		}
+	}
+
+	@Override
+	public String getValue() {
+		try {
+			return i.getValue();
+		} catch (RemoteException e) {
+			return null;
+		}
+	}
+
+	@Override
+	public void setFile(String file) throws FilesystemAccessException,
+			BadStateChangeException {
+		checkBadFilename(file);
+		try {
+			i.setFile(file);
+		} catch (RemoteException e) {
+			throw new FilesystemAccessException("cannot set file for input", e);
+		}
+	}
+
+	@Override
+	public void setValue(String value) throws BadStateChangeException {
+		try {
+			i.setValue(value);
+		} catch (RemoteException e) {
+			throw new BadStateChangeException(e);
+		}
+	}
+
+	@Override
+	public String getDelimiter() {
+		try {
+			return i.getDelimiter();
+		} catch (RemoteException e) {
+			return null;
+		}
+	}
+
+	@Override
+	public void setDelimiter(String delimiter) throws BadStateChangeException {
+		try {
+			if (delimiter != null)
+				delimiter = delimiter.substring(0, 1);
+			i.setDelimiter(delimiter);
+		} catch (RemoteException e) {
+			throw new BadStateChangeException(e);
+		}
+	}
+}
+
+@SuppressWarnings("serial")
+class SecurityContextReconstructionException extends RuntimeException {
+	public SecurityContextReconstructionException(Throwable t) {
+		super("failed to rebuild security context", t);
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/RunConnection.java b/server-webapp/src/main/java/org/taverna/server/master/worker/RunConnection.java
new file mode 100644
index 0000000..cf55ea0
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/RunConnection.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static org.taverna.server.master.worker.RunConnection.COUNT_QUERY;
+import static org.taverna.server.master.worker.RunConnection.NAMES_QUERY;
+import static org.taverna.server.master.worker.RunConnection.SCHEMA;
+import static org.taverna.server.master.worker.RunConnection.TABLE;
+import static org.taverna.server.master.worker.RunConnection.TIMEOUT_QUERY;
+import static org.taverna.server.master.worker.RunConnection.UNTERMINATED_QUERY;
+
+import java.io.IOException;
+import java.rmi.MarshalledObject;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.jdo.annotations.Column;
+import javax.jdo.annotations.Join;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.PrimaryKey;
+import javax.jdo.annotations.Queries;
+import javax.jdo.annotations.Query;
+
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.interfaces.SecurityContextFactory;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * The representation of the connections to the runs that actually participates
+ * in the persistence system.
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceCapable(table = TABLE, schema = SCHEMA)
+@Queries({
+		@Query(name = "count", language = "SQL", value = COUNT_QUERY, unique = "true", resultClass = Integer.class),
+		@Query(name = "names", language = "SQL", value = NAMES_QUERY, unique = "false", resultClass = String.class),
+		@Query(name = "unterminated", language = "SQL", value = UNTERMINATED_QUERY, unique = "false", resultClass = String.class),
+		@Query(name = "timedout", language = "SQL", value = TIMEOUT_QUERY, unique = "false", resultClass = String.class) })
+public class RunConnection {
+	static final String SCHEMA = "TAVERNA";
+	static final String TABLE = "RUN_CONNECTION";
+	private static final String FULL_NAME = SCHEMA + "." + TABLE;
+	static final String COUNT_QUERY = "SELECT count(*) FROM " + FULL_NAME;
+	static final String NAMES_QUERY = "SELECT ID FROM " + FULL_NAME;
+	static final String TIMEOUT_QUERY = "SELECT ID FROM " + FULL_NAME
+			+ "   WHERE expiry < CURRENT_TIMESTAMP";
+	static final String UNTERMINATED_QUERY = "SELECT ID FROM " + FULL_NAME
+			+ "   WHERE doneTransitionToFinished = 0";
+	static final int NAME_LENGTH = 48; 
+
+	@PrimaryKey
+	@Column(length = 40)
+	private String id;
+
+	@Persistent(defaultFetchGroup = "true")
+	@Column(length = NAME_LENGTH)
+	private String name;
+
+	@Persistent(defaultFetchGroup = "true")
+	private Date creationInstant;
+
+	@Persistent(defaultFetchGroup = "true", serialized = "true")
+	@Column(jdbcType = "BLOB", sqlType = "BLOB")
+	private Workflow workflow;
+
+	@Persistent(defaultFetchGroup = "true")
+	private Date expiry;
+
+	@Persistent(defaultFetchGroup = "true")
+	@Join(table = TABLE + "_READERS", column = "ID")
+	private String[] readers;
+
+	@Persistent(defaultFetchGroup = "true")
+	@Join(table = TABLE + "_WRITERS", column = "ID")
+	private String[] writers;
+
+	@Persistent(defaultFetchGroup = "true")
+	@Join(table = TABLE + "_DESTROYERS", column = "ID")
+	private String[] destroyers;
+
+	@Persistent(defaultFetchGroup = "true", serialized = "true")
+	@Column(jdbcType = "BLOB", sqlType = "BLOB")
+	private MarshalledObject<RemoteSingleRun> run;
+
+	@Persistent(defaultFetchGroup = "true")
+	private int doneTransitionToFinished;
+
+	@Persistent(defaultFetchGroup = "true")
+	private int generateProvenance;
+
+	@Persistent(defaultFetchGroup = "true")
+	@Column(length = 128)
+	String owner;
+
+	@Persistent(defaultFetchGroup = "true")
+	@Column(length = 36)
+	private String securityToken;
+
+	@Persistent(defaultFetchGroup = "true", serialized = "true")
+	@Column(jdbcType = "BLOB", sqlType = "BLOB")
+	private SecurityContextFactory securityContextFactory;
+	@Persistent(defaultFetchGroup = "true", serialized = "true")
+	@Column(jdbcType = "BLOB", sqlType = "BLOB")
+	private Credential[] credentials;
+	@Persistent(defaultFetchGroup = "true", serialized = "true")
+	@Column(jdbcType = "BLOB", sqlType = "BLOB")
+	private Trust[] trust;
+
+	private static final String[] STRING_ARY = new String[0];
+
+	public String getId() {
+		return id;
+	}
+
+	public boolean isFinished() {
+		return doneTransitionToFinished != 0;
+	}
+
+	public void setFinished(boolean finished) {
+		doneTransitionToFinished = (finished ? 1 : 0);
+	}
+
+	public boolean isProvenanceGenerated() {
+		return generateProvenance != 0;
+	}
+
+	public void setProvenanceGenerated(boolean generate) {
+		generateProvenance = (generate ? 1 : 0);
+	}
+
+	/**
+	 * Manufacture a persistent representation of the given workflow run. Must
+	 * be called within the context of a transaction.
+	 * 
+	 * @param rrd
+	 *            The remote delegate of the workflow run.
+	 * @return The persistent object.
+	 * @throws IOException
+	 *             If serialisation fails.
+	 */
+	@Nonnull
+	public static RunConnection toDBform(@Nonnull RemoteRunDelegate rrd)
+			throws IOException {
+		RunConnection rc = new RunConnection();
+		rc.id = rrd.id;
+		rc.makeChanges(rrd);
+		return rc;
+	}
+
+	private static List<String> list(String[] ary) {
+		if (ary == null)
+			return emptyList();
+		return asList(ary);
+	}
+
+	/**
+	 * Get the remote run delegate for a particular persistent connection. Must
+	 * be called within the context of a transaction.
+	 * 
+	 * @param db
+	 *            The database facade.
+	 * @return The delegate object.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	@Nonnull
+	public RemoteRunDelegate fromDBform(@Nonnull RunDBSupport db)
+			throws Exception {
+		RemoteRunDelegate rrd = new RemoteRunDelegate();
+		rrd.id = getId();
+		rrd.creationInstant = creationInstant;
+		rrd.workflow = workflow;
+		rrd.expiry = expiry;
+		rrd.readers = new HashSet<>(list(readers));
+		rrd.writers = new HashSet<>(list(writers));
+		rrd.destroyers = new HashSet<>(list(destroyers));
+		rrd.run = run.get();
+		rrd.doneTransitionToFinished = isFinished();
+		rrd.generateProvenance = isProvenanceGenerated();
+		rrd.secContext = securityContextFactory.create(rrd,
+				new UsernamePrincipal(owner));
+		((SecurityContextDelegate)rrd.secContext).setCredentialsAndTrust(credentials,trust);
+		rrd.db = db;
+		rrd.factory = db.getFactory();
+		rrd.name = name;
+		return rrd;
+	}
+
+	/**
+	 * Flush changes from a remote run delegate to the database. Must be called
+	 * within the context of a transaction.
+	 * 
+	 * @param rrd
+	 *            The remote run delegate object that has potential changes.
+	 * @throws IOException
+	 *             If anything goes wrong in serialization.
+	 */
+	public void makeChanges(@Nonnull RemoteRunDelegate rrd) throws IOException {
+		// Properties that are set exactly once
+		if (creationInstant == null) {
+			creationInstant = rrd.getCreationTimestamp();
+			workflow = rrd.getWorkflow();
+			run = new MarshalledObject<>(rrd.run);
+			securityContextFactory = rrd.getSecurityContext().getFactory();
+			owner = rrd.getSecurityContext().getOwner().getName();
+			securityToken = ((org.taverna.server.master.worker.SecurityContextFactory) securityContextFactory)
+					.issueNewPassword();
+		}
+		// Properties that are set multiple times
+		expiry = rrd.getExpiry();
+		readers = rrd.getReaders().toArray(STRING_ARY);
+		writers = rrd.getWriters().toArray(STRING_ARY);
+		destroyers = rrd.getDestroyers().toArray(STRING_ARY);
+		credentials = rrd.getSecurityContext().getCredentials();
+		trust = rrd.getSecurityContext().getTrusted();
+		if (rrd.name.length() > NAME_LENGTH)
+			this.name = rrd.name.substring(0, NAME_LENGTH);
+		else
+			this.name = rrd.name;
+		setFinished(rrd.doneTransitionToFinished);
+		setProvenanceGenerated(rrd.generateProvenance);
+	}
+
+	public String getSecurityToken() {
+		return securityToken;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/RunDBSupport.java b/server-webapp/src/main/java/org/taverna/server/master/worker/RunDBSupport.java
new file mode 100644
index 0000000..2aa7ed1
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/RunDBSupport.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.taverna.server.master.notification.NotificationEngine;
+
+/**
+ * The interface to the database of runs.
+ * 
+ * @author Donal Fellows
+ */
+public interface RunDBSupport {
+	/**
+	 * Scan each run to see if it has finished yet and issue registered
+	 * notifications if it has.
+	 */
+	void checkForFinishNow();
+
+	/**
+	 * Remove currently-expired runs from this database.
+	 */
+	void cleanNow();
+
+	/**
+	 * How many runs are stored in the database.
+	 * 
+	 * @return The current size of the run table.
+	 */
+	int countRuns();
+
+	/**
+	 * Ensure that a run gets persisted in the database. It is assumed that the
+	 * value is already in there.
+	 * 
+	 * @param run
+	 *            The run to persist.
+	 */
+	void flushToDisk(@Nonnull RemoteRunDelegate run);
+
+	/**
+	 * Select an arbitrary representative run.
+	 * 
+	 * @return The selected run.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	@Nullable
+	RemoteRunDelegate pickArbitraryRun() throws Exception;
+
+	/**
+	 * Get a list of all the run names.
+	 * 
+	 * @return The names (i.e., UUIDs) of all the runs.
+	 */
+	@Nonnull
+	List<String> listRunNames();
+
+	/**
+	 * @param notificationEngine
+	 *            A reference to the notification fabric bean.
+	 */
+	void setNotificationEngine(NotificationEngine notificationEngine);
+
+	/**
+	 * @param notifier
+	 *            A reference to the bean that creates messages about workflow
+	 *            run termination.
+	 */
+	void setNotifier(CompletionNotifier notifier);
+
+	/**
+	 * @return A reference to the actual factory for remote runs.
+	 */
+	FactoryBean getFactory();
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/RunDatabase.java b/server-webapp/src/main/java/org/taverna/server/master/worker/RunDatabase.java
new file mode 100644
index 0000000..cedb4b5
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/RunDatabase.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static java.lang.Integer.parseInt;
+import static java.util.UUID.randomUUID;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+
+import javax.annotation.Nullable;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.Policy;
+import org.taverna.server.master.interfaces.RunStore;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.notification.NotificationEngine;
+import org.taverna.server.master.notification.NotificationEngine.Message;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * The main facade bean that interfaces to the database of runs.
+ * 
+ * @author Donal Fellows
+ */
+public class RunDatabase implements RunStore, RunDBSupport {
+	private Log log = LogFactory.getLog("Taverna.Server.Worker.RunDB");
+	RunDatabaseDAO dao;
+	CompletionNotifier backupNotifier;
+	Map<String, CompletionNotifier> typedNotifiers;
+	private NotificationEngine notificationEngine;
+	@Autowired
+	private FactoryBean factory;
+	private Map<String, TavernaRun> cache = new HashMap<>();
+
+	@Override
+	@Required
+	public void setNotifier(CompletionNotifier n) {
+		backupNotifier = n;
+	}
+
+	public void setTypeNotifiers(List<CompletionNotifier> notifiers) {
+		typedNotifiers = new HashMap<>();
+		for (CompletionNotifier n : notifiers)
+			typedNotifiers.put(n.getName(), n);
+	}
+
+	@Required
+	@Override
+	public void setNotificationEngine(NotificationEngine notificationEngine) {
+		this.notificationEngine = notificationEngine;
+	}
+
+	@Required
+	public void setDao(RunDatabaseDAO dao) {
+		this.dao = dao;
+	}
+
+	@Override
+	public void checkForFinishNow() {
+		/*
+		 * Get which runs are actually newly finished; this requires getting the
+		 * candidates from the database and *then* doing the expensive requests
+		 * to the back end to find out the status.
+		 */
+		Map<String, RemoteRunDelegate> notifiable = new HashMap<>();
+		for (RemoteRunDelegate p : dao.getPotentiallyNotifiable())
+			if (p.getStatus() == Status.Finished)
+				notifiable.put(p.getId(), p);
+
+		// Check if there's nothing more to do
+		if (notifiable.isEmpty())
+			return;
+
+		/*
+		 * Tell the database about the ones we've got.
+		 */
+		dao.markFinished(notifiable.keySet());
+
+		/*
+		 * Send out the notifications. The notification addresses are stored in
+		 * the back-end engine, so this is *another* thing that can take time.
+		 */
+		for (RemoteRunDelegate rrd : notifiable.values())
+			for (Listener l : rrd.getListeners())
+				if (l.getName().equals("io")) {
+					try {
+						notifyFinished(rrd.id, l, rrd);
+					} catch (Exception e) {
+						log.warn("failed to do notification of completion", e);
+					}
+					break;
+				}
+	}
+
+	@Override
+	public void cleanNow() {
+		List<String> cleaned;
+		try {
+			cleaned = dao.doClean();
+		} catch (Exception e) {
+			log.warn("failure during deletion of expired runs", e);
+			return;
+		}
+		synchronized (cache) {
+			for (String id : cleaned)
+				cache.remove(id);
+		}
+	}
+
+	@Override
+	public int countRuns() {
+		return dao.countRuns();
+	}
+
+	@Override
+	public void flushToDisk(RemoteRunDelegate run) {
+		try {
+			dao.flushToDisk(run);
+		} catch (IOException e) {
+			throw new RuntimeException(
+					"unexpected problem when persisting run record in database",
+					e);
+		}
+	}
+
+	@Override
+	public RemoteRunDelegate pickArbitraryRun() throws Exception {
+		return dao.pickArbitraryRun();
+	}
+
+	@Override
+	public List<String> listRunNames() {
+		return dao.listRunNames();
+	}
+
+	@Nullable
+	private TavernaRun get(String uuid) {
+		TavernaRun run = null;
+		synchronized (cache) {
+			run = cache.get(uuid);
+		}
+		try {
+			if (run != null)
+				run.ping();
+		} catch (UnknownRunException e) {
+			if (log.isDebugEnabled())
+				log.debug("stale mapping in cache?", e);
+			// Don't need to flush the cache; this happens when cleaning anyway
+			run = null;
+		}
+		if (run == null)
+			run = dao.get(uuid);
+		return run;
+	}
+
+	@Override
+	public TavernaRun getRun(UsernamePrincipal user, Policy p, String uuid)
+			throws UnknownRunException {
+		// Check first to see if the 'uuid' actually looks like a UUID; if
+		// not, throw it out immediately without logging an exception.
+		try {
+			UUID.fromString(uuid);
+		} catch (IllegalArgumentException e) {
+			if (log.isDebugEnabled())
+				log.debug("run ID does not look like UUID; rejecting...");
+			throw new UnknownRunException();
+		}
+		TavernaRun run = get(uuid);
+		if (run != null && (user == null || p.permitAccess(user, run)))
+			return run;
+		throw new UnknownRunException();
+	}
+
+	@Override
+	public TavernaRun getRun(String uuid) throws UnknownRunException {
+		TavernaRun run = get(uuid);
+		if (run != null)
+			return run;
+		throw new UnknownRunException();
+	}
+
+	@Override
+	public Map<String, TavernaRun> listRuns(UsernamePrincipal user, Policy p) {
+		synchronized (cache) {
+			Map<String, TavernaRun> cached = new HashMap<>();
+			for (Entry<String, TavernaRun> e : cache.entrySet()) {
+				TavernaRun r = e.getValue();
+				if (p.permitAccess(user, r))
+					cached.put(e.getKey(), r);
+			}
+			if (!cached.isEmpty())
+				return cached;
+		}
+		return dao.listRuns(user, p);
+	}
+
+	private void logLength(String message, Object obj) {
+		if (!log.isDebugEnabled())
+			return;
+		try {
+			ByteArrayOutputStream baos = new ByteArrayOutputStream();
+			try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+				oos.writeObject(obj);
+			}
+			log.debug(message + ": " + baos.size());
+		} catch (Exception e) {
+			log.warn("oops", e);
+		}
+	}
+
+	@Override
+	public String registerRun(TavernaRun run) {
+		if (!(run instanceof RemoteRunDelegate))
+			throw new IllegalArgumentException(
+					"run must be created by localworker package");
+		RemoteRunDelegate rrd = (RemoteRunDelegate) run;
+		if (rrd.id == null)
+			rrd.id = randomUUID().toString();
+		logLength("RemoteRunDelegate serialized length", rrd);
+		try {
+			dao.persistRun(rrd);
+		} catch (IOException e) {
+			throw new RuntimeException(
+					"unexpected problem when persisting run record in database",
+					e);
+		}
+		synchronized (cache) {
+			cache.put(rrd.getId(), run);
+		}
+		return rrd.getId();
+	}
+
+	@Override
+	public void unregisterRun(String uuid) {
+		try {
+			if (dao.unpersistRun(uuid))
+				synchronized (cache) {
+					cache.remove(uuid);
+				}
+		} catch (RuntimeException e) {
+			if (log.isDebugEnabled())
+				log.debug("problem persisting the deletion of the run " + uuid,
+						e);
+		}
+	}
+
+	/**
+	 * Process the event that a run has finished.
+	 * 
+	 * @param name
+	 *            The name of the run.
+	 * @param io
+	 *            The io listener of the run (used to get information about the
+	 *            run).
+	 * @param run
+	 *            The handle to the run.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	private void notifyFinished(final String name, Listener io,
+			final RemoteRunDelegate run) throws Exception {
+		String to = io.getProperty("notificationAddress");
+		final int code;
+		try {
+			code = parseInt(io.getProperty("exitcode"));
+		} catch (NumberFormatException nfe) {
+			// Ignore; not much we can do here...
+			return;
+		}
+
+		notificationEngine.dispatchMessage(run, to, new Message() {
+			private CompletionNotifier getNotifier(String type) {
+				CompletionNotifier n = typedNotifiers.get(type);
+				if (n == null)
+					n = backupNotifier;
+				return n;
+			}
+
+			@Override
+			public String getContent(String type) {
+				return getNotifier(type).makeCompletionMessage(name, run, code);
+			}
+
+			@Override
+			public String getTitle(String type) {
+				return getNotifier(type).makeMessageSubject(name, run, code);
+			}
+		});
+	}
+
+	@Override
+	public FactoryBean getFactory() {
+		return factory;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/RunDatabaseDAO.java b/server-webapp/src/main/java/org/taverna/server/master/worker/RunDatabaseDAO.java
new file mode 100644
index 0000000..51931c0
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/RunDatabaseDAO.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static org.taverna.server.master.worker.RunConnection.toDBform;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.jdo.annotations.PersistenceAware;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.interfaces.Policy;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
+import org.taverna.server.master.utils.JDOSupport;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * This handles storing runs, interfacing with the underlying state engine as
+ * necessary.
+ * 
+ * @author Donal Fellows
+ */
+@PersistenceAware
+public class RunDatabaseDAO extends JDOSupport<RunConnection> {
+	public RunDatabaseDAO() {
+		super(RunConnection.class);
+	}
+
+	private Log log = LogFactory.getLog("Taverna.Server.Worker.RunDB");
+	private RunDatabase facade;
+
+	@Required
+	public void setFacade(RunDatabase facade) {
+		this.facade = facade;
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+	@SuppressWarnings("unchecked")
+	private List<String> nameRuns() {
+		if (log.isDebugEnabled())
+			log.debug("fetching all run names");
+		return (List<String>) namedQuery("names").execute();
+	}
+
+	/**
+	 * @return The number of workflow runs in the database.
+	 */
+	@WithinSingleTransaction
+	public int countRuns() {
+		if (log.isDebugEnabled())
+			log.debug("counting the number of runs");
+		return (Integer) namedQuery("count").execute();
+	}
+
+	@SuppressWarnings("unchecked")
+	private List<String> expiredRuns() {
+		return (List<String>) namedQuery("timedout").execute();
+	}
+
+	@SuppressWarnings("unchecked")
+	private List<String> unterminatedRuns() {
+		return (List<String>) namedQuery("unterminated").execute();
+	}
+
+	@Nullable
+	private RunConnection pickRun(@Nonnull String name) {
+		if (log.isDebugEnabled())
+			log.debug("fetching the run called " + name);
+		try {
+			RunConnection rc = getById(name);
+			if (rc == null)
+				log.warn("no result for " + name);
+			return rc;
+		} catch (RuntimeException e) {
+			log.warn("problem in fetch", e);
+			throw e;
+		}
+	}
+
+	@Nullable
+	@WithinSingleTransaction
+	public String getSecurityToken(@Nonnull String name) {
+		RunConnection rc = getById(name);
+		if (rc == null)
+			return null;
+		return rc.getSecurityToken();
+	}
+
+	private void persist(@Nonnull RemoteRunDelegate rrd) throws IOException {
+		persist(toDBform(rrd));
+	}
+
+	@Nonnull
+	private List<RunConnection> allRuns() {
+		try {
+			List<RunConnection> rcs = new ArrayList<>();
+			List<String> names = nameRuns();
+			for (String id : names) {
+				try {
+					if (id != null)
+						rcs.add(pickRun(id));
+				} catch (RuntimeException e) {
+					continue;
+				}
+			}
+			return rcs;
+		} catch (RuntimeException e) {
+			log.warn("problem in fetch", e);
+			throw e;
+		}
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
+
+	/**
+	 * Obtain a workflow run handle.
+	 * 
+	 * @param name
+	 *            The identifier of the run.
+	 * @return The run handle, or <tt>null</tt> if there is no such run.
+	 */
+	@Nullable
+	@WithinSingleTransaction
+	public TavernaRun get(String name) {
+		try {
+			RunConnection rc = pickRun(name);
+			return (rc == null) ? null : rc.fromDBform(facade);
+		} catch (Exception e) {
+			return null;
+		}
+	}
+
+	/**
+	 * Get the runs that a user can read things from.
+	 * 
+	 * @param user
+	 *            Who is asking?
+	 * @param p
+	 *            The policy that determines what they can see.
+	 * @return A mapping from run IDs to run handles.
+	 */
+	@Nonnull
+	@WithinSingleTransaction
+	public Map<String, TavernaRun> listRuns(UsernamePrincipal user, Policy p) {
+		Map<String, TavernaRun> result = new HashMap<>();
+		for (String id : nameRuns())
+			try {
+				RemoteRunDelegate rrd = pickRun(id).fromDBform(facade);
+				if (p.permitAccess(user, rrd))
+					result.put(id, rrd);
+			} catch (Exception e) {
+				continue;
+			}
+		return result;
+	}
+
+	/**
+	 * @return A list of the IDs for all workflow runs.
+	 */
+	@Nonnull
+	@WithinSingleTransaction
+	public List<String> listRunNames() {
+		List<String> runNames = new ArrayList<>();
+		for (RunConnection rc : allRuns())
+			if (rc.getId() != null)
+				runNames.add(rc.getId());
+		return runNames;
+	}
+
+	/**
+	 * @return An arbitrary, representative workflow run.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	@Nullable
+	@WithinSingleTransaction
+	public RemoteRunDelegate pickArbitraryRun() throws Exception {
+		for (RunConnection rc : allRuns()) {
+			if (rc.getId() == null)
+				continue;
+			return rc.fromDBform(facade);
+		}
+		return null;
+	}
+
+	/**
+	 * Make a workflow run persistent. Must only be called once per workflow
+	 * run.
+	 * 
+	 * @param rrd
+	 *            The workflow run to persist.
+	 * @throws IOException
+	 *             If anything goes wrong with serialisation of the run.
+	 */
+	@WithinSingleTransaction
+	public void persistRun(@Nonnull RemoteRunDelegate rrd) throws IOException {
+		persist(rrd);
+	}
+
+	/**
+	 * Stop a workflow run from being persistent.
+	 * 
+	 * @param name
+	 *            The ID of the run.
+	 * @return Whether a deletion happened.
+	 */
+	@WithinSingleTransaction
+	public boolean unpersistRun(String name) {
+		RunConnection rc = pickRun(name);
+		if (rc != null)
+			delete(rc);
+		return rc != null;
+	}
+
+	/**
+	 * Ensure that the given workflow run is synchronized with the database.
+	 * 
+	 * @param run
+	 *            The run to synchronise.
+	 * @throws IOException
+	 *             If serialization of anything fails.
+	 */
+	@WithinSingleTransaction
+	public void flushToDisk(@Nonnull RemoteRunDelegate run) throws IOException {
+		getById(run.id).makeChanges(run);
+	}
+
+	/**
+	 * Remove all workflow runs that have expired.
+	 * 
+	 * @return The ids of the deleted runs.
+	 */
+	@Nonnull
+	@PerfLogged
+	@WithinSingleTransaction
+	public List<String> doClean() {
+		if (log.isDebugEnabled())
+			log.debug("deleting runs that timed out before " + new Date());
+		List<String> toDelete = expiredRuns();
+		if (log.isDebugEnabled())
+			log.debug("found " + toDelete.size() + " runs to delete");
+		for (String id : toDelete) {
+			RunConnection rc = getById(id);
+			try {
+				rc.fromDBform(facade).run.destroy();
+			} catch (Exception e) {
+				if (log.isDebugEnabled())
+					log.debug("failed to delete execution resource for " + id,
+							e);
+			}
+			delete(rc);
+		}
+		return toDelete;
+	}
+
+	/**
+	 * @return A list of workflow runs that are candidates for doing
+	 *         notification of termination.
+	 */
+	@Nonnull
+	@PerfLogged
+	@WithinSingleTransaction
+	public List<RemoteRunDelegate> getPotentiallyNotifiable() {
+		List<RemoteRunDelegate> toNotify = new ArrayList<>();
+		for (String id : unterminatedRuns())
+			try {
+				RunConnection rc = getById(id);
+				toNotify.add(rc.fromDBform(facade));
+			} catch (Exception e) {
+				log.warn("failed to fetch connection token"
+						+ "for notification of completion check", e);
+			}
+		return toNotify;
+	}
+
+	@PerfLogged
+	@WithinSingleTransaction
+	public void markFinished(@Nonnull Set<String> terminated) {
+		for (String id : terminated) {
+			RunConnection rc = getById(id);
+			if (rc == null)
+				continue;
+			try {
+				rc.fromDBform(facade).doneTransitionToFinished = true;
+				rc.setFinished(true);
+			} catch (Exception e) {
+				log.warn("failed to note termination", e);
+			}
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/RunFactoryConfiguration.java b/server-webapp/src/main/java/org/taverna/server/master/worker/RunFactoryConfiguration.java
new file mode 100644
index 0000000..29ac884
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/RunFactoryConfiguration.java
@@ -0,0 +1,395 @@
+package org.taverna.server.master.worker;
+
+import static org.springframework.jmx.support.MetricType.COUNTER;
+import static org.springframework.jmx.support.MetricType.GAUGE;
+import static org.taverna.server.master.TavernaServer.JMX_ROOT;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.PreDestroy;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.annotation.Order;
+import org.springframework.jmx.export.annotation.ManagedAttribute;
+import org.springframework.jmx.export.annotation.ManagedMetric;
+import org.springframework.jmx.export.annotation.ManagedResource;
+import org.taverna.server.master.factories.ConfigurableRunFactory;
+import org.taverna.server.master.localworker.LocalWorkerState;
+
+@ManagedResource(objectName = JMX_ROOT + "Factory", description = "The factory for runs.")
+public abstract class RunFactoryConfiguration implements ConfigurableRunFactory {
+	protected Log log = LogFactory.getLog("Taverna.Server.Worker");
+	protected LocalWorkerState state;
+	protected RunDBSupport runDB;
+	private int totalRuns = 0;
+
+	@PreDestroy
+	void closeLog() {
+		log = null;
+	}
+
+	@Autowired(required = true)
+	@Order(0)
+	void setState(LocalWorkerState state) {
+		this.state = state;
+	}
+
+	@Autowired(required = true)
+	@Order(0)
+	void setRunDB(RunDBSupport runDB) {
+		this.runDB = runDB;
+	}
+
+	/**
+	 * Drop any current references to the registry of runs, and kill off that
+	 * process.
+	 */
+	protected abstract void reinitRegistry();
+
+	/**
+	 * Drop any current references to the run factory subprocess and kill it
+	 * off.
+	 */
+	protected abstract void reinitFactory();
+
+	/** Count the number of operating runs. */
+	protected abstract int operatingCount() throws Exception;
+
+	protected final synchronized void incrementRunCount() {
+		totalRuns++;
+	}
+
+	@Override
+	@ManagedAttribute(description = "Whether it is allowed to start a run executing.", currencyTimeLimit = 30)
+	public final boolean isAllowingRunsToStart() {
+		try {
+			return state.getOperatingLimit() > getOperatingCount();
+		} catch (Exception e) {
+			log.info("failed to get operating run count", e);
+			return false;
+		}
+	}
+
+	@Override
+	@ManagedAttribute(description = "The host holding the RMI registry to communicate via.")
+	public final String getRegistryHost() {
+		return state.getRegistryHost();
+	}
+
+	@Override
+	@ManagedAttribute(description = "The host holding the RMI registry to communicate via.")
+	public final void setRegistryHost(String host) {
+		state.setRegistryHost(host);
+		reinitRegistry();
+		reinitFactory();
+	}
+
+	@Override
+	@ManagedAttribute(description = "The port number of the RMI registry. Should not normally be set.")
+	public final int getRegistryPort() {
+		return state.getRegistryPort();
+	}
+
+	@Override
+	@ManagedAttribute(description = "The port number of the RMI registry. Should not normally be set.")
+	public final void setRegistryPort(int port) {
+		state.setRegistryPort(port);
+		reinitRegistry();
+		reinitFactory();
+	}
+
+	@Nonnull
+	@Override
+	@ManagedAttribute(description = "What JAR do we use to start the RMI registry process?")
+	public final String getRmiRegistryJar() {
+		return state.getRegistryJar();
+	}
+
+	@Override
+	@ManagedAttribute(description = "What JAR do we use to start the RMI registry process?")
+	public final void setRmiRegistryJar(String rmiRegistryJar) {
+		state.setRegistryJar(rmiRegistryJar);
+		reinitRegistry();
+		reinitFactory();
+	}
+
+	@Override
+	@ManagedAttribute(description = "The maximum number of simultaneous runs supported by the server.", currencyTimeLimit = 300)
+	public final int getMaxRuns() {
+		return state.getMaxRuns();
+	}
+
+	@Override
+	@ManagedAttribute(description = "The maximum number of simultaneous runs supported by the server.", currencyTimeLimit = 300)
+	public final void setMaxRuns(int maxRuns) {
+		state.setMaxRuns(maxRuns);
+	}
+
+	/** @return How many minutes should a workflow live by default? */
+	@Override
+	@ManagedAttribute(description = "How many minutes should a workflow live by default?", currencyTimeLimit = 300)
+	public final int getDefaultLifetime() {
+		return state.getDefaultLifetime();
+	}
+
+	/**
+	 * Set how long a workflow should live by default.
+	 * 
+	 * @param defaultLifetime
+	 *            Default lifetime, in minutes.
+	 */
+	@Override
+	@ManagedAttribute(description = "How many minutes should a workflow live by default?", currencyTimeLimit = 300)
+	public final void setDefaultLifetime(int defaultLifetime) {
+		state.setDefaultLifetime(defaultLifetime);
+	}
+
+	/**
+	 * @return How many milliseconds to wait between checks to see if a worker
+	 *         process has registered.
+	 */
+	@Override
+	@ManagedAttribute(description = "How many milliseconds to wait between checks to see if a worker process has registered.", currencyTimeLimit = 300)
+	public final int getSleepTime() {
+		return state.getSleepMS();
+	}
+
+	/**
+	 * @param sleepTime
+	 *            How many milliseconds to wait between checks to see if a
+	 *            worker process has registered.
+	 */
+	@Override
+	@ManagedAttribute(description = "How many milliseconds to wait between checks to see if a worker process has registered.", currencyTimeLimit = 300)
+	public final void setSleepTime(int sleepTime) {
+		state.setSleepMS(sleepTime);
+	}
+
+	/**
+	 * @return How many seconds to wait for a worker process to register itself.
+	 */
+	@Override
+	@ManagedAttribute(description = "How many seconds to wait for a worker process to register itself.", currencyTimeLimit = 300)
+	public final int getWaitSeconds() {
+		return state.getWaitSeconds();
+	}
+
+	/**
+	 * @param seconds
+	 *            How many seconds to wait for a worker process to register
+	 *            itself.
+	 */
+	@Override
+	@ManagedAttribute(description = "How many seconds to wait for a worker process to register itself.", currencyTimeLimit = 300)
+	public final void setWaitSeconds(int seconds) {
+		state.setWaitSeconds(seconds);
+	}
+
+	/** @return The script to run to start running a workflow. */
+	@Nonnull
+	@Override
+	@ManagedAttribute(description = "The script to run to start running a workflow.", currencyTimeLimit = 300)
+	public final String getExecuteWorkflowScript() {
+		return state.getExecuteWorkflowScript();
+	}
+
+	/**
+	 * @param executeWorkflowScript
+	 *            The script to run to start running a workflow.
+	 */
+	@Override
+	@ManagedAttribute(description = "The script to run to start running a workflow.", currencyTimeLimit = 300)
+	public final void setExecuteWorkflowScript(String executeWorkflowScript) {
+		state.setExecuteWorkflowScript(executeWorkflowScript);
+		reinitFactory();
+	}
+
+	/** @return The location of the JAR implementing the server worker processes. */
+	@Nonnull
+	@Override
+	@ManagedAttribute(description = "The location of the JAR implementing the server worker processes.")
+	public final String getServerWorkerJar() {
+		return state.getServerWorkerJar();
+	}
+
+	/**
+	 * @param serverWorkerJar
+	 *            The location of the JAR implementing the server worker
+	 *            processes.
+	 */
+	@Override
+	@ManagedAttribute(description = "The location of the JAR implementing the server worker processes.")
+	public final void setServerWorkerJar(String serverWorkerJar) {
+		state.setServerWorkerJar(serverWorkerJar);
+		reinitFactory();
+	}
+
+	/** @return The list of additional arguments used to make a worker process. */
+	@Nonnull
+	@Override
+	@ManagedAttribute(description = "The list of additional arguments used to make a worker process.", currencyTimeLimit = 300)
+	public final String[] getExtraArguments() {
+		return state.getExtraArgs();
+	}
+
+	/**
+	 * @param extraArguments
+	 *            The list of additional arguments used to make a worker
+	 *            process.
+	 */
+	@Override
+	@ManagedAttribute(description = "The list of additional arguments used to make a worker process.", currencyTimeLimit = 300)
+	public final void setExtraArguments(@Nonnull String[] extraArguments) {
+		state.setExtraArgs(extraArguments);
+		reinitFactory();
+	}
+
+	/** @return Which java executable to run. */
+	@Nonnull
+	@Override
+	@ManagedAttribute(description = "Which java executable to run.", currencyTimeLimit = 300)
+	public final String getJavaBinary() {
+		return state.getJavaBinary();
+	}
+
+	/**
+	 * @param javaBinary
+	 *            Which java executable to run.
+	 */
+	@Override
+	@ManagedAttribute(description = "Which java executable to run.", currencyTimeLimit = 300)
+	public final void setJavaBinary(@Nonnull String javaBinary) {
+		state.setJavaBinary(javaBinary);
+		reinitFactory();
+	}
+
+	/**
+	 * @return A file containing a password to use when running a program as
+	 *         another user (e.g., with sudo).
+	 */
+	@Nullable
+	@Override
+	@ManagedAttribute(description = "A file containing a password to use when running a program as another user (e.g., with sudo).", currencyTimeLimit = 300)
+	public final String getPasswordFile() {
+		return state.getPasswordFile();
+	}
+
+	/**
+	 * @param passwordFile
+	 *            A file containing a password to use when running a program as
+	 *            another user (e.g., with sudo).
+	 */
+	@Override
+	@ManagedAttribute(description = "A file containing a password to use when running a program as another user (e.g., with sudo).", currencyTimeLimit = 300)
+	public final void setPasswordFile(@Nullable String passwordFile) {
+		state.setPasswordFile(passwordFile);
+		reinitFactory();
+	}
+
+	/**
+	 * @return The location of the JAR implementing the secure-fork process.
+	 */
+	@Nonnull
+	@Override
+	@ManagedAttribute(description = "The location of the JAR implementing the secure-fork process.", currencyTimeLimit = 300)
+	public final String getServerForkerJar() {
+		return state.getServerForkerJar();
+	}
+
+	/**
+	 * @param serverForkerJar
+	 *            The location of the JAR implementing the secure-fork process.
+	 */
+	@Override
+	@ManagedAttribute(description = "The location of the JAR implementing the secure-fork process.", currencyTimeLimit = 300)
+	public final void setServerForkerJar(String forkerJarFilename) {
+		state.setServerForkerJar(forkerJarFilename);
+		reinitFactory();
+	}
+
+	/**
+	 * @return How many times has a workflow run been spawned by this engine.
+	 *         Restarts reset this counter.
+	 */
+	@Override
+	@ManagedMetric(description = "How many times has a workflow run been spawned by this engine.", currencyTimeLimit = 10, metricType = COUNTER, category = "throughput")
+	public final synchronized int getTotalRuns() {
+		return totalRuns;
+	}
+
+	/**
+	 * @return How many checks were done for the worker process the last time a
+	 *         spawn was tried.
+	 */
+	@Override
+	@ManagedAttribute(description = "How many checks were done for the worker process the last time a spawn was tried.", currencyTimeLimit = 60)
+	public abstract int getLastStartupCheckCount();
+
+	@Nonnull
+	@Override
+	@ManagedAttribute(description = "The names of the current runs.", currencyTimeLimit = 5)
+	public final String[] getCurrentRunNames() {
+		List<String> names = runDB.listRunNames();
+		return names.toArray(new String[names.size()]);
+	}
+
+	@Override
+	@ManagedAttribute(description = "What the factory subprocess's main RMI interface is registered as.", currencyTimeLimit = 60)
+	public abstract String getFactoryProcessName();
+
+	/**
+	 * @return What was the exit code from the last time the factory subprocess
+	 *         was killed?
+	 */
+	@Override
+	@ManagedAttribute(description = "What was the exit code from the last time the factory subprocess was killed?")
+	public abstract Integer getLastExitCode();
+
+	/**
+	 * @return The mapping of user names to RMI factory IDs.
+	 */
+	@Override
+	@ManagedAttribute(description = "The mapping of user names to RMI factory IDs.", currencyTimeLimit = 60)
+	public abstract String[] getFactoryProcessMapping();
+
+	@Override
+	@ManagedAttribute(description = "The maximum number of simultaneous operating runs supported by the server.", currencyTimeLimit = 300)
+	public final void setOperatingLimit(int operatingLimit) {
+		state.setOperatingLimit(operatingLimit);
+	}
+
+	@Override
+	@ManagedAttribute(description = "The maximum number of simultaneous operating runs supported by the server.", currencyTimeLimit = 300)
+	public final int getOperatingLimit() {
+		return state.getOperatingLimit();
+	}
+
+	/**
+	 * @return A count of the number of runs believed to actually be in the
+	 *         {@linkplain uk.org.taverna.server.master.common.Status#Operating
+	 *         operating} state.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	@Override
+	@ManagedMetric(description = "How many workflow runs are currently actually executing.", currencyTimeLimit = 10, metricType = GAUGE, category = "throughput")
+	public final int getOperatingCount() throws Exception {
+		return operatingCount();
+	}
+
+	@Override
+	@ManagedAttribute(description="Whether to tell a workflow to generate provenance bundles by default.")
+	public final void setGenerateProvenance(boolean genProv) {
+		state.setGenerateProvenance(genProv);
+	}
+
+	@Override
+	@ManagedAttribute(description="Whether to tell a workflow to generate provenance bundles by default.")
+	public final boolean getGenerateProvenance() {
+		return state.getGenerateProvenance();
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextDelegate.java b/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextDelegate.java
new file mode 100644
index 0000000..ff14986
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextDelegate.java
@@ -0,0 +1,649 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static java.lang.String.format;
+import static java.util.Arrays.fill;
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.defaults.Default.CERTIFICATE_FIELD_NAMES;
+import static org.taverna.server.master.defaults.Default.CERTIFICATE_TYPE;
+import static org.taverna.server.master.defaults.Default.CREDENTIAL_FILE_SIZE_LIMIT;
+import static org.taverna.server.master.identity.WorkflowInternalAuthProvider.PREFIX;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.rmi.RemoteException;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.security.auth.x500.X500Principal;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.UriBuilder;
+import javax.xml.ws.handler.MessageContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.taverna.server.localworker.remote.ImplementationException;
+import org.taverna.server.localworker.remote.RemoteSecurityContext;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.exceptions.NoDirectoryEntryException;
+import org.taverna.server.master.interfaces.File;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Implementation of a security context.
+ * 
+ * @author Donal Fellows
+ */
+public abstract class SecurityContextDelegate implements TavernaSecurityContext {
+	Log log = LogFactory.getLog("Taverna.Server.Worker");
+	private final UsernamePrincipal owner;
+	private final List<Credential> credentials = new ArrayList<>();
+	private final List<Trust> trusted = new ArrayList<>();
+	private final RemoteRunDelegate run;
+	private final Object lock = new Object();
+	final SecurityContextFactory factory;
+
+	private transient Keystore keystore;
+	private transient Map<URI, String> uriToAliasMap;
+
+	/**
+	 * Initialise the context delegate.
+	 * 
+	 * @param run
+	 *            What workflow run is this for?
+	 * @param owner
+	 *            Who owns the workflow run?
+	 * @param factory
+	 *            What class built this object?
+	 */
+	protected SecurityContextDelegate(RemoteRunDelegate run,
+			UsernamePrincipal owner, SecurityContextFactory factory) {
+		this.run = run;
+		this.owner = owner;
+		this.factory = factory;
+	}
+
+	@Override
+	public SecurityContextFactory getFactory() {
+		return factory;
+	}
+
+	@Override
+	public UsernamePrincipal getOwner() {
+		return owner;
+	}
+
+	@Override
+	public Credential[] getCredentials() {
+		synchronized (lock) {
+			return credentials.toArray(new Credential[credentials.size()]);
+		}
+	}
+
+	/**
+	 * Get the human-readable name of a principal.
+	 * 
+	 * @param principal
+	 *            The principal being decoded.
+	 * @return A name.
+	 */
+	protected final String getPrincipalName(X500Principal principal) {
+		return factory.x500Utils.getName(principal, CERTIFICATE_FIELD_NAMES);
+	}
+
+	/**
+	 * Cause the current state to be flushed to the database.
+	 */
+	protected final void flushToDB() {
+		factory.db.flushToDisk(run);
+	}
+
+	@Override
+	public void addCredential(Credential toAdd) {
+		synchronized (lock) {
+			int idx = credentials.indexOf(toAdd);
+			if (idx != -1)
+				credentials.set(idx, toAdd);
+			else
+				credentials.add(toAdd);
+			flushToDB();
+		}
+	}
+
+	@Override
+	public void deleteCredential(Credential toDelete) {
+		synchronized (lock) {
+			credentials.remove(toDelete);
+			flushToDB();
+		}
+	}
+
+	@Override
+	public Trust[] getTrusted() {
+		synchronized (lock) {
+			return trusted.toArray(new Trust[trusted.size()]);
+		}
+	}
+
+	@Override
+	public void addTrusted(Trust toAdd) {
+		synchronized (lock) {
+			int idx = trusted.indexOf(toAdd);
+			if (idx != -1)
+				trusted.set(idx, toAdd);
+			else
+				trusted.add(toAdd);
+			flushToDB();
+		}
+	}
+
+	@Override
+	public void deleteTrusted(Trust toDelete) {
+		synchronized (lock) {
+			trusted.remove(toDelete);
+			flushToDB();
+		}
+	}
+
+	@Override
+	public abstract void validateCredential(Credential c)
+			throws InvalidCredentialException;
+
+	@Override
+	public void validateTrusted(Trust t) throws InvalidCredentialException {
+		InputStream contentsAsStream;
+		if (t.certificateBytes != null && t.certificateBytes.length > 0) {
+			contentsAsStream = new ByteArrayInputStream(t.certificateBytes);
+			t.certificateFile = null;
+		} else if (t.certificateFile == null
+				|| t.certificateFile.trim().isEmpty())
+			throw new InvalidCredentialException(
+					"absent or empty certificateFile");
+		else {
+			contentsAsStream = contents(t.certificateFile);
+			t.certificateBytes = null;
+		}
+		t.serverName = null;
+		if (t.fileType == null || t.fileType.trim().isEmpty())
+			t.fileType = CERTIFICATE_TYPE;
+		t.fileType = t.fileType.trim();
+		try {
+			t.loadedCertificates = CertificateFactory.getInstance(t.fileType)
+					.generateCertificates(contentsAsStream);
+			t.serverName = new ArrayList<>(t.loadedCertificates.size());
+			for (Certificate c : t.loadedCertificates)
+				t.serverName.add(getPrincipalName(((X509Certificate) c)
+						.getSubjectX500Principal()));
+		} catch (CertificateException e) {
+			throw new InvalidCredentialException(e);
+		} catch (ClassCastException e) {
+			// Do nothing; truncates the list of server names
+		}
+	}
+
+	@Override
+	public void initializeSecurityFromContext(SecurityContext securityContext)
+			throws Exception {
+		// This is how to get the info from Spring Security
+		Authentication auth = securityContext.getAuthentication();
+		if (auth == null)
+			return;
+		auth.getPrincipal();
+		// do nothing else in this implementation
+	}
+
+	@Override
+	public void initializeSecurityFromSOAPContext(MessageContext context) {
+		// do nothing in this implementation
+	}
+
+	@Override
+	public void initializeSecurityFromRESTContext(HttpHeaders context) {
+		// do nothing in this implementation
+	}
+
+	private UriBuilder getUB() {
+		return factory.uriSource.getRunUriBuilder(run);
+	}
+
+	private RunDatabaseDAO getDAO() {
+		return ((RunDatabase) factory.db).dao;
+	}
+
+	@Nullable
+	private List<X509Certificate> getCerts(URI uri) throws IOException,
+			GeneralSecurityException {
+		return factory.certFetcher.getTrustsForURI(uri);
+	}
+
+	private void installLocalPasswordCredential(List<Credential> credentials,
+			List<Trust> trusts) throws InvalidCredentialException, IOException,
+			GeneralSecurityException {
+		Credential.Password pw = new Credential.Password();
+		pw.id = "run:self";
+		pw.username = PREFIX + run.id;
+		pw.password = getDAO().getSecurityToken(run.id);
+		UriBuilder ub = getUB().segment("").fragment(factory.httpRealm);
+		pw.serviceURI = ub.build();
+		validateCredential(pw);
+		log.info("issuing self-referential credential for " + pw.serviceURI);
+		credentials.add(pw);
+		List<X509Certificate> myCerts = getCerts(pw.serviceURI);
+		if (myCerts != null && myCerts.size() > 0) {
+			Trust t = new Trust();
+			t.loadedCertificates = getCerts(pw.serviceURI);
+			trusts.add(t);
+		}
+	}
+
+	/**
+	 * Builds and transfers a keystore with suitable credentials to the back-end
+	 * workflow execution engine.
+	 * 
+	 * @throws GeneralSecurityException
+	 *             If the manipulation of the keystore, keys or certificates
+	 *             fails.
+	 * @throws IOException
+	 *             If there are problems building the data (should not happen).
+	 * @throws RemoteException
+	 *             If the conveyancing fails.
+	 */
+	@Override
+	public final void conveySecurity() throws GeneralSecurityException,
+			IOException, ImplementationException {
+		RemoteSecurityContext rc = run.run.getSecurityContext();
+
+		List<Trust> trusted = new ArrayList<>(this.trusted);
+		this.trusted.clear();
+		List<Credential> credentials = new ArrayList<>(this.credentials);
+		this.credentials.clear();
+
+		try {
+			installLocalPasswordCredential(credentials, trusted);
+		} catch (Exception e) {
+			log.warn("failed to construct local credential: "
+					+ "interaction service will fail", e);
+		}
+
+		char[] password = null;
+		try {
+			password = generateNewPassword();
+
+			log.info("constructing merged keystore");
+			Truststore truststore = new Truststore(password);
+			Keystore keystore = new Keystore(password);
+			Map<URI, String> uriToAliasMap = new HashMap<>();
+			int trustedCount = 0, keyCount = 0;
+
+			synchronized (lock) {
+				try {
+					for (Trust t : trusted) {
+						if (t == null || t.loadedCertificates == null)
+							continue;
+						for (Certificate cert : t.loadedCertificates)
+							if (cert != null) {
+								truststore.addCertificate(cert);
+								trustedCount++;
+							}
+					}
+
+					this.uriToAliasMap = uriToAliasMap;
+					this.keystore = keystore;
+					for (Credential c : credentials) {
+						addCredentialToKeystore(c);
+						keyCount++;
+					}
+				} finally {
+					this.uriToAliasMap = null;
+					this.keystore = null;
+					credentials.clear();
+					trusted.clear();
+					flushToDB();
+				}
+			}
+
+			byte[] trustbytes = null, keybytes = null;
+			try {
+				trustbytes = truststore.serialize();
+				keybytes = keystore.serialize();
+
+				// Now we've built the security information, ship it off...
+
+				log.info("transfering merged truststore with " + trustedCount
+						+ " entries");
+				rc.setTruststore(trustbytes);
+
+				log.info("transfering merged keystore with " + keyCount
+						+ " entries");
+				rc.setKeystore(keybytes);
+			} finally {
+				if (trustbytes != null)
+					fill(trustbytes, (byte) 0);
+				if (keybytes != null)
+					fill(keybytes, (byte) 0);
+			}
+			rc.setPassword(password);
+
+			log.info("transferring serviceURL->alias map with "
+					+ uriToAliasMap.size() + " entries");
+			rc.setUriToAliasMap(uriToAliasMap);
+		} finally {
+			if (password != null)
+				fill(password, ' ');
+		}
+
+		synchronized (lock) {
+			conveyExtraSecuritySettings(rc);
+		}
+	}
+
+	/**
+	 * Hook that allows additional information to be conveyed to the remote run.
+	 * 
+	 * @param remoteSecurityContext
+	 *            The remote resource that information would be passed to.
+	 * @throws IOException
+	 *             If anything goes wrong with the communication.
+	 */
+	protected void conveyExtraSecuritySettings(
+			RemoteSecurityContext remoteSecurityContext) throws IOException {
+		// Does nothing by default; overrideable
+	}
+
+	/**
+	 * @return A new password with a reasonable level of randomness.
+	 */
+	protected final char[] generateNewPassword() {
+		return randomUUID().toString().toCharArray();
+	}
+
+	/**
+	 * Adds a credential to the current keystore.
+	 * 
+	 * @param alias
+	 *            The alias to create within the keystore.
+	 * @param c
+	 *            The key-pair.
+	 * @throws KeyStoreException
+	 */
+	protected final void addKeypairToKeystore(String alias, Credential c)
+			throws KeyStoreException {
+		if (c.loadedKey == null)
+			throw new KeyStoreException("critical: credential was not verified");
+		if (uriToAliasMap.containsKey(c.serviceURI))
+			log.warn("duplicate URI in alias mapping: " + c.serviceURI);
+		keystore.addKey(alias, c.loadedKey, c.loadedTrustChain);
+		uriToAliasMap.put(c.serviceURI, alias);
+	}
+
+	/**
+	 * Adds a credential to the current keystore.
+	 * 
+	 * @param c
+	 *            The credential to add.
+	 * @throws KeyStoreException
+	 */
+	public abstract void addCredentialToKeystore(Credential c)
+			throws KeyStoreException;
+
+	/**
+	 * Read a file up to {@value #FILE_SIZE_LIMIT}kB in size.
+	 * 
+	 * @param name
+	 *            The path name of the file, relative to the context run's
+	 *            working directory.
+	 * @return A stream of the file's contents.
+	 * @throws InvalidCredentialException
+	 *             If anything goes wrong.
+	 */
+	final InputStream contents(String name) throws InvalidCredentialException {
+		try {
+			File f = (File) factory.fileUtils.getDirEntry(run, name);
+			long size = f.getSize();
+			if (size > CREDENTIAL_FILE_SIZE_LIMIT * 1024)
+				throw new InvalidCredentialException(CREDENTIAL_FILE_SIZE_LIMIT
+						+ "kB limit hit");
+			return new ByteArrayInputStream(f.getContents(0, (int) size));
+		} catch (NoDirectoryEntryException | FilesystemAccessException e) {
+			throw new InvalidCredentialException(e);
+		} catch (ClassCastException e) {
+			throw new InvalidCredentialException("not a file", e);
+		}
+	}
+
+	@Override
+	public Set<String> getPermittedDestroyers() {
+		return run.getDestroyers();
+	}
+
+	@Override
+	public void setPermittedDestroyers(Set<String> destroyers) {
+		run.setDestroyers(destroyers);
+	}
+
+	@Override
+	public Set<String> getPermittedUpdaters() {
+		return run.getWriters();
+	}
+
+	@Override
+	public void setPermittedUpdaters(Set<String> updaters) {
+		run.setWriters(updaters);
+	}
+
+	@Override
+	public Set<String> getPermittedReaders() {
+		return run.getReaders();
+	}
+
+	@Override
+	public void setPermittedReaders(Set<String> readers) {
+		run.setReaders(readers);
+	}
+
+	/**
+	 * Reinstall the credentials and the trust extracted from serialization to
+	 * the database.
+	 * 
+	 * @param credentials
+	 *            The credentials to reinstall.
+	 * @param trust
+	 *            The trusted certificates to reinstall.
+	 */
+	void setCredentialsAndTrust(Credential[] credentials, Trust[] trust) {
+		synchronized (lock) {
+			this.credentials.clear();
+			if (credentials != null)
+				for (Credential c : credentials)
+					try {
+						validateCredential(c);
+						this.credentials.add(c);
+					} catch (InvalidCredentialException e) {
+						log.warn("failed to revalidate credential: " + c, e);
+					}
+			this.trusted.clear();
+			if (trust != null)
+				for (Trust t : trust)
+					try {
+						validateTrusted(t);
+						this.trusted.add(t);
+					} catch (InvalidCredentialException e) {
+						log.warn("failed to revalidate trust assertion: " + t,
+								e);
+					}
+		}
+	}
+
+	static class SecurityStore {
+		private KeyStore ks;
+		private char[] password;
+
+		SecurityStore(char[] password) throws GeneralSecurityException {
+			this.password = password.clone();
+			ks = KeyStore.getInstance("UBER", "BC");
+			try {
+				ks.load(null, this.password);
+			} catch (IOException e) {
+				throw new GeneralSecurityException(
+						"problem initializing blank truststore", e);
+			}
+		}
+
+		final synchronized void setCertificate(String alias, Certificate c)
+				throws KeyStoreException {
+			if (ks == null)
+				throw new IllegalStateException("store already written");
+			ks.setCertificateEntry(alias, c);
+		}
+
+		final synchronized void setKey(String alias, Key key, Certificate[] trustChain)
+				throws KeyStoreException {
+			if (ks == null)
+				throw new IllegalStateException("store already written");
+			ks.setKeyEntry(alias, key, password, trustChain);
+		}
+
+		final synchronized byte[] serialize(boolean logIt)
+				throws GeneralSecurityException {
+			if (ks == null)
+				throw new IllegalStateException("store already written");
+			try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
+				ks.store(stream, password);
+				if (logIt)
+					LogFactory.getLog("Taverna.Server.Worker").debug(
+							"serialized UBER/BC truststore (size: " + ks.size()
+									+ ") with password \""
+									+ new String(password) + "\"");
+				return stream.toByteArray();
+			} catch (IOException e) {
+				throw new GeneralSecurityException(
+						"problem serializing keystore", e);
+			} finally {
+				ks = null;
+				fill(password, ' ');
+			}
+		}
+
+		@Override
+		protected final void finalize() {
+			fill(password, ' ');
+			ks = null;
+		}
+	}
+
+	/**
+	 * A trust store that can only be added to or serialized. Only trusted
+	 * certificates can be placed in it.
+	 * 
+	 * @author Donal Fellows
+	 */
+	class Truststore extends SecurityStore {
+		Truststore(char[] password) throws GeneralSecurityException {
+			super(password);
+		}
+
+		/**
+		 * Add a trusted certificate to the truststore. No certificates can be
+		 * added after the truststore is serialized.
+		 * 
+		 * @param cert
+		 *            The certificate (typically belonging to a root CA) to add.
+		 * @throws KeyStoreException
+		 *             If anything goes wrong.
+		 */
+		public void addCertificate(Certificate cert) throws KeyStoreException {
+			X509Certificate c = (X509Certificate) cert;
+			String alias = format("trustedcert#%s#%s#%s",
+					getPrincipalName(c.getSubjectX500Principal()),
+					getPrincipalName(c.getIssuerX500Principal()),
+					factory.x500Utils.getSerial(c));
+			setCertificate(alias, c);
+			if (log.isDebugEnabled() && factory.logSecurityDetails)
+				log.debug("added cert with alias \"" + alias + "\" of type "
+						+ c.getClass().getCanonicalName());
+		}
+
+		/**
+		 * Get the byte serialization of this truststore. This can only be
+		 * fetched exactly once.
+		 * 
+		 * @return The serialization.
+		 * @throws GeneralSecurityException
+		 *             If anything goes wrong.
+		 */
+		public byte[] serialize() throws GeneralSecurityException {
+			return serialize(log.isDebugEnabled() && factory.logSecurityDetails);
+		}
+	}
+
+	/**
+	 * A key store that can only be added to or serialized. Only keys can be
+	 * placed in it.
+	 * 
+	 * @author Donal Fellows
+	 */
+	class Keystore extends SecurityStore {
+		Keystore(char[] password) throws GeneralSecurityException {
+			super(password);
+		}
+
+		/**
+		 * Add a key to the keystore. No keys can be added after the keystore is
+		 * serialized.
+		 * 
+		 * @param alias
+		 *            The alias of the key.
+		 * @param key
+		 *            The secret/private key to add.
+		 * @param trustChain
+		 *            The trusted certificate chain of the key. Should be
+		 *            <tt>null</tt> for secret keys.
+		 * @throws KeyStoreException
+		 *             If anything goes wrong.
+		 */
+		public void addKey(String alias, Key key, Certificate[] trustChain)
+				throws KeyStoreException {
+			setKey(alias, key, trustChain);
+			if (log.isDebugEnabled() && factory.logSecurityDetails)
+				log.debug("added key with alias \"" + alias + "\" of type "
+						+ key.getClass().getCanonicalName());
+		}
+
+		/**
+		 * Get the byte serialization of this keystore. This can only be fetched
+		 * exactly once.
+		 * 
+		 * @return The serialization.
+		 * @throws GeneralSecurityException
+		 *             If anything goes wrong.
+		 */
+		public byte[] serialize() throws GeneralSecurityException {
+			return serialize(log.isDebugEnabled() && factory.logSecurityDetails);
+		}
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextDelegateImpl.java b/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextDelegateImpl.java
new file mode 100644
index 0000000..d36d2da
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextDelegateImpl.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static java.lang.String.format;
+import static javax.xml.ws.handler.MessageContext.HTTP_REQUEST_HEADERS;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.rmi.RemoteException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Map;
+
+import javax.crypto.spec.SecretKeySpec;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.xml.ws.handler.MessageContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.taverna.server.localworker.remote.RemoteSecurityContext;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.utils.UsernamePrincipal;
+import org.taverna.server.master.utils.X500Utils;
+
+/**
+ * Factoring out of the part of the security context handling that actually
+ * deals with the different types of credentials.
+ * 
+ * @author Donal Fellows
+ */
+class SecurityContextDelegateImpl extends SecurityContextDelegate {
+	private static final char USERNAME_PASSWORD_SEPARATOR = '\u0000';
+	private static final String USERNAME_PASSWORD_KEY_ALGORITHM = "DUMMY";
+	/** What passwords are encoded as. */
+	private static final Charset UTF8 = Charset.forName("UTF-8");
+
+	private X500Utils x500Utils;
+
+	/**
+	 * Initialise the context delegate.
+	 * 
+	 * @param run
+	 *            What workflow run is this for?
+	 * @param owner
+	 *            Who owns the workflow run?
+	 * @param factory
+	 *            What class built this object?
+	 */
+	protected SecurityContextDelegateImpl(RemoteRunDelegate run,
+			UsernamePrincipal owner, SecurityContextFactory factory) {
+		super(run, owner, factory);
+		this.x500Utils = factory.x500Utils;
+	}
+
+	@Override
+	public void validateCredential(Credential c)
+			throws InvalidCredentialException {
+		try {
+			if (c instanceof Credential.Password)
+				validatePasswordCredential((Credential.Password) c);
+			else if (c instanceof Credential.KeyPair)
+				validateKeyCredential((Credential.KeyPair) c);
+			else
+				throw new InvalidCredentialException("unknown credential type");
+		} catch (InvalidCredentialException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new InvalidCredentialException(e);
+		}
+	}
+
+	@Override
+	public void addCredentialToKeystore(Credential c) throws KeyStoreException {
+		try {
+			if (c instanceof Credential.Password)
+				addUserPassToKeystore((Credential.Password) c);
+			else if (c instanceof Credential.KeyPair)
+				addKeypairToKeystore((Credential.KeyPair) c);
+			else
+				throw new KeyStoreException("unknown credential type");
+		} catch (KeyStoreException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new KeyStoreException(e);
+		}
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+
+	/**
+	 * Tests whether the given username+password credential descriptor is valid.
+	 * If it is invalid, an exception will be thrown describing what the problem
+	 * is. Validation mainly consists of listing what the username is.
+	 * 
+	 * @param passwordDescriptor
+	 *            The credential descriptor to validate.
+	 * @throws InvalidCredentialException
+	 *             If the username is empty. NB: the password may be empty!
+	 *             That's legal (if unwise).
+	 */
+	protected void validatePasswordCredential(
+			Credential.Password passwordDescriptor)
+			throws InvalidCredentialException {
+		if (passwordDescriptor.username == null
+				|| passwordDescriptor.username.trim().isEmpty())
+			throw new InvalidCredentialException("absent or empty username");
+		if (passwordDescriptor.serviceURI == null)
+			throw new InvalidCredentialException("absent service URI");
+		String keyToSave = passwordDescriptor.username
+				+ USERNAME_PASSWORD_SEPARATOR + passwordDescriptor.password;
+		passwordDescriptor.loadedKey = encodeKey(keyToSave);
+		passwordDescriptor.loadedTrustChain = null;
+	}
+
+	private static Key encodeKey(String key) {
+		return new SecretKeySpec(key.getBytes(UTF8),
+				USERNAME_PASSWORD_KEY_ALGORITHM);
+	}
+
+	/**
+	 * Adds a username/password credential pair to the current keystore.
+	 * 
+	 * @param userpassCredential
+	 *            The username and password.
+	 * @throws KeyStoreException
+	 */
+	protected void addUserPassToKeystore(Credential.Password userpassCredential)
+			throws KeyStoreException {
+		String alias = format("password#%s",
+				userpassCredential.serviceURI.toASCIIString());
+		addKeypairToKeystore(alias, userpassCredential);
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+
+	/**
+	 * Tests whether the given key-pair credential descriptor is valid. If it is
+	 * invalid, an exception will be thrown describing what the problem is.
+	 * 
+	 * @param keypairDescriptor
+	 *            The descriptor to validate.
+	 * @throws InvalidCredentialException
+	 *             If the descriptor is invalid
+	 * @throws KeyStoreException
+	 *             If we don't understand the keystore type or the contents of
+	 *             the keystore
+	 * @throws NoSuchAlgorithmException
+	 *             If the keystore is of a known type but we can't comprehend
+	 *             its security
+	 * @throws CertificateException
+	 *             If the keystore does not include enough information about the
+	 *             trust chain of the keypair
+	 * @throws UnrecoverableKeyException
+	 *             If we can't get the key out of the keystore
+	 * @throws IOException
+	 *             If we can't read the keystore for prosaic reasons (e.g., file
+	 *             absent)
+	 */
+	protected void validateKeyCredential(Credential.KeyPair keypairDescriptor)
+			throws InvalidCredentialException, KeyStoreException,
+			NoSuchAlgorithmException, CertificateException, IOException,
+			UnrecoverableKeyException {
+		if (keypairDescriptor.credentialName == null
+				|| keypairDescriptor.credentialName.trim().isEmpty())
+			throw new InvalidCredentialException(
+					"absent or empty credentialName");
+
+		InputStream contentsAsStream;
+		if (keypairDescriptor.credentialBytes != null
+				&& keypairDescriptor.credentialBytes.length > 0) {
+			contentsAsStream = new ByteArrayInputStream(
+					keypairDescriptor.credentialBytes);
+			keypairDescriptor.credentialFile = null;
+		} else if (keypairDescriptor.credentialFile == null
+				|| keypairDescriptor.credentialFile.trim().isEmpty())
+			throw new InvalidCredentialException(
+					"absent or empty credentialFile");
+		else {
+			contentsAsStream = contents(keypairDescriptor.credentialFile);
+			keypairDescriptor.credentialBytes = new byte[0];
+		}
+		if (keypairDescriptor.fileType == null
+				|| keypairDescriptor.fileType.trim().isEmpty())
+			keypairDescriptor.fileType = KeyStore.getDefaultType();
+		keypairDescriptor.fileType = keypairDescriptor.fileType.trim();
+
+		KeyStore ks = KeyStore.getInstance(keypairDescriptor.fileType);
+		char[] password = keypairDescriptor.unlockPassword.toCharArray();
+		ks.load(contentsAsStream, password);
+
+		try {
+			keypairDescriptor.loadedKey = ks.getKey(
+					keypairDescriptor.credentialName, password);
+		} catch (UnrecoverableKeyException ignored) {
+			keypairDescriptor.loadedKey = ks.getKey(
+					keypairDescriptor.credentialName, new char[0]);
+		}
+		if (keypairDescriptor.loadedKey == null)
+			throw new InvalidCredentialException(
+					"no such credential in key store");
+		keypairDescriptor.loadedTrustChain = ks
+				.getCertificateChain(keypairDescriptor.credentialName);
+		if (keypairDescriptor.loadedTrustChain == null
+				|| keypairDescriptor.loadedTrustChain.length == 0)
+			throw new InvalidCredentialException(
+					"could not establish trust chain for credential");
+	}
+
+	/**
+	 * Adds a key-pair to the current keystore.
+	 * 
+	 * @param c
+	 *            The key-pair.
+	 * @throws KeyStoreException
+	 */
+	protected void addKeypairToKeystore(Credential.KeyPair c)
+			throws KeyStoreException {
+		X509Certificate subjectCert = (X509Certificate) c.loadedTrustChain[0];
+		String alias = format("keypair#%s#%s#%s",
+				getPrincipalName(subjectCert.getSubjectX500Principal()),
+				getPrincipalName(subjectCert.getIssuerX500Principal()),
+				x500Utils.getSerial(subjectCert));
+		addKeypairToKeystore(alias, c);
+	}
+}
+
+/**
+ * Special subclass that adds support for HELIO project security tokens.
+ * 
+ * @author Donal Fellows
+ */
+class HelioSecurityContextDelegateImpl extends SecurityContextDelegateImpl {
+	/**
+	 * Initialise the context delegate.
+	 * 
+	 * @param run
+	 *            What workflow run is this for?
+	 * @param owner
+	 *            Who owns the workflow run?
+	 * @param factory
+	 *            What class built this object?
+	 */
+	protected HelioSecurityContextDelegateImpl(RemoteRunDelegate run,
+			UsernamePrincipal owner, SecurityContextFactory factory) {
+		super(run, owner, factory);
+	}
+
+	private Log log = LogFactory.getLog("Taverna.Server.Worker");
+	/** The name of the HTTP header holding the CIS token. */
+	private static final String HELIO_CIS_TOKEN = "X-Helio-CIS";
+	private transient String helioToken;
+
+	@Override
+	public void initializeSecurityFromSOAPContext(MessageContext context) {
+		// does nothing
+		@SuppressWarnings("unchecked")
+		Map<String, List<String>> headers = (Map<String, List<String>>) context
+				.get(HTTP_REQUEST_HEADERS);
+		if (factory.supportHelioToken && headers.containsKey(HELIO_CIS_TOKEN))
+			helioToken = headers.get(HELIO_CIS_TOKEN).get(0);
+	}
+
+	@Override
+	public void initializeSecurityFromRESTContext(HttpHeaders context) {
+		// does nothing
+		MultivaluedMap<String, String> headers = context.getRequestHeaders();
+		if (factory.supportHelioToken && headers.containsKey(HELIO_CIS_TOKEN))
+			helioToken = headers.get(HELIO_CIS_TOKEN).get(0);
+	}
+
+	@Override
+	protected void conveyExtraSecuritySettings(RemoteSecurityContext rc)
+			throws RemoteException {
+		try {
+			if (factory.supportHelioToken && helioToken != null) {
+				if (factory.logSecurityDetails)
+					log.info("transfering HELIO CIS token: " + helioToken);
+				rc.setHelioToken(helioToken);
+			}
+		} finally {
+			helioToken = null;
+		}
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextFactory.java b/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextFactory.java
new file mode 100644
index 0000000..cbccf34
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/SecurityContextFactory.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2011-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static java.security.Security.addProvider;
+import static java.security.Security.getProvider;
+import static java.security.Security.removeProvider;
+import static org.apache.commons.logging.LogFactory.getLog;
+import static org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
+
+import java.io.Serializable;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import org.apache.commons.logging.Log;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.springframework.beans.factory.annotation.Required;
+import org.springframework.beans.factory.annotation.Value;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+import org.taverna.server.master.utils.CertificateChainFetcher;
+import org.taverna.server.master.utils.FilenameUtils;
+import org.taverna.server.master.utils.UsernamePrincipal;
+import org.taverna.server.master.utils.X500Utils;
+
+/**
+ * Singleton factory. Really is a singleton (and is also very trivial); the
+ * singleton-ness is just about limiting the number of instances of this around
+ * even when lots of serialization is going on.
+ * 
+ * @see Serializable
+ * @author Donal Fellows
+ */
+public class SecurityContextFactory implements
+		org.taverna.server.master.interfaces.SecurityContextFactory {
+	private static final long serialVersionUID = 12345678987654321L;
+	private static SecurityContextFactory instance;
+	transient RunDBSupport db;
+	transient FilenameUtils fileUtils;
+	transient X500Utils x500Utils;
+	transient UriBuilderFactory uriSource;
+	transient CertificateChainFetcher certFetcher;
+	transient String httpRealm;
+	private transient PasswordIssuer passwordIssuer;
+	private transient BouncyCastleProvider provider;
+
+	/**
+	 * Whether to support HELIO CIS tokens.
+	 */
+	@Value("${helio.cis.enableTokenPassing}")
+	boolean supportHelioToken;
+
+	/**
+	 * Whether to log the details of security (passwords, etc).
+	 */
+	@Value("${log.security.details}")
+	boolean logSecurityDetails;
+
+	private Log log() {
+		return getLog("Taverna.Server.Worker.Security");
+	}
+
+	private void installAsInstance(SecurityContextFactory handle) {
+		instance = handle;
+	}
+
+	@PreDestroy
+	void removeAsSingleton() {
+		installAsInstance(null);
+		try {
+			if (provider != null)
+				removeProvider(provider.getName());
+		} catch (SecurityException e) {
+			log().warn(
+					"failed to remove BouncyCastle security provider; "
+							+ "might be OK if configured in environment", e);
+		}
+	}
+
+	@PostConstruct
+	void setAsSingleton() {
+		installAsInstance(this);
+		if (getProvider(PROVIDER_NAME) == null)
+			try {
+				provider = new BouncyCastleProvider();
+				if (addProvider(provider) == -1)
+					provider = null;
+			} catch (SecurityException e) {
+				log().warn(
+						"failed to install BouncyCastle security provider; "
+								+ "might be OK if already configured", e);
+				provider = null;
+			}
+	}
+
+	@Required
+	public void setRunDatabase(RunDBSupport db) {
+		this.db = db;
+	}
+
+	@Required
+	public void setCertificateFetcher(CertificateChainFetcher fetcher) {
+		this.certFetcher = fetcher;
+	}
+
+	@Required
+	public void setFilenameConverter(FilenameUtils fileUtils) {
+		this.fileUtils = fileUtils;
+	}
+
+	@Required
+	public void setX500Utils(X500Utils x500Utils) {
+		this.x500Utils = x500Utils;
+	}
+
+	@Required
+	public void setUriSource(UriBuilderFactory uriSource) {
+		this.uriSource = uriSource;
+	}
+
+	@Required
+	public void setHttpRealm(String realm) {
+		this.httpRealm = realm; //${http.realmName}
+	}
+
+	@Required
+	public void setPasswordIssuer(PasswordIssuer issuer) {
+		this.passwordIssuer = issuer;
+	}
+
+	@Override
+	public SecurityContextDelegate create(TavernaRun run,
+			UsernamePrincipal owner) throws Exception {
+		Log log = log();
+		if (log.isDebugEnabled())
+			log.debug("constructing security context delegate for " + owner);
+		RemoteRunDelegate rrd = (RemoteRunDelegate) run;
+		return new HelioSecurityContextDelegateImpl(rrd, owner, this);
+	}
+
+	private Object readResolve() {
+		if (instance == null)
+			installAsInstance(this);
+		return instance;
+	}
+
+	public String issueNewPassword() {
+		return passwordIssuer.issue();
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/SimpleFormattedCompletionNotifier.java b/server-webapp/src/main/java/org/taverna/server/master/worker/SimpleFormattedCompletionNotifier.java
new file mode 100644
index 0000000..793d291
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/SimpleFormattedCompletionNotifier.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import static org.taverna.server.master.defaults.Default.NOTIFY_MESSAGE_FORMAT;
+
+import java.text.MessageFormat;
+
+import org.springframework.beans.factory.annotation.Required;
+
+/**
+ * Completion notifier that sends messages by email.
+ * 
+ * @author Donal Fellows
+ */
+public class SimpleFormattedCompletionNotifier implements CompletionNotifier {
+	@Required
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	/**
+	 * @param subject
+	 *            The subject of the notification email.
+	 */
+	@Required
+	public void setSubject(String subject) {
+		this.subject = subject;
+	}
+
+	/**
+	 * @param messageFormat
+	 *            The template for the body of the message to send. Parameter #0
+	 *            will be substituted with the ID of the job, and parameter #1
+	 *            will be substituted with the exit code.
+	 */
+	public void setMessageFormat(String messageFormat) {
+		this.format = new MessageFormat(messageFormat);
+	}
+
+	private String name;
+	private String subject;
+	private MessageFormat format = new MessageFormat(NOTIFY_MESSAGE_FORMAT);
+
+	@Override
+	public String makeCompletionMessage(String name, RemoteRunDelegate run,
+			int code) {
+		return format.format(new Object[] { name, code });
+	}
+
+	@Override
+	public String makeMessageSubject(String name, RemoteRunDelegate run,
+			int code) {
+		return subject;
+	}
+
+	@Override
+	public String getName() {
+		return name;
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/VelocityCompletionNotifier.java b/server-webapp/src/main/java/org/taverna/server/master/worker/VelocityCompletionNotifier.java
new file mode 100644
index 0000000..cf67853
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/VelocityCompletionNotifier.java
@@ -0,0 +1,105 @@
+package org.taverna.server.master.worker;
+
+import java.io.StringWriter;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.springframework.beans.factory.annotation.Required;
+import org.taverna.server.master.common.version.Version;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.UriBuilderFactory;
+
+public class VelocityCompletionNotifier implements CompletionNotifier {
+	private String subject;
+	private VelocityEngine engine;
+	private Template template;
+	private String name;
+	private String templateName;
+	private UriBuilderFactory ubf;
+
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * @param subject
+	 *            The subject of the notification email.
+	 */
+	@Required
+	public void setSubject(String subject) {
+		this.subject = subject;
+	}
+
+	/**
+	 * @param engine
+	 *            The configured Apache Velocity engine.
+	 */
+	@Required
+	public void setVelocityEngine(VelocityEngine engine) {
+		this.engine = engine;
+	}
+
+	/**
+	 * @param uriBuilderFactory
+	 *            The configured URI builder factory.
+	 */
+	@Required
+	public void setUriBuilderFactory(UriBuilderFactory uriBuilderFactory) {
+		this.ubf = uriBuilderFactory;
+	}
+
+	/**
+	 * @param name
+	 *            The name of the template.
+	 */
+	@Required
+	public void setName(String name) {
+		this.name = name;
+		this.templateName = getClass().getName() + "_" + name + ".vtmpl";
+	}
+
+	private Template getTemplate() {
+		if (template == null)
+			synchronized(this) {
+				if (template == null)
+					template = engine.getTemplate(templateName);
+			}
+		return template;
+	}
+
+	@Override
+	public String makeCompletionMessage(String name, RemoteRunDelegate run,
+			int code) {
+		VelocityContext ctxt = new VelocityContext();
+		ctxt.put("id", name);
+		ctxt.put("uriBuilder", ubf.getRunUriBuilder(run));
+		ctxt.put("name", run.getName());
+		ctxt.put("creationTime", run.getCreationTimestamp());
+		ctxt.put("startTime", run.getStartTimestamp());
+		ctxt.put("finishTime", run.getFinishTimestamp());
+		ctxt.put("expiryTime", run.getExpiry());
+		ctxt.put("serverVersion", Version.JAVA);
+		for (Listener l : run.getListeners())
+			if (l.getName().equals("io")) {
+				for (String p : l.listProperties())
+					try {
+						ctxt.put("prop_" + p, l.getProperty(p));
+					} catch (NoListenerException e) {
+						// Ignore...
+					}
+				break;
+			}
+		StringWriter sw = new StringWriter();
+		getTemplate().merge(ctxt, sw);
+		return sw.toString();
+	}
+
+	@Override
+	public String makeMessageSubject(String name, RemoteRunDelegate run,
+			int code) {
+		return subject;
+	}
+}
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/WorkerModel.java b/server-webapp/src/main/java/org/taverna/server/master/worker/WorkerModel.java
new file mode 100644
index 0000000..1abe617
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/WorkerModel.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2010-2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.worker;
+
+import java.net.URI;
+import java.util.List;
+
+import org.taverna.server.master.common.Status;
+
+/**
+ * Profile of the getters and setters in a worker system. Ensures that the
+ * persisted state matches the public view on the state model at least fairly
+ * closely.
+ * 
+ * @author Donal Fellows
+ */
+public interface WorkerModel extends PolicyLimits {
+
+	/**
+	 * @param defaultLifetime
+	 *            how long a workflow run should live by default, in minutes.
+	 */
+	public abstract void setDefaultLifetime(int defaultLifetime);
+
+	/**
+	 * @return how long a workflow run should live by default, in minutes.
+	 */
+	public abstract int getDefaultLifetime();
+
+	/**
+	 * @param maxRuns
+	 *            the maximum number of extant workflow runs
+	 */
+	public abstract void setMaxRuns(int maxRuns);
+
+	/**
+	 * @param factoryProcessNamePrefix
+	 *            the prefix used for factory processes in RMI
+	 */
+	public abstract void setFactoryProcessNamePrefix(
+			String factoryProcessNamePrefix);
+
+	/**
+	 * @return the prefix used for factory processes in RMI
+	 */
+	public abstract String getFactoryProcessNamePrefix();
+
+	/**
+	 * @param executeWorkflowScript
+	 *            the script to run to actually run a workflow
+	 */
+	public abstract void setExecuteWorkflowScript(String executeWorkflowScript);
+
+	/**
+	 * @return the script to run to actually run a workflow
+	 */
+	public abstract String getExecuteWorkflowScript();
+
+	/**
+	 * @param extraArgs
+	 *            the extra arguments to pass into the workflow runner
+	 */
+	public abstract void setExtraArgs(String[] extraArgs);
+
+	/**
+	 * @return the extra arguments to pass into the workflow runner
+	 */
+	public abstract String[] getExtraArgs();
+
+	/**
+	 * @param waitSeconds
+	 *            the number of seconds to wait for subprocesses to start
+	 */
+	public abstract void setWaitSeconds(int waitSeconds);
+
+	/**
+	 * @return the number of seconds to wait for subprocesses to start
+	 */
+	public abstract int getWaitSeconds();
+
+	/**
+	 * @param sleepMS
+	 *            milliseconds to wait between polling for a started
+	 *            subprocess's status
+	 */
+	public abstract void setSleepMS(int sleepMS);
+
+	/**
+	 * @return milliseconds to wait between polling for a started subprocess's
+	 *         status
+	 */
+	public abstract int getSleepMS();
+
+	/**
+	 * @param serverWorkerJar
+	 *            the full path name of the file system access worker
+	 *            subprocess's implementation JAR
+	 */
+	public abstract void setServerWorkerJar(String serverWorkerJar);
+
+	/**
+	 * @return the full path name of the file system access worker subprocess's
+	 *         implementation JAR
+	 */
+	public abstract String getServerWorkerJar();
+
+	/**
+	 * @param javaBinary
+	 *            the full path name to the Java binary to use
+	 */
+	public abstract void setJavaBinary(String javaBinary);
+
+	/**
+	 * @return the full path name to the Java binary to use
+	 */
+	public abstract String getJavaBinary();
+
+	/**
+	 * @param registryPort
+	 *            what port is the RMI registry on
+	 */
+	public abstract void setRegistryPort(int registryPort);
+
+	/**
+	 * @return what port is the RMI registry on
+	 */
+	public abstract int getRegistryPort();
+
+	/**
+	 * @param registryHost
+	 *            what host (network interface) is the RMI registry on
+	 */
+	public abstract void setRegistryHost(String registryHost);
+
+	/**
+	 * @return what host (network interface) is the RMI registry on
+	 */
+	public abstract String getRegistryHost();
+
+	/**
+	 * @param serverForkerJar
+	 *            the full path name of the impersonation engine's
+	 *            implementation JAR
+	 */
+	public abstract void setServerForkerJar(String serverForkerJar);
+
+	/**
+	 * @return the full path name of the impersonation engine's implementation
+	 *         JAR
+	 */
+	public abstract String getServerForkerJar();
+
+	/**
+	 * @param passwordFile
+	 *            the full path name of a file containing a password to use with
+	 *            sudo (or empty for none)
+	 */
+	public abstract void setPasswordFile(String passwordFile);
+
+	/**
+	 * @return the full path name of a file containing a password to use with
+	 *         sudo (or empty for none)
+	 */
+	public abstract String getPasswordFile();
+
+	/**
+	 * @param operatingLimit
+	 *            the maximum number of runs in the
+	 *            {@linkplain Status#Operating operating} state at once
+	 */
+	public abstract void setOperatingLimit(int operatingLimit);
+
+	@Override
+	void setPermittedWorkflowURIs(List<URI> permittedWorkflows);
+
+	/**
+	 * @return the full path name of the RMI registry subprocess's
+	 *         implementation JAR
+	 */
+	String getRegistryJar();
+
+	/**
+	 * @param rmiRegistryJar
+	 *            the full path name of the RMI registry subprocess's
+	 *            implementation JAR
+	 */
+	void setRegistryJar(String rmiRegistryJar);
+
+	/**
+	 * @return whether a run should generate provenance information by default
+	 */
+	boolean getGenerateProvenance();
+
+	/**
+	 * @param generateProvenance
+	 *            whether a run should generate provenance information by
+	 *            default
+	 */
+	void setGenerateProvenance(boolean generateProvenance);
+}
\ No newline at end of file
diff --git a/server-webapp/src/main/java/org/taverna/server/master/worker/package-info.java b/server-webapp/src/main/java/org/taverna/server/master/worker/package-info.java
new file mode 100644
index 0000000..6007f88
--- /dev/null
+++ b/server-webapp/src/main/java/org/taverna/server/master/worker/package-info.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+/**
+ * A Taverna Server back-end that works by forking off workflow executors.
+ */
+package org.taverna.server.master.worker;
+
diff --git a/server-webapp/src/main/replacementscripts/executeworkflow.bat b/server-webapp/src/main/replacementscripts/executeworkflow.bat
new file mode 100644
index 0000000..c678855
--- /dev/null
+++ b/server-webapp/src/main/replacementscripts/executeworkflow.bat
@@ -0,0 +1,25 @@
+@ECHO OFF
+
+REM Taverna startup script
+
+REM distribution directory
+set TAVERNA_HOME=%~dp0
+
+REM 300 MB memory, 140 MB for classes
+set ARGS=-Xmx300m -XX:MaxPermSize=140m
+
+REM Taverna system properties
+set ARGS=%ARGS% "-Draven.profile=file:%TAVERNA_HOME%conf/current-profile.xml"
+set ARGS=%ARGS% -Djava.system.class.loader=net.sf.taverna.raven.prelauncher.BootstrapClassLoader 
+set ARGS=%ARGS% -Draven.launcher.app.main=net.sf.taverna.t2.commandline.CommandLineLauncher
+set ARGS=%ARGS% -Draven.launcher.show_splashscreen=false
+set ARGS=%ARGS% -Djava.awt.headless=true
+set ARGS=%ARGS% "-Dtaverna.startup=%TAVERNA_HOME%."
+IF NOT x%RAVEN_APPHOME%==x SET ARGS=%ARGS% "-Draven.launcher.app.home=%RAVEN_APPHOME%"
+IF NOT x%TAVERNA_RUN_ID%==x SET ARGS=%ARGS% "-Dtaverna.runid=%TAVERNA_RUN_ID%"
+IF NOT x%INTERACTION_HOST%==x SET ARGS=%ARGS% "-Dtaverna.interaction.host=%INTERACTION_HOST%"
+IF NOT x%INTERACTION_PORT%==x SET ARGS=%ARGS% "-Dtaverna.interaction.port=%INTERACTION_PORT%"
+IF NOT x%INTERACTION_WEBDAV%==x SET ARGS=%ARGS% "-Dtaverna.interaction.webdav_path=%INTERACTION_WEBDAV%"
+IF NOT x%INTERACTION_FEED%==x SET ARGS=%ARGS% "-Dtaverna.interaction.feed_path=%INTERACTION_FEED%"
+
+java %ARGS% -jar "%TAVERNA_HOME%lib\prelauncher-2.3.jar" %*
diff --git a/server-webapp/src/main/replacementscripts/executeworkflow.sh b/server-webapp/src/main/replacementscripts/executeworkflow.sh
new file mode 100644
index 0000000..e9e1d36
--- /dev/null
+++ b/server-webapp/src/main/replacementscripts/executeworkflow.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+
+set -e
+
+# 300 MB memory, 140 MB for classes
+memlimit=-Xmx300m
+permsize=-XX:MaxPermSize=140m
+
+## Parse the command line to extract the pieces to move around to before or
+## after the JAR filename...
+pre=-Djava.awt.headless=true 
+post=
+for arg
+do
+    case $arg in
+	-JXmx*) memlimit=`echo $arg | sed 's/-JX/-X/'` ;;
+	-JXX:MaxPermSize=*) permsize=`echo $arg | sed 's/-JXX/-XX/'` ;;
+	-J*) pre="$pre `echo $arg | sed 's/-J/-/'`" ;;
+	-D*) pre="$pre $arg" ;;
+	*) post="$post \"$arg\"" ;;
+    esac
+done
+if test "xx" = "x${post}x"; then
+    echo "Missing arguments! Bug in argument processing?" >&2
+    exit 1
+fi
+eval set x $post
+shift
+
+## resolve links - $0 may be a symlink
+prog="$0"
+
+real_path() {
+    readlink -m "$1" 2>/dev/null || python -c 'import os,sys;print os.path.realpath(sys.argv[1])' "$1"
+}
+
+realprog=`real_path "$prog"`
+taverna_home=`dirname "$realprog"`
+javabin=java
+if test -x "$JAVA_HOME/bin/java"; then
+    javabin="$JAVA_HOME/bin/java"
+fi
+APPHOME_PROP= 
+if test x != "x$TAVERNA_APPHOME"; then
+    APPHOME_PROP="-Dtaverna.app.home=$TAVERNA_APPHOME"
+fi
+RUNID_PROP= 
+if test x != "x$TAVERNA_RUN_ID"; then
+    RUNID_PROP="-Dtaverna.runid=$TAVERNA_RUN_ID"
+fi
+INTERACTION_PROPS=-Dtaverna.interaction.ignore_requests=true
+if test x != "x$INTERACTION_HOST"; then
+    INTERACTION_PROPS="$INTERACTION_PROPS -Dtaverna.interaction.host=$INTERACTION_HOST"
+    INTERACTION_PROPS="$INTERACTION_PROPS -Dtaverna.interaction.port=$INTERACTION_PORT"
+    INTERACTION_PROPS="$INTERACTION_PROPS -Dtaverna.interaction.webdav_path=$INTERACTION_WEBDAV"
+    INTERACTION_PROPS="$INTERACTION_PROPS -Dtaverna.interaction.feed_path=$INTERACTION_FEED"
+    if test x != "x$INTERACTION_PUBLISH"; then
+    	INTERACTION_PROPS="$INTERACTION_PROPS -Dtaverna.interaction.publishAddressOverride=$INTERACTION_PUBLISH"
+    fi
+fi
+
+MainClass=net.sf.taverna.t2.commandline.CommandLineLauncher
+
+echo "pid:$$"
+exec "$javabin" $memlimit $permsize \
+  "-Dlog4j.configuration=file://$taverna_home/conf/log4j.properties " \
+  "-Djava.util.logging.config.file=$taverna_home/conf/logging.properties " \
+  "-Dtaverna.app.startup=$taverna_home" -Dtaverna.interaction.ignore_requests=true \
+  $APPHOME_PROP $RUNID_PROP $INTERACTION_PROPS -Djava.awt.headless=true \
+  -Dcom.sun.net.ssl.enableECC=false -Djsse.enableSNIExtension=false $pre \
+  -jar "$taverna_home/lib/taverna-command-line-0.1.1.jar" \
+  ${1+"$@"}
diff --git a/server-webapp/src/main/resources/admin.html b/server-webapp/src/main/resources/admin.html
new file mode 100644
index 0000000..a80a783
--- /dev/null
+++ b/server-webapp/src/main/resources/admin.html
@@ -0,0 +1,240 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Taverna Server ${project.version} Administration Interface</title>
+<link id="admin" href="admin" />
+<script type="text/javascript" src="admin/static/jquery-1.8.0.min.js"></script>
+<script type="text/javascript" src="admin/static/jquery-ui-1.8.23.custom.min.js"></script>
+<script type="text/javascript" src="admin/static/admin.js"></script>
+<link href="admin/static/jquery-ui-1.8.23.custom.css" rel="stylesheet" type="text/css" />
+
+</head>
+<body>
+<img height="70" style="float:left" src="admin/static/t2cogs.png">
+<h1>Taverna Server ${project.version} Administration Interface</h1>
+<br clear="left"/>
+<div id="body">
+<ul>
+  <li><a href="#t-global">Global Settings</a></li>
+  <li><a href="#t-users">Users</a></li>
+  <li><a href="#t-workflows">Workflows</a></li>
+  <li><a href="#t-usage">Usage Records</a></li>
+  <li><a href="#t-worker">Local Worker Configuration</a></li>
+</ul>
+  
+<div id="t-global">
+<label title="The number of invocations of the main interface webapp that have been done. Be aware that one service call can result in many invocations due to resource resolution." for="invokationCount">Invocation Count:</label>
+<span title="The number of invocations of the main interface webapp that have been done. Be aware that one service call can result in many invocations due to resource resolution." id="invokationCount">0</span>
+<br>
+<label title="The number of runs that currently exist." for="runCount">Run Count:</label>
+<span title="The number of runs that currently exist." id="runCount">0</span>
+<br>
+<label title="The number of runs that are currently operating." for="operatingCount">Operating Run Count:</label>
+<span title="The number of runs that are currently operating." id="operatingCount">0</span>
+<br>
+<label title="Whether workflow runs should create provenance traces by default. Users can explicitly override this." for="generateProvenance">Generate Provenance by Default</label>
+<input type="checkbox" id="generateProvenance" />
+<br>
+<label title="The time it took for the back-end engine to start up, in seconds. Should usually be short." for="startupTime">Back-End Startup Time (seconds):</label>
+<span title="The time it took for the back-end engine to start up, in seconds. Should usually be short." id="startupTime">0</span>
+<br>
+<label title="The exit code from the last time the back-end was shut down. Blank if the back end has never been shut down while the current webapp instance is running (i.e., since the last boot of the container)." for="lastExitCode">Back-End Last Exit Code:</label>
+<span title="The exit code from the last time the back-end was shut down. Blank if the back end has never been shut down while the current webapp instance is running (i.e., since the last boot of the container)." id="lastExitCode"></span>
+<p>
+<label title="Whether new workflow runs should be created. Disabling this does not prevent existing runs from executing." for="allowNew">Allow New Runs</label>
+<input type="checkbox" id="allowNew" />
+<label title="Whether to record the workflows being run by users. Very noisy due to length of workflow documents, occasionally useful." for="logWorkflows">Log Executed Workflows</label>
+<input type="checkbox" id="logWorkflows" />
+<label title="Whether to record exceptions generated by users in the code (as well as converting them to faults and error responses). Useful for debugging, but noisy." for="logFaults">Log User Exceptions</label>
+<input type="checkbox" id="logFaults" />
+<p>
+<label title="The maximum number of workflow runs that can exist at once, in any state." for="runLimit">Maximum Simultaneous Existing Workflow Runs</label>
+<input title="The maximum number of workflow runs that can exist at once, in any state." id="runLimit" size="3" />
+<br>
+<label title="The maximum number of workflow runs that can be executing at once." for="operatingLimit">Maximum Simultaneous Executing Workflow Runs</label>
+<input title="The maximum number of workflow runs that can be executing at once." id="operatingLimit" size="3" />
+<br>
+<label title="How long to allow a workflow to execute for by default (clients can change this), in minutes." for="defaultLifetime">Default Run Lifetime (minutes)</label>
+<input title="How long to allow a workflow to execute for by default (clients can change this), in minutes." id="defaultLifetime" size="7" />
+</div><!-- t-global -->
+
+<div id="t-users">
+<table id="userList">
+  <tr><th>Username<th>System Username</tr>
+</table>
+<h3>Add a user</h3>
+<table border=1>
+  <tr>
+    <td><label title="The user name to create." for="newUsername">Username</label>
+    <td><input title="The user name to create." size=12 id="newUsername" />
+  </tr>
+  <tr>
+    <td><label title="The password to use for the user." for="newPassword">Password</label>
+    <td><input title="The password to use for the user." size=12 id="newPassword" type="password"/>
+  </tr>
+  <tr>
+    <td><label title="The system account to run the user's workflows in; leave blank for the default." for="newSysID">System ID</label>
+    <td><input title="The system account to run the user's workflows in; leave blank for the default." size=12 id="newSysID" />
+  </tr>
+  <tr><td colspan=2>
+    <label title="Whether to allow this user to log in at all." for="newEnabled">Enabled</label>
+    <input type="checkbox" id="newEnabled" />
+    <label title="Whether the user has administrative privileges (can see all workflow runs, can access the administration page)." for="newAdmin">Admin</label>
+    <input type="checkbox" id="newAdmin" />
+  </td></tr>
+  <tr><td colspan=2>
+    <button id="makeNewUser">Create a new user</button>
+  </td></tr>
+</table>
+</div><!-- t-users -->
+
+<div id="t-workflows">
+<label title="Workflow URIs to limit execution to." for="workflows">Workflow URIs (one per line)</label>
+<br>
+<textarea title="Workflow URIs to limit execution to." rows="5" cols="60" id="workflows"></textarea>
+<p>
+<button id="saveWorkflows">Save</button> <button id="refreshWorkflows">Refresh</button> <button id="emptyWorkflows">Empty URIs list</button>
+</div>
+
+<div id="t-usage">
+Download <a href="#" id="ur">usage records</a> (warning: may be slow!)
+<p>
+<label title="The name of a file to write usage records to. Note that this file will end up containing many XML documents concatenated together; it is up to you to split them up as necessary. Each record is only written as it is generated; this does not produce historic data." for="usageRecordDumpFile">Usage Record Dump File</label>
+<input title="The name of a file to write usage records to. Note that this file will end up containing many XML documents concatenated together; it is up to you to split them up as necessary. Each record is only written as it is generated; this does not produce historic data." id="usageRecordDumpFile" size="50" />
+</div><!-- t-usage -->
+
+<div id="t-worker">
+  <div id="a-worker">
+
+    <h3><a href="#">Subprocess Implementation Control</a></h3>
+    <div>
+      <table>
+	<tr>
+	  <td> <label title="The full path of the Java executable to use. Normally set correct by default." for="javaBinary">Java Executable (for subprocesses):</label> </td>
+	  <td> <input title="The full path of the Java executable to use. Normally set correct by default." id="javaBinary" size="80" /> </td>
+	</tr>
+	<tr>
+	  <td> <label title="The full path of the secure subprocess fork engine to use. Normally set correct by default." for="serverForkerJar">Subprocess Factory JAR:</label> </td>
+	  <td> <input title="The full path of the secure subprocess fork engine to use. Normally set correct by default." id="serverForkerJar" size="80" /> </td>
+	</tr>
+	<tr>
+	  <td> <label title="The full path of a file containing the credentials to use with sudo. Leave blank to use a password-less connection (see documentation for how to configure)." for="runasPasswordFile">File with password for sudo:</label> </td>
+	  <td> <input title="The full path of a file containing the credentials to use with sudo. Leave blank to use a password-less connection (see documentation for how to configure)." id="runasPasswordFile" size="80" /> </td>
+	</tr>
+	<tr>
+	  <td> <label title="The full path of the user filesystem access and workflow initiation engine to use. Normally set correct by default." for="serverWorkerJar">User Filesystem Access JAR:</label> </td>
+	  <td> <input title="The full path of the user filesystem access and workflow initiation engine to use. Normally set correct by default." id="serverWorkerJar" size="80" /> </td>
+	</tr>
+	<tr>
+	  <td> <label title="The full path of the workflow engine executable. Normally set correctly by default." for="executeWorkflowScript">Workflow Engine Executable:</label> </td>
+	  <td> <input title="The full path of the workflow engine executable. Normally set correctly by default." id="executeWorkflowScript" size="80" /> </td>
+	</tr>
+      </table>
+    </div>
+
+    <h3><a href="#">Worker Registration Control</a></h3>
+    <div>
+      <label title="The machine hosting the RMI registry. WARNING: changing this will probably break your configuration! Contact the myGrid team for help before adjusting!" for="registryHost">Registry Host</label>
+      <input title="The machine hosting the RMI registry. WARNING: changing this will probably break your configuration! Contact the myGrid team for help before adjusting!" id="registryHost" size="20" />
+      <label title="The port number the RMI registry. WARNING: changing this will probably break your configuration! Contact the myGrid team for help before adjusting!" for="registryPort">Port</label>
+      <input title="The port number the RMI registry. WARNING: changing this will probably break your configuration! Contact the myGrid team for help before adjusting!" id="registryPort" size="5" />
+      <br>
+      <label title="The full path of the RMI registry implementation JAR file. WARNING: changing this will probably break your configuration! Contact the myGrid team for help before adjusting!" for="registryJar">RMI Registry JAR</label>
+      <input title="The full path of the RMI registry implementation JAR file. WARNING: changing this will probably break your configuration! Contact the myGrid team for help before adjusting!" id="registryJar" size="80" />
+      <br>
+      <label title="The time to wait (in seconds) for the back-end processes to boot and register themselves with the RMI registry. Busy machines may need a longer value here." for="registrationWaitSeconds">Time to wait for registration (seconds)</label>
+      <input title="The time to wait (in seconds) for the back-end processes to boot and register themselves with the RMI registry. Busy machines may need a longer value here." id="registrationWaitSeconds" size="5" />
+      <br>
+      <label title="How long to wait (in milliseconds) between probes to the registry to detect the registration of a back-end process." for="registrationPollMillis">Time to wait between polling to
+      detect registration (milliseconds)</label>
+      <input title="How long to wait (in milliseconds) between probes to the registry to detect the registration of a back-end process." id="registrationPollMillis" size="5" />
+    </div>
+
+    <h3><a href="#">System User/Factory ID Mapping</a></h3>
+    <div>
+      <table title="The mapping of system user IDs to factory identifiers (used in the RMI registry). Note that this is read-only." id="factoryProcessMapping" border="1">
+      </table>
+    </div>
+
+   	<h3><a href="#">Extra Workflow Engine Configuration</a></h3>
+   	<div>
+   		<h4>System Properties</h4>
+   		<table id="extraArguments-prop">
+   		<tr><td></td><td><button title="Add a system property to pass to the back-end engine." id="extra-prop-add">Add System Property</button></td></tr>
+   		</table>
+   		<h4>Environment Variables</h4>
+   		<table id="extraArguments-env">
+   		<tr><td></td><td><button title="Add an environment variable to pass to the back-end engine." id="extra-env-add">Add Environment Variable</button></td></tr>
+   		</table>
+   		<h4>Java Runtime Configuration</h4>
+   		<table id="extraArguments-runtime">
+   		<tr><td></td><td><button title="Add a Java runtime parameter (e.g., Xmx=400m to set the memory usage limit to 400MB) to pass to the back-end engine. Note the lack of a leading '-' character!" id="extra-run-add">Add Runtime Configuration</button></td></tr>
+   		</table>
+   	</div>
+
+  </div><!-- a-worker -->
+</div><!-- t-worker -->
+
+</div>
+
+<hr>
+<address>Donal Fellows / University of Manchester</address>
+
+<!-- DIALOG BOXES -->
+<div id="dialog-confirm" title="Delete user?" style="display: none">
+  <p>
+  <span class="ui-icon ui-icon-alert" style="float:left; margin:0 7px 20px 0;"></span>
+  This user will be permanently deleted from the system. Are you sure?
+  </p>
+</div>
+
+<div id="dialog-password" title="Change password?" style="display: none">
+  <p>
+  <span class="ui-icon ui-icon-alert" style="float:left; margin:0 7px 20px 0;"></span>
+  This will permanently change the user's password. Make sure you wish
+  to do this.
+  </p>
+  <p>
+  <input title="New password" id="change-password" type="password" size="12" />
+  <br>
+  Please repeat it to be sure...
+  <br>
+  <input title="New password (again)" id="change-password2" type="password" size="12" />
+  </p>
+</div>
+
+<div id="dialog-environment" title="Set environment variable?" style="display: none">
+  <p>
+  <span class="ui-icon ui-icon-alert" style="float:left; margin:0 7px 20px 0;"></span>
+  Set an environment variable to be passed to the workflow engine.
+  </p>
+  <p>
+  <input title="Environment variable name" id="env-key" size="15" /> =
+  <input title="Environment variable value" id="env-value" size="20" />
+  </p>
+</div>
+
+<div id="dialog-runtime" title="Set runtime configuration?" style="display: none">
+  <p>
+  <span class="ui-icon ui-icon-alert" style="float:left; margin:0 7px 20px 0;"></span>
+  Set a runtime parameter (e.g., -Xmx400m for a 400MB memory limit) for the Java runtime.
+  </p>
+  <p>
+  <input title="Java runtime configuration parameter" id="runtime-value" size="20" />
+  </p>
+</div>
+
+<div id="dialog-property" title="Set runtime property?" style="display: none">
+  <p>
+  <span class="ui-icon ui-icon-alert" style="float:left; margin:0 7px 20px 0;"></span>
+  Set a configuration property for the Java runtime.
+  </p>
+  <p>
+  <input title="System property name" id="prop-key" size="15" /> =
+  <input title="System property value" id="prop-value" size="20" />
+  </p>
+</div>
+
+</body>
+</html>
diff --git a/server-webapp/src/main/resources/capabilities.properties b/server-webapp/src/main/resources/capabilities.properties
new file mode 100644
index 0000000..2b4844f
--- /dev/null
+++ b/server-webapp/src/main/resources/capabilities.properties
@@ -0,0 +1,38 @@
+# This is currently a hand-curated list. This sucks! 
+
+######## --- PLATFORM --- ########
+http\://ns.taverna.org.uk/2013/software/taverna = 2.5
+
+######## --- OUTPUTS/PROVENANCE --- ########
+http\://ns.taverna.org.uk/2013/bundle/run = 1.0
+http\://ns.taverna.org.uk/2013/provenance/prov = 1.0
+
+######## --- ACTIVITIES --- ########
+http\://ns.taverna.org.uk/2010/activity/nested-workflow = 1.5
+http\://ns.taverna.org.uk/2010/activity/apiconsumer = 1.5
+http\://ns.taverna.org.uk/2010/activity/beanshell = 1.5
+http\://ns.taverna.org.uk/2010/activity/localworker = 1.5
+http\://ns.taverna.org.uk/2010/activity/biomart = 1.5
+http\://ns.taverna.org.uk/2010/activity/biomoby/object = 1.5
+http\://ns.taverna.org.uk/2010/activity/biomoby/service = 1.5
+http\://ns.taverna.org.uk/2010/activity/rshell = 1.5
+http\://ns.taverna.org.uk/2010/activity/soaplab = 1.5
+http\://ns.taverna.org.uk/2010/activity/spreadsheet-import = 1.5
+http\://ns.taverna.org.uk/2010/activity/constant = 1.5
+http\://ns.taverna.org.uk/2010/activity/component = 1.5
+http\://ns.taverna.org.uk/2010/activity/wsdl = 1.5
+http\://ns.taverna.org.uk/2010/activity/wsdl/xml-splitter/in = 1.5
+http\://ns.taverna.org.uk/2010/activity/wsdl/xml-splitter/out = 1.5
+http\://ns.taverna.org.uk/2010/activity/tool = 1.5
+http\://ns.taverna.org.uk/2010/activity/rest = 1.5
+http\://ns.taverna.org.uk/2010/activity/xpath = 1.5
+http\://ns.taverna.org.uk/2010/activity/webdav = 1.5
+http\://ns.taverna.org.uk/2010/activity/interaction = 1.5
+
+######## --- DISPATCH LAYERS --- ########
+http\://ns.taverna.org.uk/2010/scufl2/taverna/dispatchlayer/ErrorBounce = 1.5
+http\://ns.taverna.org.uk/2010/scufl2/taverna/dispatchlayer/Failover = 1.5
+http\://ns.taverna.org.uk/2010/scufl2/taverna/dispatchlayer/Invoke = 1.5
+http\://ns.taverna.org.uk/2010/scufl2/taverna/dispatchlayer/Loop = 1.5
+http\://ns.taverna.org.uk/2010/scufl2/taverna/dispatchlayer/Parallelize = 1.5
+http\://ns.taverna.org.uk/2010/scufl2/taverna/dispatchlayer/Retry = 1.5
\ No newline at end of file
diff --git a/server-webapp/src/main/resources/log4j.properties b/server-webapp/src/main/resources/log4j.properties
new file mode 100644
index 0000000..ea0ea12
--- /dev/null
+++ b/server-webapp/src/main/resources/log4j.properties
@@ -0,0 +1,39 @@
+log4j.rootLogger=info, R
+log4j.category.DataNucleus.Query=warn
+#log4j.category.DataNucleus.Datastore.Schema=debug
+#log4j.logger.org.springframework.security=DEBUG
+#log4j.category.Taverna=debug
+
+log4j.appender.R=org.apache.log4j.RollingFileAppender
+log4j.appender.R.File=${catalina.home}/logs/tavserv.out
+log4j.appender.R.MaxFileSize=10MB
+log4j.appender.R.MaxBackupIndex=30
+log4j.appender.R.layout=org.apache.log4j.PatternLayout
+log4j.appender.R.layout.ConversionPattern=%d{yyyyMMdd'T'HHmmss.SSS} %-5p %c{1} %C{1} - %m%n
+
+#log4j.category.Taverna=INFO, A1
+#log4j.category.Taverna.Server.LocalWorker.RunDB=INFO
+#log4j.category.Taverna.Server.Webapp=INFO
+#log4j.category.Taverna.Server.LocalWorker.Policy=INFO
+#log4j.category.Taverna.Server.LocalWorker.Security=INFO
+## Swallow Derby's messages
+#log4j.category.Derby=WARN, B2
+## Will you _shut up_, DataNucleus! <hits with rolled-up newspaper>
+#log4j.category.DataNucleus=WARN, B2
+##log4j.category.DataNucleus.SchemaTool=DEBUG
+##log4j.category.DataNucleus.Datastore.Schema=DEBUG
+##log4j.category.DataNucleus.Datastore.Native=DEBUG
+##log4j.logger.org.springframework.security=DEBUG, B2
+#log4j.category.org.springframework=INFO, B2
+#log4j.category.org.apache.cxf=INFO, B2
+#log4j.category.org.apache.cxf.jaxrs.utils.JAXRSUtils=INFO
+#log4j.category.eu.medsea=INFO, B2
+#log4j.category.org.apache.axiom=INFO, B2
+## Appender for Taverna Server components
+#log4j.appender.A1=org.apache.log4j.ConsoleAppender
+#log4j.appender.A1.layout=org.apache.log4j.PatternLayout
+#log4j.appender.A1.layout.ConversionPattern=%d{yyyyMMdd'T'HHmmss.SSS} %-5p %c{1} %C{1} - %m%n
+## Appender for Framework components
+#log4j.appender.B2=org.apache.log4j.ConsoleAppender
+#log4j.appender.B2.layout=org.apache.log4j.PatternLayout
+#log4j.appender.B2.layout.ConversionPattern=%d{yyyyMMdd'T'HHmmss.SSS} %-5p %c{1} - %m%n
diff --git a/server-webapp/src/main/resources/security.policy b/server-webapp/src/main/resources/security.policy
new file mode 100644
index 0000000..1ec4166
--- /dev/null
+++ b/server-webapp/src/main/resources/security.policy
@@ -0,0 +1,3 @@
+grant {
+   permission java.security.AllPermission "*:*";
+};
diff --git a/server-webapp/src/main/resources/static/admin.js b/server-webapp/src/main/resources/static/admin.js
new file mode 100644
index 0000000..c2c41bf
--- /dev/null
+++ b/server-webapp/src/main/resources/static/admin.js
@@ -0,0 +1,549 @@
+// Wrappers round AJAX calls to simplify *most* of this stuff
+
+/** How to retrieve text asynchronously. */
+function getText(u, done) {
+	$.ajax({
+		type : "GET",
+		url : u,
+		async : true,
+		cache : false,
+		accept : "text/plain",
+		dataType : "text",
+		success : done
+	});
+}
+/** How to retrieve JSON asynchronously. */
+function getJSON(u, done) {
+	$.ajax({
+		type : "GET",
+		url : u,
+		async : true,
+		cache : false,
+		dataType : "json",
+		accept : "application/json",
+		success : done
+	});
+}
+/** How to send a PUT of text asynchronously. */
+function putText(u, val, done) {
+	$.ajax({
+		type : "PUT",
+		url : u,
+		async : false,
+		cache : false,
+		data : val,
+		contentType : "text/plain",
+		dataType : "text",
+		processData : false,
+		success : done,
+		error : function(jqXHR, textStatus, errorThrown) {
+			alert(errorThrown);
+		}
+	});
+}
+/** How to send a PUT of XML asynchronously. */
+function putXML(u, xml, done) {
+	$.ajax({
+		type : "PUT",
+		url : u,
+		async : false,
+		cache : false,
+		contentType : "application/xml",
+		data : new XMLSerializer().serializeToString(xml),
+		success : done,
+		error : function(jqXHR, textStatus, errorThrown) {
+			alert(errorThrown);
+		}
+	});
+}
+/** How to send a POST of XML asynchronously. */
+function postXML(u, xml, done) {
+	$.ajax({
+		type : "POST",
+		url : u,
+		async : false,
+		cache : false,
+		contentType : "application/xml",
+		data : new XMLSerializer().serializeToString(xml),
+		success : done,
+		error : function(jqXHR, textStatus, errorThrown) {
+			alert(errorThrown);
+		}
+	});
+}
+/** How to send a DELETE asynchronously. */
+function deleteUrl(u, done) {
+	$.ajax({
+		type : "DELETE",
+		url : u,
+		async : true,
+		cache : false,
+		success : done,
+		error : function(jqXHR, textStatus, errorThrown) {
+			alert(errorThrown);
+		}
+	});
+}
+
+/** Locates a URL with respect to the administrative REST interface. */
+function where(tail) {
+	return $("#admin")[0].href + "/" + tail;
+}
+
+// Make an XML element structure
+// Derived from hack on Stack Overflow, but with extra tinkering
+/** Function called to create a node in an XML structure. */
+var Node;
+/** Function called to create nodes in an XML structure from an array. */
+var NodeAll;
+(function() {
+	var doc = document.implementation.createDocument(null, null, null);
+	var adminNS = "http://ns.taverna.org.uk/2010/xml/server/admin/";
+	Node = function() {
+		var node = doc.createElementNS(adminNS, arguments[0]), child;
+		for ( var i = 1; i < arguments.length; i++) {
+			child = arguments[i];
+			if (child == undefined)
+				continue;
+			if (typeof child != 'object')
+				child = doc.createTextNode(child.toString());
+			node.appendChild(child);
+		}
+		return node;
+	};
+	NodeAll = function(wrapperElem, wrappedElem, elements) {
+		var node = doc.createElementNS(adminNS, wrapperElem);
+		for ( var i = 0; i < elements.length; i++) {
+			var child = doc.createElementNS(adminNS, wrappedElem);
+			var text = doc.createTextNode(elements[i]);
+			child.appendChild(text);
+			node.appendChild(child);
+		}
+		return node;
+	};
+})();
+
+/** The IDs of boolean-coupled buttons. */
+var buttonlist = [ "allowNew", "logFaults", "logWorkflows", "generateProvenance" ];
+/** The IDs of values that track server variables without setting things. */
+var readonlies = [ "invokationCount", "lastExitCode", "runCount", "startupTime", "operatingCount" ];
+/** The IDs of text/numeric-coupled entries. */
+var entries = [ "defaultLifetime", "executeWorkflowScript", "javaBinary",
+		"registrationPollMillis", "registrationWaitSeconds", "registryHost",
+		"registryPort", "runLimit", "runasPasswordFile", "serverForkerJar",
+		"serverWorkerJar", "usageRecordDumpFile", "operatingLimit",
+		"registryJar" ];
+/** Cached information about users. */
+var userinfo = [];
+/** Extra arguments to pass to the runtime. */
+var extraAry = [];
+
+/** How to update the read-only fields; will be called periodically */
+function updateRO() {
+	$.each(readonlies, function(idx, val) {
+		var widget = $("#" + val);
+		getText(where(val), function(data) {
+			widget.html(data);
+		});
+	});
+	getJSON(where("factoryProcessMapping"),
+			function(data) {
+				var ary = data.stringList.string;
+				var tbl = $("#factoryProcessMapping");
+				tbl.html("<tr><th>User<th>ID</tr>");
+				if (ary != undefined)
+					for ( var i = 0; i < ary.length - 1; i += 2)
+						tbl.append("<tr><td>" + ary[i] + "<td>" + ary[i + 1]
+								+ "</tr>");
+			});
+}
+
+/**
+ * Generate a user row with suitable indices, but no content (it will be pushed
+ * into the row later).
+ */
+function userRowHTML(idx) {
+	// USER NAME
+	var content = "<td><span id='username" + idx
+			+ "' title='The login name of the user.'></span></td>";
+	// SYSTEM ID MAPPING
+	content += "<td><input id='userlocal"
+			+ idx
+			+ "' title='The system username to run workflows as, or blank for default.' /></td>";
+	// ENABLED
+	content += "<td><label title='Is this user allowed to log in?' for='useron"
+			+ idx + "'>Enabled</label>" + "<input type='checkbox' id='useron"
+			+ idx + "' /></td>";
+	// ADMIN
+	content += "<td><label title='Is this user an admin (allowed to access this page)?' for='useradmin"
+			+ idx
+			+ "'>Admin</label>"
+			+ "<input type='checkbox' id='useradmin"
+			+ idx + "' /></td>";
+	// SET PASSWORD
+	content += "<td><button title='Set the password for this user.' id='userpass"
+			+ idx + "'>Set Password</button></td>";
+	// DELETE
+	content += "<td><button title='Delete this user. Take care to not delete yourself!' id='userdel"
+			+ idx + "'>Delete</button></td>";
+
+	return "<tr id='usersep" + idx + "' class='userrows'>"
+			+ "<td colspan=6><hr></td></tr>" + "<tr id='userrow" + idx
+			+ "' class='userrows'>" + content + "</tr>";
+}
+
+/** How to get the list of permitted workflows; called on demand */
+function refreshWorkflows() {
+	var wftable = $("#workflows"), wfbut = $("#saveWorkflows"), wfbut1 = $("#emptyWorkflows"), wfref = $("#refreshWorkflows");
+	wfbut.button("disable");
+	wfbut1.button("disable");
+	wfref.button("disable");
+	getJSON(where("permittedWorkflowURIs"), function(data) {
+		var s = "";
+		$.each(data.stringList.string || [], function(idx, str) {
+			s += $.trim(str) + "\n";
+		});
+		wftable.val($.trim(s));
+		wfbut.button("enable");
+		wfbut1.button("enable");
+		wfref.button("enable");
+	});
+}
+/** How to set the list of permitted workflows; called when the user clicks */
+function saveWorkflows() {
+	var wftable = $("#workflows"), wfbut = $("#saveWorkflows"), wfbut1 = $("#emptyWorkflows");
+	var xml = NodeAll("stringList", "string", wftable.val().split("\n"));
+	wfbut.button("disable");
+	wfbut1.button("disable");
+	putXML(where("permittedWorkflowURIs"), xml, function() {
+		refreshWorkflows();
+	});
+}
+
+/** How to empty the list of permitted workflows; called when the user clicks */
+function emptyWorkflows() {
+        var wftable = $("#workflows"), wfbut = $("#saveWorkflows"), wfbut1 = $("#emptyWorkflows");
+        var xml = NodeAll("stringList", "string", "");
+        wfbut.button("disable");
+        wfbut1.button("disable");
+        putXML(where("permittedWorkflowURIs"), xml, function() {
+                refreshWorkflows();
+        });
+}
+
+/** How to update the table of users; called on demand */
+function refreshUsers() {
+	var usertable = $("#userList");
+	getJSON(where("users"), function(data) {
+		$(".userrows").remove();
+		userinfo = [];
+		$.each(data.userList.user, function(idx, url) {
+			usertable.append(userRowHTML(idx));
+			var i = idx;
+			userinfo[i] = {
+				url : url
+			};
+			getJSON(url, function(data) {
+				var model = userinfo[i].model = data.userDesc;
+				$("#username" + i).html(model.username);
+				$("#userlocal" + i).val(model.localUserId).change(function() {
+					updateUser(i, "localUserId", $(this).val());
+				});
+				$("#useron" + i).button().attr("checked", model.enabled)
+						.button("refresh").click(
+								function() {
+									updateUser(i, "enabled", $(this).attr(
+											"checked") == "checked");
+								});
+				$("#useradmin" + i).button().attr("checked", model.admin)
+						.button("refresh").click(
+								function() {
+									updateUser(i, "admin", $(this).attr(
+											"checked") == "checked");
+								});
+				$("#userpass" + i).button({
+					icons : {
+						primary : "ui-icon-alert"
+					}
+				}).click(function() {
+					updatePasswordUser(i);
+				});
+				$("#userdel" + i).button({
+					icons : {
+						primary : "ui-icon-trash"
+					},
+					text : false
+				}).click(function() {
+					deleteUser(i);
+				});
+			});
+			return true;
+		});
+	});
+}
+
+/** How to delete a user by index (with dialog) */
+function deleteUser(idx) {
+	$("#dialog-confirm").dialog({
+		modal : true,
+		autoOpen : false,
+		buttons : {
+			"OK" : function() {
+				$(this).dialog("close");
+				deleteUrl(userinfo[idx].url, function() {
+					refreshUsers();
+				});
+			},
+			"Cancel" : function() {
+				$(this).dialog("close");
+			}
+		}
+	});
+	$("#dialog-confirm").dialog("open");
+}
+
+/** How to update a user's password by index (with dialog) */
+function updatePasswordUser(idx) {
+	$("#change-password").val("");
+	$("#change-password2").val("");
+	$("#dialog-password").dialog({
+		modal : true,
+		autoOpen : false,
+		buttons : {
+			"OK" : function() {
+				$(this).dialog("close");
+				var pass = $("#change-password").val();
+				var pass2 = $("#change-password2").val();
+				$("#change-password").val("");
+				$("#change-password2").val("");
+				if (pass.equals(pass2))
+					updateUser(idx, "password", pass);
+			},
+			"Cancel" : function() {
+				$(this).dialog("close");
+				$("#change-password").val("");
+			}
+		}
+	});
+	$("#dialog-password").dialog("open");
+}
+
+/** How to set a specific field of a user record */
+function updateUser(idx, field, value) {
+	var model = userinfo[idx].model;
+	var xml = Node("userDesc", Node("username", model.username),
+			field == "password" ? Node("password", value) : undefined,
+			field == "localUserId" ? Node("localUserId", value)
+					: model.localUserId == undefined ? undefined : Node(
+							"localUserId", model.localUserId), Node("enabled",
+					field == "enabled" ? value : model.enabled), Node("admin",
+					field == "admin" ? value : model.admin));
+	putXML(userinfo[idx].url, xml, function() {
+		refreshUsers();
+	});
+}
+
+/** How to configure all the buttons and entries */
+function connectButtonsAndEntries() {
+	$.each(buttonlist, function(idx, val) {
+		var widget = $("#" + val);
+		var u = where(val);
+		widget.button();
+		getText(u, function(data) {
+			widget.attr('checked', (data + "") != "false");
+			widget.button("refresh");
+		});
+		widget.change(function() {
+			putText(u, widget.attr("checked") == "checked", function(data) {
+				widget.attr('checked', (data + "") != "false");
+				widget.button("refresh");
+				return true;
+			});
+		});
+	});
+	$.each(entries, function(idx, val) {
+		var widget = $("#" + val);
+		var u = where(val);
+		getText(u, function(data) {
+			widget.val(data);
+		});
+		widget.change(function() {
+			putText(u, widget.val(), function(data) {
+				widget.val(data);
+				return true;
+			});
+		});
+	});
+}
+
+/** What happens when the user tries to make a new user */
+function makeNewUser() {
+	var sysid = $("#newSysID").val();
+	var newuserinfo = {
+		admin : $("#newAdmin").attr("checked") == "checked",
+		enabled : $("#newEnabled").attr("checked") == "checked",
+		username : $("#newUsername").val(),
+		password : $("#newPassword").val()
+	};
+	// Blank out the password immediately!
+	$("#newPassword").val("");
+	if (sysid.length > 0) {
+		newuserinfo.localUserId = sysid;
+	}
+	if (newuserinfo.username == "" || newuserinfo.password == "") {
+		alert("Won't create user; need a username and a password!");
+		return;
+	}
+	var xml = newuserinfo = Node("userDesc", Node("username",
+			newuserinfo.username), Node("password", newuserinfo.password),
+			newuserinfo.localUserId == undefined ? undefined : Node(
+					"localUserId", newuserinfo.localUserId), Node("enabled",
+					newuserinfo.enabled), Node("admin", newuserinfo.admin));
+	postXML(where("users"), xml, function() {
+		refreshUsers();
+	});
+}
+
+/** Handle the extra arguments */
+function loadExtraArgs() {
+	getJSON(where("extraArguments"),
+			function(data) {
+				var rows = data.stringList.string || [];
+				if ((typeof rows) == "string")
+					rows = [ rows ];
+				$(".extraargrow").remove();
+				extraAry = rows;
+				var i;
+				function row() {
+					var buf = "<tr class='extraargrow'>";
+					for ( var i = 1; i < arguments.length; i++)
+						buf += "<td>" + arguments[i] + "</td>";
+					return $(arguments[0]).append(buf + "</tr>");
+				}
+				function delbutn(id, what) {
+					return "<button id='" + id + "' title='Delete this " + what
+							+ ".'>Del</button>";
+				}
+				for (i = 0; i < extraAry.length; i++) {
+					var rowid = "extradel" + i;
+					if (rows[i].match("^-D")) {
+						var m = rows[i].match("^-D([^=]*)=(.*)$");
+						row("#extraArguments-prop", delbutn(rowid,
+								"property assignment"), "<tt><b>-D</b>" + m[1]
+								+ "<b>=</b>" + m[2] + "</tt>");
+					} else if (rows[i].match("-E")) {
+						var m = rows[i].match("^-E([^=]*)=(.*)$");
+						row("#extraArguments-env", delbutn(rowid,
+								"environment assignment"), "<tt><b>-E</b>"
+								+ m[1] + "<b>=</b>" + m[2] + "</tt>");
+					} else {
+						var m = rows[i].match("^-J(.*)$");
+						row("#extraArguments-runtime", delbutn(rowid,
+								"runtime parameter"), "<tt><b>-J</b>" + m[1]
+								+ "</tt>");
+					}
+					$("#" + rowid).button({
+						icons : {
+							primary : "ui-icon-trash"
+						},
+						text : false
+					}).click(
+							(function(row) {
+								return function() {
+									extraAry.splice(row, 1);
+									var xml = NodeAll("stringList", "string",
+											extraAry);
+									putXML(where("extraArguments"), xml,
+											function() {
+												loadExtraArgs();
+											});
+								};
+							})(i));
+				}
+			});
+}
+
+/** Run a dialog for creating an extra argument. */
+function addExtraArg(dialogId, prefix, part1id, part2id) {
+	$(dialogId).dialog({
+		modal : true,
+		autoOpen : false,
+		buttons : {
+			"OK" : function() {
+				$(this).dialog("close");
+				var str = prefix + $(part1id).val();
+				if (part2id != undefined)
+					str += "=" + $(part2id).val();
+				extraAry.push(str);
+				var xml = NodeAll("stringList", "string", extraAry);
+				putXML(where("extraArguments"), xml, function() {
+					loadExtraArgs();
+				});
+			},
+			"Cancel" : function() {
+				$(this).dialog("close");
+			}
+		}
+	});
+	$(dialogId).dialog("open");
+}
+
+/** Start everything going on page load */
+$(function() {
+	// Must be done in this order because the accordion is inside a tab
+	$("#a-worker").accordion({
+		collapsible : true,
+		fillSpace : true,
+		autoHeight : false
+	});
+	$("#body").tabs({
+		selected : 0
+	});
+	$("#saveWorkflows").button({
+		disabled : true
+	}).click(function(event) {
+		saveWorkflows();
+		event.preventDefault();
+	});
+	$("#refreshWorkflows").button({
+		disabled : true
+	}).click(function(event) {
+		refreshWorkflows();
+		event.preventDefault();
+	});
+	$("#emptyWorkflows").button({
+                disabled : true
+        }).click(function(event) {
+                emptyWorkflows();
+                event.preventDefault();
+        });
+
+	// Make the link to the list of usage records point correctly
+	// Original plan called for browsable table, but that's too slow
+	$("#ur").attr("href", where("usageRecords"));
+
+	connectButtonsAndEntries();
+	updateRO();
+	setInterval(updateRO, 30000);
+	refreshUsers();
+	refreshWorkflows();
+	$("#newEnabled").button();
+	$("#newAdmin").button({
+		icons : {
+			primary : "ui-icon-alert"
+		}
+	});
+	$("#makeNewUser").button().click(function() {
+		makeNewUser();
+	});
+	$("#extra-prop-add").button().click(function() {
+		addExtraArg("#dialog-property", "-D", "#prop-key", "#prop-value");
+	});
+	$("#extra-env-add").button().click(function() {
+		addExtraArg("#dialog-environment", "-E", "#env-key", "#env-value");
+	});
+	$("#extra-run-add").button().click(function() {
+		addExtraArg("#dialog-runtime", "-J", "#runtime-value");
+	});
+	loadExtraArgs();
+});
diff --git a/server-webapp/src/main/resources/static/jquery-1.8.0.min.js b/server-webapp/src/main/resources/static/jquery-1.8.0.min.js
new file mode 100644
index 0000000..f121291
--- /dev/null
+++ b/server-webapp/src/main/resources/static/jquery-1.8.0.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v@1.8.0 jquery.com | jquery.org/license */

+(function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d<e;d++)p.event.add(b,c,h[c][d])}g.data&&(g.data=p.extend({},g.data))}function bE(a,b){var c;if(b.nodeType!==1)return;b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase(),c==="object"?(b.parentNode&&(b.outerHTML=a.outerHTML),p.support.html5Clone&&a.innerHTML&&!p.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):c==="input"&&bv.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):c==="option"?b.selected=a.defaultSelected:c==="input"||c==="textarea"?b.defaultValue=a.defaultValue:c==="script"&&b.text!==a.text&&(b.text=a.text),b.removeAttribute(p.expando)}function bF(a){return typeof a.getElementsByTagName!="undefined"?a.getElementsByTagName("*"):typeof a.querySelectorAll!="undefined"?a.querySelectorAll("*"):[]}function bG(a){bv.test(a.type)&&(a.defaultChecked=a.checked)}function bX(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=bV.length;while(e--){b=bV[e]+c;if(b in a)return b}return d}function bY(a,b){return a=b||a,p.css(a,"display")==="none"||!p.contains(a.ownerDocument,a)}function bZ(a,b){var c,d,e=[],f=0,g=a.length;for(;f<g;f++){c=a[f];if(!c.style)continue;e[f]=p._data(c,"olddisplay"),b?(!e[f]&&c.style.display==="none"&&(c.style.display=""),c.style.display===""&&bY(c)&&(e[f]=p._data(c,"olddisplay",cb(c.nodeName)))):(d=bH(c,"display"),!e[f]&&d!=="none"&&p._data(c,"olddisplay",d))}for(f=0;f<g;f++){c=a[f];if(!c.style)continue;if(!b||c.style.display==="none"||c.style.display==="")c.style.display=b?e[f]||"":"none"}return a}function b$(a,b,c){var d=bO.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function b_(a,b,c,d){var e=c===(d?"border":"content")?4:b==="width"?1:0,f=0;for(;e<4;e+=2)c==="margin"&&(f+=p.css(a,c+bU[e],!0)),d?(c==="content"&&(f-=parseFloat(bH(a,"padding"+bU[e]))||0),c!=="margin"&&(f-=parseFloat(bH(a,"border"+bU[e]+"Width"))||0)):(f+=parseFloat(bH(a,"padding"+bU[e]))||0,c!=="padding"&&(f+=parseFloat(bH(a,"border"+bU[e]+"Width"))||0));return f}function ca(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=!0,f=p.support.boxSizing&&p.css(a,"boxSizing")==="border-box";if(d<=0){d=bH(a,b);if(d<0||d==null)d=a.style[b];if(bP.test(d))return d;e=f&&(p.support.boxSizingReliable||d===a.style[b]),d=parseFloat(d)||0}return d+b_(a,b,c||(f?"border":"content"),e)+"px"}function cb(a){if(bR[a])return bR[a];var b=p("<"+a+">").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write("<!doctype html><html><body>"),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bR[a]=c,c}function ch(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||cd.test(a)?d(a,e):ch(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ch(a+"["+e+"]",b[e],c,d);else d(a,b)}function cy(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h<i;h++)d=g[h],f=/^\+/.test(d),f&&(d=d.substr(1)||"*"),e=a[d]=a[d]||[],e[f?"unshift":"push"](c)}}function cz(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h,i=a[f],j=0,k=i?i.length:0,l=a===cu;for(;j<k&&(l||!h);j++)h=i[j](c,d,e),typeof h=="string"&&(!l||g[h]?h=b:(c.dataTypes.unshift(h),h=cz(a,c,d,e,h,g)));return(l||!h)&&!g["*"]&&(h=cz(a,c,d,e,"*",g)),h}function cA(a,c){var d,e,f=p.ajaxSettings.flatOptions||{};for(d in c)c[d]!==b&&((f[d]?a:e||(e={}))[d]=c[d]);e&&p.extend(!0,a,e)}function cB(a,c,d){var e,f,g,h,i=a.contents,j=a.dataTypes,k=a.responseFields;for(f in k)f in d&&(c[k[f]]=d[f]);while(j[0]==="*")j.shift(),e===b&&(e=a.mimeType||c.getResponseHeader("content-type"));if(e)for(f in i)if(i[f]&&i[f].test(e)){j.unshift(f);break}if(j[0]in d)g=j[0];else{for(f in d){if(!j[0]||a.converters[f+" "+j[0]]){g=f;break}h||(h=f)}g=g||h}if(g)return g!==j[0]&&j.unshift(g),d[g]}function cC(a,b){var c,d,e,f,g=a.dataTypes.slice(),h=g[0],i={},j=0;a.dataFilter&&(b=a.dataFilter(b,a.dataType));if(g[1])for(c in a.converters)i[c.toLowerCase()]=a.converters[c];for(;e=g[++j];)if(e!=="*"){if(h!=="*"&&h!==e){c=i[h+" "+e]||i["* "+e];if(!c)for(d in i){f=d.split(" ");if(f[1]===e){c=i[h+" "+f[0]]||i["* "+f[0]];if(c){c===!0?c=i[d]:i[d]!==!0&&(e=f[0],g.splice(j--,0,e));break}}}if(c!==!0)if(c&&a["throws"])b=c(b);else try{b=c(b)}catch(k){return{state:"parsererror",error:c?k:"No conversion from "+h+" to "+e}}}h=e}return{state:"success",data:b}}function cK(){try{return new a.XMLHttpRequest}catch(b){}}function cL(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function cT(){return setTimeout(function(){cM=b},0),cM=p.now()}function cU(a,b){p.each(b,function(b,c){var d=(cS[b]||[]).concat(cS["*"]),e=0,f=d.length;for(;e<f;e++)if(d[e].call(a,b,c))return})}function cV(a,b,c){var d,e=0,f=0,g=cR.length,h=p.Deferred().always(function(){delete i.elem}),i=function(){var b=cM||cT(),c=Math.max(0,j.startTime+j.duration-b),d=1-(c/j.duration||0),e=0,f=j.tweens.length;for(;e<f;e++)j.tweens[e].run(d);return h.notifyWith(a,[j,d,c]),d<1&&f?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:p.extend({},b),opts:p.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:cM||cT(),duration:c.duration,tweens:[],createTween:function(b,c,d){var e=p.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(e),e},stop:function(b){var c=0,d=b?j.tweens.length:0;for(;c<d;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;cW(k,j.opts.specialEasing);for(;e<g;e++){d=cR[e].call(j,a,k,j.opts);if(d)return d}return cU(j,k),p.isFunction(j.opts.start)&&j.opts.start.call(a,j),p.fx.timer(p.extend(i,{anim:j,queue:j.opts.queue,elem:a})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}function cW(a,b){var c,d,e,f,g;for(c in a){d=p.camelCase(c),e=b[d],f=a[c],p.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=p.cssHooks[d];if(g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}}function cX(a,b,c){var d,e,f,g,h,i,j,k,l=this,m=a.style,n={},o=[],q=a.nodeType&&bY(a);c.queue||(j=p._queueHooks(a,"fx"),j.unqueued==null&&(j.unqueued=0,k=j.empty.fire,j.empty.fire=function(){j.unqueued||k()}),j.unqueued++,l.always(function(){l.always(function(){j.unqueued--,p.queue(a,"fx").length||j.empty.fire()})})),a.nodeType===1&&("height"in b||"width"in b)&&(c.overflow=[m.overflow,m.overflowX,m.overflowY],p.css(a,"display")==="inline"&&p.css(a,"float")==="none"&&(!p.support.inlineBlockNeedsLayout||cb(a.nodeName)==="inline"?m.display="inline-block":m.zoom=1)),c.overflow&&(m.overflow="hidden",p.support.shrinkWrapBlocks||l.done(function(){m.overflow=c.overflow[0],m.overflowX=c.overflow[1],m.overflowY=c.overflow[2]}));for(d in b){f=b[d];if(cO.exec(f)){delete b[d];if(f===(q?"hide":"show"))continue;o.push(d)}}g=o.length;if(g){h=p._data(a,"fxshow")||p._data(a,"fxshow",{}),q?p(a).show():l.done(function(){p(a).hide()}),l.done(function(){var b;p.removeData(a,"fxshow",!0);for(b in n)p.style(a,b,n[b])});for(d=0;d<g;d++)e=o[d],i=l.createTween(e,q?h[e]:0),n[e]=h[e]||p.style(a,e),e in h||(h[e]=i.start,q&&(i.end=i.start,i.start=e==="width"||e==="height"?1:0))}}function cY(a,b,c,d,e){return new cY.prototype.init(a,b,c,d,e)}function cZ(a,b){var c,d={height:a},e=0;for(;e<4;e+=2-b)c=bU[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function c_(a){return p.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}var c,d,e=a.document,f=a.location,g=a.navigator,h=a.jQuery,i=a.$,j=Array.prototype.push,k=Array.prototype.slice,l=Array.prototype.indexOf,m=Object.prototype.toString,n=Object.prototype.hasOwnProperty,o=String.prototype.trim,p=function(a,b){return new p.fn.init(a,b,c)},q=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,r=/\S/,s=/\s+/,t=r.test(" ")?/^[\s\xA0]+|[\s\xA0]+$/g:/^\s+|\s+$/g,u=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.0",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i<j;i++)if((a=arguments[i])!=null)for(c in a){d=h[c],e=a[c];if(h===e)continue;k&&e&&(p.isPlainObject(e)||(f=p.isArray(e)))?(f?(f=!1,g=d&&p.isArray(d)?d:[]):g=d&&p.isPlainObject(d)?d:{},h[c]=p.extend(k,g,e)):e!==b&&(h[c]=e)}return h},p.extend({noConflict:function(b){return a.$===p&&(a.$=i),b&&a.jQuery===p&&(a.jQuery=h),p},isReady:!1,readyWait:1,holdReady:function(a){a?p.readyWait++:p.ready(!0)},ready:function(a){if(a===!0?--p.readyWait:p.isReady)return;if(!e.body)return setTimeout(p.ready,1);p.isReady=!0;if(a!==!0&&--p.readyWait>0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f<g;)if(c.apply(a[f++],d)===!1)break}else if(h){for(e in a)if(c.call(a[e],e,a[e])===!1)break}else for(;f<g;)if(c.call(a[f],f,a[f++])===!1)break;return a},trim:o?function(a){return a==null?"":o.call(a)}:function(a){return a==null?"":a.toString().replace(t,"")},makeArray:function(a,b){var c,d=b||[];return a!=null&&(c=p.type(a),a.length==null||c==="string"||c==="function"||c==="regexp"||p.isWindow(a)?j.call(d,a):p.merge(d,a)),d},inArray:function(a,b,c){var d;if(b){if(l)return l.call(b,a,c);d=b.length,c=c?c<0?Math.max(0,d+c):c:0;for(;c<d;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,c){var d=c.length,e=a.length,f=0;if(typeof d=="number")for(;f<d;f++)a[e++]=c[f];else while(c[f]!==b)a[e++]=c[f++];return a.length=e,a},grep:function(a,b,c){var d,e=[],f=0,g=a.length;c=!!c;for(;f<g;f++)d=!!b(a[f],f),c!==d&&e.push(a[f]);return e},map:function(a,c,d){var e,f,g=[],h=0,i=a.length,j=a instanceof p||i!==b&&typeof i=="number"&&(i>0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h<i;h++)e=c(a[h],h,d),e!=null&&(g[g.length]=e);else for(f in a)e=c(a[f],f,d),e!=null&&(g[g.length]=e);return g.concat.apply([],g)},guid:1,proxy:function(a,c){var d,e,f;return typeof c=="string"&&(d=a[c],c=a,a=d),p.isFunction(a)?(e=k.call(arguments,2),f=function(){return a.apply(c,e.concat(k.call(arguments)))},f.guid=a.guid=a.guid||f.guid||p.guid++,f):b},access:function(a,c,d,e,f,g,h){var i,j=d==null,k=0,l=a.length;if(d&&typeof d=="object"){for(k in d)p.access(a,c,k,d[k],1,g,e);f=1}else if(e!==b){i=h===b&&p.isFunction(e),j&&(i?(i=c,c=function(a,b,c){return i.call(p(a),c)}):(c.call(a,e),c=null));if(c)for(;k<l;k++)c(a[k],d,i?e.call(a[k],k,c(a[k],d)):e,h);f=1}return f?a:j?c.call(a):l?c(a[0],d):g},now:function(){return(new Date).getTime()}}),p.ready.promise=function(b){if(!d){d=p.Deferred();if(e.readyState==="complete"||e.readyState!=="loading"&&e.addEventListener)setTimeout(p.ready,1);else if(e.addEventListener)e.addEventListener("DOMContentLoaded",D,!1),a.addEventListener("load",p.ready,!1);else{e.attachEvent("onreadystatechange",D),a.attachEvent("onload",p.ready);var c=!1;try{c=a.frameElement==null&&e.documentElement}catch(f){}c&&c.doScroll&&function g(){if(!p.isReady){try{c.doScroll("left")}catch(a){return setTimeout(g,50)}p.ready()}}()}}return d.promise(b)},p.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){E["[object "+b+"]"]=b.toLowerCase()}),c=p(e);var F={};p.Callbacks=function(a){a=typeof a=="string"?F[a]||G(a):p.extend({},a);var c,d,e,f,g,h,i=[],j=!a.once&&[],k=function(b){c=a.memory&&b,d=!0,h=f||0,f=0,g=i.length,e=!0;for(;i&&h<g;h++)if(i[h].apply(b[0],b[1])===!1&&a.stopOnFalse){c=!1;break}e=!1,i&&(j?j.length&&k(j.shift()):c?i=[]:l.disable())},l={add:function(){if(i){var b=i.length;(function d(b){p.each(b,function(b,c){p.isFunction(c)&&(!a.unique||!l.has(c))?i.push(c):c&&c.length&&d(c)})})(arguments),e?g=i.length:c&&(f=b,k(c))}return this},remove:function(){return i&&p.each(arguments,function(a,b){var c;while((c=p.inArray(b,i,c))>-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return typeof a=="object"?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b<d;b++)c[b]&&p.isFunction(c[b].promise)?c[b].promise().done(g(b,j,c)).fail(f.reject).progress(g(b,i,h)):--e}return e||f.resolveWith(j,c),f.promise()}}),p.support=function(){var b,c,d,f,g,h,i,j,k,l,m,n=e.createElement("div");n.setAttribute("className","t"),n.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length||!d)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="<table><tr><td></td><td>t</td></tr></table>",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="<div></div>",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/^(?:\{.*\}|\[.*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||++p.uuid:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e<f;e++)delete d[b[e]];if(!(c?K:p.isEmptyObject)(d))return}}if(!c){delete h[i].data;if(!K(h[i]))return}g?p.cleanData([a],!0):p.support.deleteExpando||h!=h.window?delete h[i]:h[i]=null},_data:function(a,b,c){return p.data(a,b,c,!0)},acceptData:function(a){var b=a.nodeName&&p.noData[a.nodeName.toLowerCase()];return!b||b!==!0&&a.getAttribute("classid")===b}}),p.fn.extend({data:function(a,c){var d,e,f,g,h,i=this[0],j=0,k=null;if(a===b){if(this.length){k=p.data(i);if(i.nodeType===1&&!p._data(i,"parsedAttrs")){f=i.attributes;for(h=f.length;j<h;j++)g=f[j].name,g.indexOf("data-")===0&&(g=p.camelCase(g.substring(5)),J(i,g,k[g]));p._data(i,"parsedAttrs",!0)}}return k}return typeof a=="object"?this.each(function(){p.data(this,a)}):(d=a.split(".",2),d[1]=d[1]?"."+d[1]:"",e=d[1]+"!",p.access(this,function(c){if(c===b)return k=this.triggerHandler("getData"+e,[d[0]]),k===b&&i&&(k=p.data(i,a),k=J(i,a,k)),k===b&&d[1]?this.data(d[0]):k;d[1]=c,this.each(function(){var b=p(this);b.triggerHandler("setData"+e,d),p.data(this,a,c),b.triggerHandler("changeData"+e,d)})},null,c,arguments.length>1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.shift(),e=p._queueHooks(a,b),f=function(){p.dequeue(a,b)};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),delete e.stop,d.call(a,f,e)),!c.length&&e&&e.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length<d?p.queue(this[0],a):c===b?this:this.each(function(){var b=p.queue(this,a,c);p._queueHooks(this,a),a==="fx"&&b[0]!=="inprogress"&&p.dequeue(this,a)})},dequeue:function(a){return this.each(function(){p.dequeue(this,a)})},delay:function(a,b){return a=p.fx?p.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){var d,e=1,f=p.Deferred(),g=this,h=this.length,i=function(){--e||f.resolveWith(g,[g])};typeof a!="string"&&(c=a,a=b),a=a||"fx";while(h--)(d=p._data(g[h],a+"queueHooks"))&&d.empty&&(e++,d.empty.add(i));return i(),f.promise(c)}});var L,M,N,O=/[\t\r\n]/g,P=/\r/g,Q=/^(?:button|input)$/i,R=/^(?:button|input|object|select|textarea)$/i,S=/^a(?:rea|)$/i,T=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,U=p.support.getSetAttribute;p.fn.extend({attr:function(a,b){return p.access(this,p.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{f=" "+e.className+" ";for(g=0,h=b.length;g<h;g++)~f.indexOf(" "+b[g]+" ")||(f+=b[g]+" ");e.className=p.trim(f)}}}return this},removeClass:function(a){var c,d,e,f,g,h,i;if(p.isFunction(a))return this.each(function(b){p(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(s);for(h=0,i=this.length;h<i;h++){e=this[h];if(e.nodeType===1&&e.className){d=(" "+e.className+" ").replace(O," ");for(f=0,g=c.length;f<g;f++)while(d.indexOf(" "+c[f]+" ")>-1)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c<d;c++)if(this[c].nodeType===1&&(" "+this[c].className+" ").replace(O," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c<d;c++){e=h[c];if(e.selected&&(p.support.optDisabled?!e.disabled:e.getAttribute("disabled")===null)&&(!e.parentNode.disabled||!p.nodeName(e.parentNode,"optgroup"))){b=p(e).val();if(i)return b;g.push(b)}}return i&&!g.length&&h.length?p(h[f]).val():g},set:function(a,b){var c=p.makeArray(b);return p(a).find("option").each(function(){this.selected=p.inArray(p(this).val(),c)>=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,""+d),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g<d.length;g++)e=d[g],e&&(c=p.propFix[e]||e,f=T.test(e),f||p.attr(a,e,""),a.removeAttribute(U?e:c),f&&c in a&&(a[c]=!1))}},attrHooks:{type:{set:function(a,b){if(Q.test(a.nodeName)&&a.parentNode)p.error("type property can't be changed");else if(!p.support.radioValue&&b==="radio"&&p.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}},value:{get:function(a,b){return L&&p.nodeName(a,"button")?L.get(a,b):b in a?a.value:null},set:function(a,b,c){if(L&&p.nodeName(a,"button"))return L.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e,f,g,h=a.nodeType;if(!a||h===3||h===8||h===2)return;return g=h!==1||!p.isXMLDoc(a),g&&(c=p.propFix[c]||c,f=p.propHooks[c]),d!==b?f&&"set"in f&&(e=f.set(a,d,c))!==b?e:a[c]=d:f&&"get"in f&&(e=f.get(a,c))!==null?e:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):R.test(a.nodeName)||S.test(a.nodeName)&&a.href?0:b}}}}),M={get:function(a,c){var d,e=p.prop(a,c);return e===!0||typeof e!="boolean"&&(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;return b===!1?p.removeAttr(a,c):(d=p.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase())),c}},U||(N={name:!0,id:!0,coords:!0},L=p.valHooks.button={get:function(a,c){var d;return d=a.getAttributeNode(c),d&&(N[c]?d.value!=="":d.specified)?d.value:b},set:function(a,b,c){var d=a.getAttributeNode(c);return d||(d=e.createAttribute(c),a.setAttributeNode(d)),d.value=b+""}},p.each(["width","height"],function(a,b){p.attrHooks[b]=p.extend(p.attrHooks[b],{set:function(a,c){if(c==="")return a.setAttribute(b,"auto"),c}})}),p.attrHooks.contenteditable={get:L.get,set:function(a,b,c){b===""&&(b="false"),L.set(a,b,c)}}),p.support.hrefNormalized||p.each(["href","src","width","height"],function(a,c){p.attrHooks[c]=p.extend(p.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),p.support.style||(p.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),p.support.optSelected||(p.propHooks.selected=p.extend(p.propHooks.selected,{get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}})),p.support.enctype||(p.propFix.enctype="encoding"),p.support.checkOn||p.each(["radio","checkbox"],function(){p.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),p.each(["radio","checkbox"],function(){p.valHooks[this]=p.extend(p.valHooks[this],{set:function(a,b){if(p.isArray(b))return a.checked=p.inArray(p(a).val(),b)>=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j<c.length;j++){k=W.exec(c[j])||[],l=k[1],m=(k[2]||"").split(".").sort(),r=p.event.special[l]||{},l=(f?r.delegateType:r.bindType)||l,r=p.event.special[l]||{},n=p.extend({type:l,origType:k[1],data:e,handler:d,guid:d.guid,selector:f,namespace:m.join(".")},o),q=i[l];if(!q){q=i[l]=[],q.delegateCount=0;if(!r.setup||r.setup.call(a,e,m,h)===!1)a.addEventListener?a.addEventListener(l,h,!1):a.attachEvent&&a.attachEvent("on"+l,h)}r.add&&(r.add.call(a,n),n.handler.guid||(n.handler.guid=d.guid)),f?q.splice(q.delegateCount++,0,n):q.push(n),p.event.global[l]=!0}a=null},global:{},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,q,r=p.hasData(a)&&p._data(a);if(!r||!(m=r.events))return;b=p.trim(_(b||"")).split(" ");for(f=0;f<b.length;f++){g=W.exec(b[f])||[],h=i=g[1],j=g[2];if(!h){for(h in m)p.event.remove(a,h+b[f],c,d,!0);continue}n=p.event.special[h]||{},h=(d?n.delegateType:n.bindType)||h,o=m[h]||[],k=o.length,j=j?new RegExp("(^|\\.)"+j.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(l=0;l<o.length;l++)q=o[l],(e||i===q.origType)&&(!c||c.guid===q.guid)&&(!j||j.test(q.namespace))&&(!d||d===q.selector||d==="**"&&q.selector)&&(o.splice(l--,1),q.selector&&o.delegateCount--,n.remove&&n.remove.call(a,q));o.length===0&&k!==o.length&&((!n.teardown||n.teardown.call(a,j,r.handle)===!1)&&p.removeEvent(a,h,r.handle),delete m[h])}p.isEmptyObject(m)&&(delete r.handle,p.removeData(a,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,f,g){if(!f||f.nodeType!==3&&f.nodeType!==8){var h,i,j,k,l,m,n,o,q,r,s=c.type||c,t=[];if($.test(s+p.event.triggered))return;s.indexOf("!")>=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;j<q.length&&!c.isPropagationStopped();j++)k=q[j][0],c.type=q[j][1],o=(p._data(k,"events")||{})[c.type]&&p._data(k,"handle"),o&&o.apply(k,d),o=m&&k[m],o&&p.acceptData(k)&&o.apply(k,d)===!1&&c.preventDefault();return c.type=s,!g&&!c.isDefaultPrevented()&&(!n._default||n._default.apply(f.ownerDocument,d)===!1)&&(s!=="click"||!p.nodeName(f,"a"))&&p.acceptData(f)&&m&&f[s]&&(s!=="focus"&&s!=="blur"||c.target.offsetWidth!==0)&&!p.isWindow(f)&&(l=f[m],l&&(f[m]=null),p.event.triggered=s,f[s](),p.event.triggered=b,l&&(f[m]=l)),c.result}return},dispatch:function(c){c=p.event.fix(c||a.event);var d,e,f,g,h,i,j,k,l,m,n,o=(p._data(this,"events")||{})[c.type]||[],q=o.delegateCount,r=[].slice.call(arguments),s=!c.exclusive&&!c.namespace,t=p.event.special[c.type]||{},u=[];r[0]=c,c.delegateTarget=this;if(t.preDispatch&&t.preDispatch.call(this,c)===!1)return;if(q&&(!c.button||c.type!=="click")){g=p(this),g.context=this;for(f=c.target;f!=this;f=f.parentNode||this)if(f.disabled!==!0||c.type!=="click"){i={},k=[],g[0]=f;for(d=0;d<q;d++)l=o[d],m=l.selector,i[m]===b&&(i[m]=g.is(m)),i[m]&&k.push(l);k.length&&u.push({elem:f,matches:k})}}o.length>q&&u.push({elem:this,matches:o.slice(q)});for(d=0;d<u.length&&!c.isPropagationStopped();d++){j=u[d],c.currentTarget=j.elem;for(e=0;e<j.matches.length&&!c.isImmediatePropagationStopped();e++){l=j.matches[e];if(s||!c.namespace&&!l.namespace||c.namespace_re&&c.namespace_re.test(l.namespace))c.data=l.data,c.handleObj=l,h=((p.event.special[l.origType]||{}).handle||l.handler).apply(j.elem,r),h!==b&&(c.result=h,h===!1&&(c.preventDefault(),c.stopPropagation()))}}return t.postDispatch&&t.postDispatch.call(this,c),c.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return a.which==null&&(a.which=b.charCode!=null?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,c){var d,f,g,h=c.button,i=c.fromElement;return a.pageX==null&&c.clientX!=null&&(d=a.target.ownerDocument||e,f=d.documentElement,g=d.body,a.pageX=c.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=c.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)),!a.relatedTarget&&i&&(a.relatedTarget=i===a.target?c.toElement:i),!a.which&&h!==b&&(a.which=h&1?1:h&2?3:h&4?2:0),a}},fix:function(a){if(a[p.expando])return a;var b,c,d=a,f=p.event.fixHooks[a.type]||{},g=f.props?this.props.concat(f.props):this.props;a=p.Event(d);for(b=g.length;b;)c=g[--b],a[c]=d[c];return a.target||(a.target=d.srcElement||e),a.target.nodeType===3&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,f.filter?f.filter(a,d):a},special:{ready:{setup:p.bindReady},load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(a,b,c){p.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}},simulate:function(a,b,c,d){var e=p.extend(new p.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?p.event.trigger(e,null,b):p.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},p.event.handle=p.event.dispatch,p.removeEvent=e.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]=="undefined"&&(a[d]=null),a.detachEvent(d,c))},p.Event=function(a,b){if(this instanceof p.Event)a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?bb:ba):this.type=a,b&&p.extend(this,b),this.timeStamp=a&&a.timeStamp||p.now(),this[p.expando]=!0;else return new p.Event(a,b)},p.Event.prototype={preventDefault:function(){this.isDefaultPrevented=bb;var a=this.originalEvent;if(!a)return;a.preventDefault?a.preventDefault():a.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=bb;var a=this.originalEvent;if(!a)return;a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=bb,this.stopPropagation()},isDefaultPrevented:ba,isPropagationStopped:ba,isImmediatePropagationStopped:ba},p.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){p.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj,g=f.selector;if(!e||e!==d&&!p.contains(d,e))a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b;return c}}}),p.support.submitBubbles||(p.event.special.submit={setup:function(){if(p.nodeName(this,"form"))return!1;p.event.add(this,"click._submit keypress._submit",function(a){var c=a.target,d=p.nodeName(c,"input")||p.nodeName(c,"button")?c.form:b;d&&!p._data(d,"_submit_attached")&&(p.event.add(d,"submit._submit",function(a){a._submit_bubble=!0}),p._data(d,"_submit_attached",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&p.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){if(p.nodeName(this,"form"))return!1;p.event.remove(this,"._submit")}}),p.support.changeBubbles||(p.event.special.change={setup:function(){if(V.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")p.event.add(this,"propertychange._change",function(a){a.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),p.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),p.event.simulate("change",this,a,!0)});return!1}p.event.add(this,"beforeactivate._change",function(a){var b=a.target;V.test(b.nodeName)&&!p._data(b,"_change_attached")&&(p.event.add(b,"change._change",function(a){this.parentNode&&!a.isSimulated&&!a.isTrigger&&p.event.simulate("change",this.parentNode,a,!0)}),p._data(b,"_change_attached",!0))})},handle:function(a){var b=a.target;if(this!==b||a.isSimulated||a.isTrigger||b.type!=="radio"&&b.type!=="checkbox")return a.handleObj.handler.apply(this,arguments)},teardown:function(){return p.event.remove(this,"._change"),V.test(this.nodeName)}}),p.support.focusinBubbles||p.each({focus:"focusin",blur:"focusout"},function(a,b){var c=0,d=function(a){p.event.simulate(b,a.target,p.event.fix(a),!0)};p.event.special[b]={setup:function(){c++===0&&e.addEventListener(a,d,!0)},teardown:function(){--c===0&&e.removeEventListener(a,d,!0)}}}),p.fn.extend({on:function(a,c,d,e,f){var g,h;if(typeof a=="object"){typeof c!="string"&&(d=d||c,c=b);for(h in a)this.on(h,c,d,a[h],f);return this}d==null&&e==null?(e=c,d=c=b):e==null&&(typeof c=="string"?(e=d,d=b):(e=d,d=c,c=b));if(e===!1)e=ba;else if(!e)return this;return f===1&&(g=e,e=function(a){return p().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=p.guid++)),this.each(function(){p.event.add(this,a,e,d,c)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,c,d){var e,f;if(a&&a.preventDefault&&a.handleObj)return e=a.handleObj,p(a.delegateTarget).off(e.namespace?e.origType+"."+e.namespace:e.origType,e.selector,e.handler),this;if(typeof a=="object"){for(f in a)this.off(f,c,a[f]);return this}if(c===!1||typeof c=="function")d=c,c=b;return d===!1&&(d=ba),this.each(function(){p.event.remove(this,a,d,c)})},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},live:function(a,b,c){return p(this.context).on(a,this.selector,b,c),this},die:function(a,b){return p(this.context).off(a,this.selector||"**",b),this},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return arguments.length==1?this.off(a,"**"):this.off(b,a||"**",c)},trigger:function(a,b){return this.each(function(){p.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return p.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||p.guid++,d=0,e=function(c){var e=(p._data(this,"lastToggle"+a.guid)||0)%d;return p._data(this,"lastToggle"+a.guid,e+1),c.preventDefault(),b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),p.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){p.fn[b]=function(a,c){return c==null&&(c=a,a=null),arguments.length>0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function bd(a,b,c,d){var e=0,f=b.length;for(;e<f;e++)Z(a,b[e],c,d)}function be(a,b,c,d,e,f){var g,h=$.setFilters[b.toLowerCase()];return h||Z.error(b),(a||!(g=e))&&bd(a||"*",d,g=[],e),g.length>0?h(g,c,f):[]}function bf(a,c,d,e,f){var g,h,i,j,k,l,m,n,p=0,q=f.length,s=L.POS,t=new RegExp("^"+s.source+"(?!"+r+")","i"),u=function(){var a=1,c=arguments.length-2;for(;a<c;a++)arguments[a]===b&&(g[a]=b)};for(;p<q;p++){s.exec(""),a=f[p],j=[],i=0,k=e;while(g=s.exec(a)){n=s.lastIndex=g.index+g[0].length;if(n>i){m=a.slice(i,g.index),i=n,l=[c],B.test(m)&&(k&&(l=k),k=e);if(h=H.test(m))m=m.slice(0,-5).replace(B,"$&*");g.length>1&&g[0].replace(t,u),k=be(m,g[1],g[2],l,k,h)}}k?(j=j.concat(k),(m=a.slice(i))&&m!==")"?B.test(m)?bd(m,j,d,e):Z(m,c,d,e?e.concat(k):k):o.apply(d,j)):Z(a,c,d,e)}return q===1?d:Z.uniqueSort(d)}function bg(a,b,c){var d,e,f,g=[],i=0,j=D.exec(a),k=!j.pop()&&!j.pop(),l=k&&a.match(C)||[""],m=$.preFilter,n=$.filter,o=!c&&b!==h;for(;(e=l[i])!=null&&k;i++){g.push(d=[]),o&&(e=" "+e);while(e){k=!1;if(j=B.exec(e))e=e.slice(j[0].length),k=d.push({part:j.pop().replace(A," "),captures:j});for(f in n)(j=L[f].exec(e))&&(!m[f]||(j=m[f](j,b,c)))&&(e=e.slice(j.shift().length),k=d.push({part:f,captures:j}));if(!k)break}}return k||Z.error(a),g}function bh(a,b,e){var f=b.dir,g=m++;return a||(a=function(a){return a===e}),b.first?function(b,c){while(b=b[f])if(b.nodeType===1)return a(b,c)&&b}:function(b,e){var h,i=g+"."+d,j=i+"."+c;while(b=b[f])if(b.nodeType===1){if((h=b[q])===j)return b.sizset;if(typeof h=="string"&&h.indexOf(i)===0){if(b.sizset)return b}else{b[q]=j;if(a(b,e))return b.sizset=!0,b;b.sizset=!1}}}}function bi(a,b){return a?function(c,d){var e=b(c,d);return e&&a(e===!0?c:e,d)}:b}function bj(a,b,c){var d,e,f=0;for(;d=a[f];f++)$.relative[d.part]?e=bh(e,$.relative[d.part],b):(d.captures.push(b,c),e=bi(e,$.filter[d.part].apply(null,d.captures)));return e}function bk(a){return function(b,c){var d,e=0;for(;d=a[e];e++)if(d(b,c))return!0;return!1}}var c,d,e,f,g,h=a.document,i=h.documentElement,j="undefined",k=!1,l=!0,m=0,n=[].slice,o=[].push,q=("sizcache"+Math.random()).replace(".",""),r="[\\x20\\t\\r\\n\\f]",s="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",t=s.replace("w","w#"),u="([*^$|!~]?=)",v="\\["+r+"*("+s+")"+r+"*(?:"+u+r+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+t+")|)|)"+r+"*\\]",w=":("+s+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|((?:[^,]|\\\\,|(?:,(?=[^\\[]*\\]))|(?:,(?=[^\\(]*\\))))*))\\)|)",x=":(nth|eq|gt|lt|first|last|even|odd)(?:\\((\\d*)\\)|)(?=[^-]|$)",y=r+"*([\\x20\\t\\r\\n\\f>+~])"+r+"*",z="(?=[^\\x20\\t\\r\\n\\f])(?:\\\\.|"+v+"|"+w.replace(2,7)+"|[^\\\\(),])+",A=new RegExp("^"+r+"+|((?:^|[^\\\\])(?:\\\\.)*)"+r+"+$","g"),B=new RegExp("^"+y),C=new RegExp(z+"?(?="+r+"*,|$)","g"),D=new RegExp("^(?:(?!,)(?:(?:^|,)"+r+"*"+z+")*?|"+r+"*(.*?))(\\)|$)"),E=new RegExp(z.slice(19,-6)+"\\x20\\t\\r\\n\\f>+~])+|"+y,"g"),F=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,G=/[\x20\t\r\n\f]*[+~]/,H=/:not\($/,I=/h\d/i,J=/input|select|textarea|button/i,K=/\\(?!\\)/g,L={ID:new RegExp("^#("+s+")"),CLASS:new RegExp("^\\.("+s+")"),NAME:new RegExp("^\\[name=['\"]?("+s+")['\"]?\\]"),TAG:new RegExp("^("+s.replace("[-","[-\\*")+")"),ATTR:new RegExp("^"+v),PSEUDO:new RegExp("^"+w),CHILD:new RegExp("^:(only|nth|last|first)-child(?:\\("+r+"*(even|odd|(([+-]|)(\\d*)n|)"+r+"*(?:([+-]|)"+r+"*(\\d+)|))"+r+"*\\)|)","i"),POS:new RegExp(x,"ig"),needsContext:new RegExp("^"+r+"*[>+~]|"+x,"i")},M={},N=[],O={},P=[],Q=function(a){return a.sizzleFilter=!0,a},R=function(a){return function(b){return b.nodeName.toLowerCase()==="input"&&b.type===a}},S=function(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}},T=function(a){var b=!1,c=h.createElement("div");try{b=a(c)}catch(d){}return c=null,b},U=T(function(a){a.innerHTML="<select></select>";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),V=T(function(a){a.id=q+0,a.innerHTML="<a name='"+q+"'></a><div name='"+q+"'></div>",i.insertBefore(a,i.firstChild);var b=h.getElementsByName&&h.getElementsByName(q).length===2+h.getElementsByName(q+0).length;return g=!h.getElementById(q),i.removeChild(a),b}),W=T(function(a){return a.appendChild(h.createComment("")),a.getElementsByTagName("*").length===0}),X=T(function(a){return a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!==j&&a.firstChild.getAttribute("href")==="#"}),Y=T(function(a){return a.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!a.getElementsByClassName||a.getElementsByClassName("e").length===0?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length!==1)}),Z=function(a,b,c,d){c=c||[],b=b||h;var e,f,g,i,j=b.nodeType;if(j!==1&&j!==9)return[];if(!a||typeof a!="string")return c;g=ba(b);if(!g&&!d)if(e=F.exec(a))if(i=e[1]){if(j===9){f=b.getElementById(i);if(!f||!f.parentNode)return c;if(f.id===i)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(i))&&bb(b,f)&&f.id===i)return c.push(f),c}else{if(e[2])return o.apply(c,n.call(b.getElementsByTagName(a),0)),c;if((i=e[3])&&Y&&b.getElementsByClassName)return o.apply(c,n.call(b.getElementsByClassName(i),0)),c}return bm(a,b,c,d,g)},$=Z.selectors={cacheLength:50,match:L,order:["ID","TAG"],attrHandle:{},createPseudo:Q,find:{ID:g?function(a,b,c){if(typeof b.getElementById!==j&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==j&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==j&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:W?function(a,b){if(typeof b.getElementsByTagName!==j)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(K,""),a[3]=(a[4]||a[5]||"").replace(K,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||Z.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&Z.error(a[0]),a},PSEUDO:function(a){var b,c=a[4];return L.CHILD.test(a[0])?null:(c&&(b=D.exec(c))&&b.pop()&&(a[0]=a[0].slice(0,b[0].length-c.length-1),c=b[0].slice(0,-1)),a.splice(2,3,c||a[3]),a)}},filter:{ID:g?function(a){return a=a.replace(K,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(K,""),function(b){var c=typeof b.getAttributeNode!==j&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(K,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=M[a];return b||(b=M[a]=new RegExp("(^|"+r+")"+a+"("+r+"|$)"),N.push(a),N.length>$.cacheLength&&delete M[N.shift()]),function(a){return b.test(a.className||typeof a.getAttribute!==j&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return b?function(d){var e=Z.attr(d,a),f=e+"";if(e==null)return b==="!=";switch(b){case"=":return f===c;case"!=":return f!==c;case"^=":return c&&f.indexOf(c)===0;case"*=":return c&&f.indexOf(c)>-1;case"$=":return c&&f.substr(f.length-c.length)===c;case"~=":return(" "+f+" ").indexOf(c)>-1;case"|=":return f===c||f.substr(0,c.length+1)===c+"-"}}:function(b){return Z.attr(b,a)!=null}},CHILD:function(a,b,c,d){if(a==="nth"){var e=m++;return function(a){var b,f,g=0,h=a;if(c===1&&d===0)return!0;b=a.parentNode;if(b&&(b[q]!==e||!a.sizset)){for(h=b.firstChild;h;h=h.nextSibling)if(h.nodeType===1){h.sizset=++g;if(h===a)break}b[q]=e}return f=a.sizset-d,c===0?f===0:f%c===0&&f/c>=0}}return function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b,c,d){var e=$.pseudos[a]||$.pseudos[a.toLowerCase()];return e||Z.error("unsupported pseudo: "+a),e.sizzleFilter?e(b,c,d):e}},pseudos:{not:Q(function(a,b,c){var d=bl(a.replace(A,"$1"),b,c);return function(a){return!d(a)}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!$.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},contains:Q(function(a){return function(b){return(b.textContent||b.innerText||bc(b)).indexOf(a)>-1}}),has:Q(function(a){return function(b){return Z(a,b).length>0}}),header:function(a){return I.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:R("radio"),checkbox:R("checkbox"),file:R("file"),password:R("password"),image:R("image"),submit:S("submit"),reset:S("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return J.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b,c){return c?a.slice(1):[a[0]]},last:function(a,b,c){var d=a.pop();return c?a:[d]},even:function(a,b,c){var d=[],e=c?1:0,f=a.length;for(;e<f;e=e+2)d.push(a[e]);return d},odd:function(a,b,c){var d=[],e=c?0:1,f=a.length;for(;e<f;e=e+2)d.push(a[e]);return d},lt:function(a,b,c){return c?a.slice(+b):a.slice(0,+b)},gt:function(a,b,c){return c?a.slice(0,+b+1):a.slice(+b+1)},eq:function(a,b,c){var d=a.splice(+b,1);return c?a:d}}};$.setFilters.nth=$.setFilters.eq,$.filters=$.pseudos,X||($.attrHandle={href:function(a){return a.getAttribute("href",2)},type:function(a){return a.getAttribute("type")}}),V&&($.order.push("NAME"),$.find.NAME=function(a,b){if(typeof b.getElementsByName!==j)return b.getElementsByName(a)}),Y&&($.order.splice(1,0,"CLASS"),$.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!==j&&!c)return b.getElementsByClassName(a)});try{n.call(i.childNodes,0)[0].nodeType}catch(_){n=function(a){var b,c=[];for(;b=this[a];a++)c.push(b);return c}}var ba=Z.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?b.nodeName!=="HTML":!1},bb=Z.contains=i.compareDocumentPosition?function(a,b){return!!(a.compareDocumentPosition(b)&16)}:i.contains?function(a,b){var c=a.nodeType===9?a.documentElement:a,d=b.parentNode;return a===d||!!(d&&d.nodeType===1&&c.contains&&c.contains(d))}:function(a,b){while(b=b.parentNode)if(b===a)return!0;return!1},bc=Z.getText=function(a){var b,c="",d=0,e=a.nodeType;if(e){if(e===1||e===9||e===11){if(typeof a.textContent=="string")return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=bc(a)}else if(e===3||e===4)return a.nodeValue}else for(;b=a[d];d++)c+=bc(b);return c};Z.attr=function(a,b){var c,d=ba(a);return d||(b=b.toLowerCase()),$.attrHandle[b]?$.attrHandle[b](a):U||d?a.getAttribute(b):(c=a.getAttributeNode(b),c?typeof a[b]=="boolean"?a[b]?b:null:c.specified?c.value:null:null)},Z.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},[0,0].sort(function(){return l=0}),i.compareDocumentPosition?e=function(a,b){return a===b?(k=!0,0):(!a.compareDocumentPosition||!b.compareDocumentPosition?a.compareDocumentPosition:a.compareDocumentPosition(b)&4)?-1:1}:(e=function(a,b){if(a===b)return k=!0,0;if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],g=[],h=a.parentNode,i=b.parentNode,j=h;if(h===i)return f(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)g.unshift(j),j=j.parentNode;c=e.length,d=g.length;for(var l=0;l<c&&l<d;l++)if(e[l]!==g[l])return f(e[l],g[l]);return l===c?f(a,g[l],-1):f(e[l],b,1)},f=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),Z.uniqueSort=function(a){var b,c=1;if(e){k=l,a.sort(e);if(k)for(;b=a[c];c++)b===a[c-1]&&a.splice(c--,1)}return a};var bl=Z.compile=function(a,b,c){var d,e,f,g=O[a];if(g&&g.context===b)return g;e=bg(a,b,c);for(f=0;d=e[f];f++)e[f]=bj(d,b,c);return g=O[a]=bk(e),g.context=b,g.runs=g.dirruns=0,P.push(a),P.length>$.cacheLength&&delete O[P.shift()],g};Z.matches=function(a,b){return Z(a,null,null,b)},Z.matchesSelector=function(a,b){return Z(b,null,null,[a]).length>0};var bm=function(a,b,e,f,g){a=a.replace(A,"$1");var h,i,j,k,l,m,p,q,r,s=a.match(C),t=a.match(E),u=b.nodeType;if(L.POS.test(a))return bf(a,b,e,f,s);if(f)h=n.call(f,0);else if(s&&s.length===1){if(t.length>1&&u===9&&!g&&(s=L.ID.exec(t[0]))){b=$.find.ID(s[1],b,g)[0];if(!b)return e;a=a.slice(t.shift().length)}q=(s=G.exec(t[0]))&&!s.index&&b.parentNode||b,r=t.pop(),m=r.split(":not")[0];for(j=0,k=$.order.length;j<k;j++){p=$.order[j];if(s=L[p].exec(m)){h=$.find[p]((s[1]||"").replace(K,""),q,g);if(h==null)continue;m===r&&(a=a.slice(0,a.length-r.length)+m.replace(L[p],""),a||o.apply(e,n.call(h,0)));break}}}if(a){i=bl(a,b,g),d=i.dirruns++,h==null&&(h=$.find.TAG("*",G.test(a)&&b.parentNode||b));for(j=0;l=h[j];j++)c=i.runs++,i(l,b)&&e.push(l)}return e};h.querySelectorAll&&function(){var a,b=bm,c=/'|\\/g,d=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,e=[],f=[":active"],g=i.matchesSelector||i.mozMatchesSelector||i.webkitMatchesSelector||i.oMatchesSelector||i.msMatchesSelector;T(function(a){a.innerHTML="<select><option selected></option></select>",a.querySelectorAll("[selected]").length||e.push("\\["+r+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),T(function(a){a.innerHTML="<p test=''></p>",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+r+"*(?:\"\"|'')"),a.innerHTML="<input type='hidden'>",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=e.length&&new RegExp(e.join("|")),bm=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a)))if(d.nodeType===9)try{return o.apply(f,n.call(d.querySelectorAll(a),0)),f}catch(i){}else if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){var j=d.getAttribute("id"),k=j||q,l=G.test(a)&&d.parentNode||d;j?k=k.replace(c,"\\$&"):d.setAttribute("id",k);try{return o.apply(f,n.call(l.querySelectorAll(a.replace(C,"[id='"+k+"'] $&")),0)),f}catch(i){}finally{j||d.removeAttribute("id")}}return b(a,d,f,g,h)},g&&(T(function(b){a=g.call(b,"div");try{g.call(b,"[test!='']:sizzle"),f.push($.match.PSEUDO)}catch(c){}}),f=new RegExp(f.join("|")),Z.matchesSelector=function(b,c){c=c.replace(d,"='$1']");if(!ba(b)&&!f.test(c)&&(!e||!e.test(c)))try{var h=g.call(b,c);if(h||a||b.document&&b.document.nodeType!==11)return h}catch(i){}return Z(c,null,null,[b]).length>0})}(),Z.attr=p.attr,p.find=Z,p.expr=Z.selectors,p.expr[":"]=p.expr.pseudos,p.unique=Z.uniqueSort,p.text=Z.getText,p.isXMLDoc=Z.isXML,p.contains=Z.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b<c;b++)if(p.contains(h[b],this))return!0});g=this.pushStack("","find",a);for(b=0,c=this.length;b<c;b++){d=g.length,p.find(a,this[b],g);if(b>0)for(e=d;e<g.length;e++)for(f=0;f<d;f++)if(g[f]===g[e]){g.splice(e--,1);break}}return g},has:function(a){var b,c=p(a,this),d=c.length;return this.filter(function(){for(b=0;b<d;b++)if(p.contains(this,c[b]))return!0})},not:function(a){return this.pushStack(bj(this,a,!1),"not",a)},filter:function(a){return this.pushStack(bj(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?bf.test(a)?p(a,this.context).index(this[0])>=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d<e;d++){c=this[d];while(c&&c.ownerDocument&&c!==b&&c.nodeType!==11){if(g?g.index(c)>-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/<tbody/i,br=/<|&#?\w+;/,bs=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,bu=new RegExp("<(?:"+bl+")[\\s/>]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,bz={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X<div>","</div>"]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1></$2>");try{for(;d<e;d++)c=this[d]||{},c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),c.innerHTML=a);c=0}catch(f){}}c&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(a){return bh(this[0])?this.length?this.pushStack(p(p.isFunction(a)?a():a),"replaceWith",a):this:p.isFunction(a)?this.each(function(b){var c=p(this),d=c.html();c.replaceWith(a.call(this,b,d))}):(typeof a!="string"&&(a=p(a).detach()),this.each(function(){var b=this.nextSibling,c=this.parentNode;p(this).remove(),b?p(b).before(a):p(c).append(a)}))},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){a=[].concat.apply([],a);var e,f,g,h,i=0,j=a[0],k=[],l=this.length;if(!p.support.checkClone&&l>1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i<l;i++)d.call(c&&p.nodeName(this[i],"table")?bC(this[i],"tbody"):this[i],i===h?g:p.clone(g,!0,!0))}g=f=null,k.length&&p.each(k,function(a,b){b.src?p.ajax?p.ajax({url:b.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):p.error("no ajax"):p.globalEval((b.text||b.textContent||b.innerHTML||"").replace(by,"")),b.parentNode&&b.parentNode.removeChild(b)})}return this}}),p.buildFragment=function(a,c,d){var f,g,h,i=a[0];return c=c||e,c=(c[0]||c).ownerDocument||c[0]||c,typeof c.createDocumentFragment=="undefined"&&(c=e),a.length===1&&typeof i=="string"&&i.length<512&&c===e&&i.charAt(0)==="<"&&!bt.test(i)&&(p.support.checkClone||!bw.test(i))&&(p.support.html5Clone||!bu.test(i))&&(g=!0,f=p.fragments[i],h=f!==b),f||(f=c.createDocumentFragment(),p.clean(a,c,f,d),g&&(p.fragments[i]=h&&f)),{fragment:f,cacheable:g}},p.fragments={},p.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){p.fn[a]=function(c){var d,e=0,f=[],g=p(c),h=g.length,i=this.length===1&&this[0].parentNode;if((i==null||i&&i.nodeType===11&&i.childNodes.length===1)&&h===1)return g[b](this[0]),this;for(;e<h;e++)d=(e>0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=0,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(g=b===e&&bA;(h=a[s])!=null;s++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{g=g||bk(b),l=l||g.appendChild(b.createElement("div")),h=h.replace(bo,"<$1></$2>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]==="<table>"&&!m?l.childNodes:[];for(f=n.length-1;f>=0;--f)p.nodeName(n[f],"tbody")&&!n[f].childNodes.length&&n[f].parentNode.removeChild(n[f])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l=g.lastChild}h.nodeType?t.push(h):t=p.merge(t,h)}l&&(g.removeChild(l),h=l=g=null);if(!p.support.appendChecked)for(s=0;(h=t[s])!=null;s++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(s=0;(h=t[s])!=null;s++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[s+1,0].concat(r)),s+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^margin/,bO=new RegExp("^("+q+")(.*)$","i"),bP=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bQ=new RegExp("^([-+])=("+q+")","i"),bR={},bS={position:"absolute",visibility:"hidden",display:"block"},bT={letterSpacing:0,fontWeight:400,lineHeight:1},bU=["Top","Right","Bottom","Left"],bV=["Webkit","O","Moz","ms"],bW=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return bZ(this,!0)},hide:function(){return bZ(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bW.apply(this,arguments):this.each(function(){(c?a:bY(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bX(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bQ.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bX(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bT&&(f=bT[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(a,b){var c,d,e,f,g=getComputedStyle(a,null),h=a.style;return g&&(c=g[b],c===""&&!p.contains(a.ownerDocument.documentElement,a)&&(c=p.style(a,b)),bP.test(c)&&bN.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=c,c=g.width,h.width=d,h.minWidth=e,h.maxWidth=f)),c}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bP.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0||bH(a,"display")!=="none"?ca(a,b,d):p.swap(a,bS,function(){return ca(a,b,d)})},set:function(a,c,d){return b$(a,c,d?b_(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bP.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bU[d]+b]=e[d]||e[d-2]||e[0];return f}},bN.test(a)||(p.cssHooks[a+b].set=b$)});var cc=/%20/g,cd=/\[\]$/,ce=/\r?\n/g,cf=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,cg=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||cg.test(this.nodeName)||cf.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(ce,"\r\n")}}):{name:b.name,value:c.replace(ce,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ch(d,a[d],c,f);return e.join("&").replace(cc,"+")};var ci,cj,ck=/#.*$/,cl=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cm=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,cn=/^(?:GET|HEAD)$/,co=/^\/\//,cp=/\?/,cq=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,cr=/([?&])_=[^&]*/,cs=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,ct=p.fn.load,cu={},cv={},cw=["*/"]+["*"];try{ci=f.href}catch(cx){ci=e.createElement("a"),ci.href="",ci=ci.href}cj=cs.exec(ci.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&ct)return ct.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("<div>").append(a.replace(cq,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cA(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cA(a,b),a},ajaxSettings:{url:ci,isLocal:cm.test(cj[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cw},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cy(cu),ajaxTransport:cy(cv),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cB(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cC(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=""+(c||y),k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cl.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(ck,"").replace(co,cj[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=cs.exec(l.url.toLowerCase()),l.crossDomain=!(!i||i[1]==cj[1]&&i[2]==cj[2]&&(i[3]||(i[1]==="http:"?80:443))==(cj[3]||(cj[1]==="http:"?80:443)))),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cz(cu,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!cn.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cp.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cr,"$1_="+z);l.url=A+(A===l.url?(cp.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cw+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cz(cv,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cD=[],cE=/\?/,cF=/(=)\?(?=&|$)|\?\?/,cG=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cD.pop()||p.expando+"_"+cG++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cF.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cF.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cF,"$1"+f):m?c.data=i.replace(cF,"$1"+f):k&&(c.url+=(cE.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cD.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cH,cI=a.ActiveXObject?function(){for(var a in cH)cH[a](0,1)}:!1,cJ=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cK()||cL()}:cK,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cI&&delete cH[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cJ,cI&&(cH||(cH={},p(a).unload(cI)),cH[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cM,cN,cO=/^(?:toggle|show|hide)$/,cP=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cQ=/queueHooks$/,cR=[cX],cS={"*":[function(a,b){var c,d,e,f=this.createTween(a,b),g=cP.exec(b),h=f.cur(),i=+h||0,j=1;if(g){c=+g[2],d=g[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&i){i=p.css(f.elem,a,!0)||c||1;do e=j=j||".5",i=i/j,p.style(f.elem,a,i+d),j=f.cur()/h;while(j!==1&&j!==e)}f.unit=d,f.start=i,f.end=g[1]?i+(g[1]+1)*c:c}return f}]};p.Animation=p.extend(cV,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d<e;d++)c=a[d],cS[c]=cS[c]||[],cS[c].unshift(b)},prefilter:function(a,b){b?cR.unshift(a):cR.push(a)}}),p.Tween=cY,cY.prototype={constructor:cY,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(p.cssNumber[c]?"":"px")},cur:function(){var a=cY.propHooks[this.prop];return a&&a.get?a.get(this):cY.propHooks._default.get(this)},run:function(a){var b,c=cY.propHooks[this.prop];return this.pos=b=p.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration),this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):cY.propHooks._default.set(this),this}},cY.prototype.init.prototype=cY.prototype,cY.propHooks={_default:{get:function(a){var b;return a.elem[a.prop]==null||!!a.elem.style&&a.elem.style[a.prop]!=null?(b=p.css(a.elem,a.prop,!1,""),!b||b==="auto"?0:b):a.elem[a.prop]},set:function(a){p.fx.step[a.prop]?p.fx.step[a.prop](a):a.elem.style&&(a.elem.style[p.cssProps[a.prop]]!=null||p.cssHooks[a.prop])?p.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},cY.propHooks.scrollTop=cY.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},p.each(["toggle","show","hide"],function(a,b){var c=p.fn[b];p.fn[b]=function(d,e,f){return d==null||typeof d=="boolean"||!a&&p.isFunction(d)&&p.isFunction(e)?c.apply(this,arguments):this.animate(cZ(b,!0),d,e,f)}}),p.fn.extend({fadeTo:function(a,b,c,d){return this.filter(bY).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=p.isEmptyObject(a),f=p.speed(b,c,d),g=function(){var b=cV(this,p.extend({},a),f);e&&b.stop(!0)};return e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,c,d){var e=function(a){var b=a.stop;delete a.stop,b(d)};return typeof a!="string"&&(d=c,c=a,a=b),c&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,c=a!=null&&a+"queueHooks",f=p.timers,g=p._data(this);if(c)g[c]&&g[c].stop&&e(g[c]);else for(c in g)g[c]&&g[c].stop&&cQ.test(c)&&e(g[c]);for(c=f.length;c--;)f[c].elem===this&&(a==null||f[c].queue===a)&&(f[c].anim.stop(d),b=!1,f.splice(c,1));(b||!d)&&p.dequeue(this,a)})}}),p.each({slideDown:cZ("show"),slideUp:cZ("hide"),slideToggle:cZ("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){p.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),p.speed=function(a,b,c){var d=a&&typeof a=="object"?p.extend({},a):{complete:c||!c&&b||p.isFunction(a)&&a,duration:a,easing:c&&b||b&&!p.isFunction(b)&&b};d.duration=p.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in p.fx.speeds?p.fx.speeds[d.duration]:p.fx.speeds._default;if(d.queue==null||d.queue===!0)d.queue="fx";return d.old=d.complete,d.complete=function(){p.isFunction(d.old)&&d.old.call(this),d.queue&&p.dequeue(this,d.queue)},d},p.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},p.timers=[],p.fx=cY.prototype.init,p.fx.tick=function(){var a,b=p.timers,c=0;for(;c<b.length;c++)a=b[c],!a()&&b[c]===a&&b.splice(c--,1);b.length||p.fx.stop()},p.fx.timer=function(a){a()&&p.timers.push(a)&&!cN&&(cN=setInterval(p.fx.tick,p.fx.interval))},p.fx.interval=13,p.fx.stop=function(){clearInterval(cN),cN=null},p.fx.speeds={slow:600,fast:200,_default:400},p.fx.step={},p.expr&&p.expr.filters&&(p.expr.filters.animated=function(a){return p.grep(p.timers,function(b){return a===b.elem}).length});var c$=/^(?:body|html)$/i;p.fn.offset=function(a){if(arguments.length)return a===b?this:this.each(function(b){p.offset.setOffset(this,a,b)});var c,d,e,f,g,h,i,j,k,l,m=this[0],n=m&&m.ownerDocument;if(!n)return;return(e=n.body)===m?p.offset.bodyOffset(m):(d=n.documentElement,p.contains(d,m)?(c=m.getBoundingClientRect(),f=c_(n),g=d.clientTop||e.clientTop||0,h=d.clientLeft||e.clientLeft||0,i=f.pageYOffset||d.scrollTop,j=f.pageXOffset||d.scrollLeft,k=c.top+i-g,l=c.left+j-h,{top:k,left:l}):{top:0,left:0})},p.offset={bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;return p.support.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(p.css(a,"marginTop"))||0,c+=parseFloat(p.css(a,"marginLeft"))||0),{top:b,left:c}},setOffset:function(a,b,c){var d=p.css(a,"position");d==="static"&&(a.style.position="relative");var e=p(a),f=e.offset(),g=p.css(a,"top"),h=p.css(a,"left"),i=(d==="absolute"||d==="fixed")&&p.inArray("auto",[g,h])>-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c$.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c$.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=c_(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window);
\ No newline at end of file
diff --git a/server-webapp/src/main/resources/static/jquery-ui-1.8.23.custom.css b/server-webapp/src/main/resources/static/jquery-ui-1.8.23.custom.css
new file mode 100644
index 0000000..8b8afac
--- /dev/null
+++ b/server-webapp/src/main/resources/static/jquery-ui-1.8.23.custom.css
@@ -0,0 +1,563 @@
+/*!
+ * jQuery UI CSS Framework 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ */
+
+/* Layout helpers
+----------------------------------*/
+.ui-helper-hidden { display: none; }
+.ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); }
+.ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; }
+.ui-helper-clearfix:before, .ui-helper-clearfix:after { content: ""; display: table; }
+.ui-helper-clearfix:after { clear: both; }
+.ui-helper-clearfix { zoom: 1; }
+.ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); }
+
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-disabled { cursor: default !important; }
+
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Overlays */
+.ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
+
+
+/*!
+ * jQuery UI CSS Framework 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Theming/API
+ *
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=segoe%20ui,%20Arial,%20sans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=3px&bgColorHeader=f9f9f9&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=100&borderColorHeader=cccccc&fcHeader=e69700&iconColorHeader=5fa5e3&bgColorContent=eeeeee&bgTextureContent=06_inset_hard.png&bgImgOpacityContent=100&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=0a82eb&bgColorDefault=1484e6&bgTextureDefault=08_diagonals_thick.png&bgImgOpacityDefault=22&borderColorDefault=ffffff&fcDefault=ffffff&iconColorDefault=fcdd4a&bgColorHover=2293f7&bgTextureHover=08_diagonals_thick.png&bgImgOpacityHover=26&borderColorHover=2293f7&fcHover=ffffff&iconColorHover=ffffff&bgColorActive=e69700&bgTextureActive=08_diagonals_thick.png&bgImgOpacityActive=20&borderColorActive=e69700&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=c5ddfc&bgTextureHighlight=07_diagonals_small.png&bgImgOpacityHighlight=25&borderColorHighlight=ffffff&fcHighlight=333333&iconColorHighlight=0b54d5&bgColorError=e69700&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=20&borderColorError=e69700&fcError=ffffff&iconColorError=ffffff&bgColorOverlay=e6b900&bgTextureOverlay=01_flat.png&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=e69700&bgTextureShadow=01_flat.png&bgImgOpacityShadow=0&opacityShadow=20&thicknessShadow=0px&offsetTopShadow=6px&offsetLeftShadow=6px&cornerRadiusShadow=3px
+ */
+
+
+/* Component containers
+----------------------------------*/
+.ui-widget { font-family: segoe ui, Arial, sans-serif; font-size: 1.1em; }
+.ui-widget .ui-widget { font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: segoe ui, Arial, sans-serif; font-size: 1em; }
+.ui-widget-content { border: 1px solid #aaaaaa; background: #eeeeee url(ui-bg_inset-hard_100_eeeeee_1x100.png) 50% bottom repeat-x; color: #222222; }
+.ui-widget-content a { color: #222222; }
+.ui-widget-header { border: 1px solid #cccccc; background: #f9f9f9 url(ui-bg_highlight-soft_100_f9f9f9_1x100.png) 50% 50% repeat-x; color: #e69700; font-weight: bold; }
+.ui-widget-header a { color: #e69700; }
+
+/* Interaction states
+----------------------------------*/
+.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #ffffff; background: #1484e6 url(ui-bg_diagonals-thick_22_1484e6_40x40.png) 50% 50% repeat; font-weight: bold; color: #ffffff; }
+.ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #ffffff; text-decoration: none; }
+.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #2293f7; background: #2293f7 url(ui-bg_diagonals-thick_26_2293f7_40x40.png) 50% 50% repeat; font-weight: bold; color: #ffffff; }
+.ui-state-hover a, .ui-state-hover a:hover { color: #ffffff; text-decoration: none; }
+.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #e69700; background: #e69700 url(ui-bg_diagonals-thick_20_e69700_40x40.png) 50% 50% repeat; font-weight: bold; color: #ffffff; }
+.ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #ffffff; text-decoration: none; }
+.ui-widget :active { outline: none; }
+
+/* Interaction Cues
+----------------------------------*/
+.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight  {border: 1px solid #ffffff; background: #c5ddfc url(ui-bg_diagonals-small_25_c5ddfc_40x40.png) 50% 50% repeat; color: #333333; }
+.ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #333333; }
+.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #e69700; background: #e69700 url(ui-bg_diagonals-thick_20_e69700_40x40.png) 50% 50% repeat; color: #ffffff; }
+.ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; }
+.ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; }
+.ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; }
+.ui-priority-secondary, .ui-widget-content .ui-priority-secondary,  .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; }
+.ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; }
+
+/* Icons
+----------------------------------*/
+
+/* states and images */
+.ui-icon { width: 16px; height: 16px; background-image: url(ui-icons_0a82eb_256x240.png); }
+.ui-widget-content .ui-icon {background-image: url(ui-icons_0a82eb_256x240.png); }
+.ui-widget-header .ui-icon {background-image: url(ui-icons_5fa5e3_256x240.png); }
+.ui-state-default .ui-icon { background-image: url(ui-icons_fcdd4a_256x240.png); }
+.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(ui-icons_ffffff_256x240.png); }
+.ui-state-active .ui-icon {background-image: url(ui-icons_ffffff_256x240.png); }
+.ui-state-highlight .ui-icon {background-image: url(ui-icons_0b54d5_256x240.png); }
+.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(ui-icons_ffffff_256x240.png); }
+
+/* positioning */
+.ui-icon-carat-1-n { background-position: 0 0; }
+.ui-icon-carat-1-ne { background-position: -16px 0; }
+.ui-icon-carat-1-e { background-position: -32px 0; }
+.ui-icon-carat-1-se { background-position: -48px 0; }
+.ui-icon-carat-1-s { background-position: -64px 0; }
+.ui-icon-carat-1-sw { background-position: -80px 0; }
+.ui-icon-carat-1-w { background-position: -96px 0; }
+.ui-icon-carat-1-nw { background-position: -112px 0; }
+.ui-icon-carat-2-n-s { background-position: -128px 0; }
+.ui-icon-carat-2-e-w { background-position: -144px 0; }
+.ui-icon-triangle-1-n { background-position: 0 -16px; }
+.ui-icon-triangle-1-ne { background-position: -16px -16px; }
+.ui-icon-triangle-1-e { background-position: -32px -16px; }
+.ui-icon-triangle-1-se { background-position: -48px -16px; }
+.ui-icon-triangle-1-s { background-position: -64px -16px; }
+.ui-icon-triangle-1-sw { background-position: -80px -16px; }
+.ui-icon-triangle-1-w { background-position: -96px -16px; }
+.ui-icon-triangle-1-nw { background-position: -112px -16px; }
+.ui-icon-triangle-2-n-s { background-position: -128px -16px; }
+.ui-icon-triangle-2-e-w { background-position: -144px -16px; }
+.ui-icon-arrow-1-n { background-position: 0 -32px; }
+.ui-icon-arrow-1-ne { background-position: -16px -32px; }
+.ui-icon-arrow-1-e { background-position: -32px -32px; }
+.ui-icon-arrow-1-se { background-position: -48px -32px; }
+.ui-icon-arrow-1-s { background-position: -64px -32px; }
+.ui-icon-arrow-1-sw { background-position: -80px -32px; }
+.ui-icon-arrow-1-w { background-position: -96px -32px; }
+.ui-icon-arrow-1-nw { background-position: -112px -32px; }
+.ui-icon-arrow-2-n-s { background-position: -128px -32px; }
+.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; }
+.ui-icon-arrow-2-e-w { background-position: -160px -32px; }
+.ui-icon-arrow-2-se-nw { background-position: -176px -32px; }
+.ui-icon-arrowstop-1-n { background-position: -192px -32px; }
+.ui-icon-arrowstop-1-e { background-position: -208px -32px; }
+.ui-icon-arrowstop-1-s { background-position: -224px -32px; }
+.ui-icon-arrowstop-1-w { background-position: -240px -32px; }
+.ui-icon-arrowthick-1-n { background-position: 0 -48px; }
+.ui-icon-arrowthick-1-ne { background-position: -16px -48px; }
+.ui-icon-arrowthick-1-e { background-position: -32px -48px; }
+.ui-icon-arrowthick-1-se { background-position: -48px -48px; }
+.ui-icon-arrowthick-1-s { background-position: -64px -48px; }
+.ui-icon-arrowthick-1-sw { background-position: -80px -48px; }
+.ui-icon-arrowthick-1-w { background-position: -96px -48px; }
+.ui-icon-arrowthick-1-nw { background-position: -112px -48px; }
+.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; }
+.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; }
+.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; }
+.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; }
+.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; }
+.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; }
+.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; }
+.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; }
+.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; }
+.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; }
+.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; }
+.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; }
+.ui-icon-arrowreturn-1-w { background-position: -64px -64px; }
+.ui-icon-arrowreturn-1-n { background-position: -80px -64px; }
+.ui-icon-arrowreturn-1-e { background-position: -96px -64px; }
+.ui-icon-arrowreturn-1-s { background-position: -112px -64px; }
+.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; }
+.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; }
+.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; }
+.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; }
+.ui-icon-arrow-4 { background-position: 0 -80px; }
+.ui-icon-arrow-4-diag { background-position: -16px -80px; }
+.ui-icon-extlink { background-position: -32px -80px; }
+.ui-icon-newwin { background-position: -48px -80px; }
+.ui-icon-refresh { background-position: -64px -80px; }
+.ui-icon-shuffle { background-position: -80px -80px; }
+.ui-icon-transfer-e-w { background-position: -96px -80px; }
+.ui-icon-transferthick-e-w { background-position: -112px -80px; }
+.ui-icon-folder-collapsed { background-position: 0 -96px; }
+.ui-icon-folder-open { background-position: -16px -96px; }
+.ui-icon-document { background-position: -32px -96px; }
+.ui-icon-document-b { background-position: -48px -96px; }
+.ui-icon-note { background-position: -64px -96px; }
+.ui-icon-mail-closed { background-position: -80px -96px; }
+.ui-icon-mail-open { background-position: -96px -96px; }
+.ui-icon-suitcase { background-position: -112px -96px; }
+.ui-icon-comment { background-position: -128px -96px; }
+.ui-icon-person { background-position: -144px -96px; }
+.ui-icon-print { background-position: -160px -96px; }
+.ui-icon-trash { background-position: -176px -96px; }
+.ui-icon-locked { background-position: -192px -96px; }
+.ui-icon-unlocked { background-position: -208px -96px; }
+.ui-icon-bookmark { background-position: -224px -96px; }
+.ui-icon-tag { background-position: -240px -96px; }
+.ui-icon-home { background-position: 0 -112px; }
+.ui-icon-flag { background-position: -16px -112px; }
+.ui-icon-calendar { background-position: -32px -112px; }
+.ui-icon-cart { background-position: -48px -112px; }
+.ui-icon-pencil { background-position: -64px -112px; }
+.ui-icon-clock { background-position: -80px -112px; }
+.ui-icon-disk { background-position: -96px -112px; }
+.ui-icon-calculator { background-position: -112px -112px; }
+.ui-icon-zoomin { background-position: -128px -112px; }
+.ui-icon-zoomout { background-position: -144px -112px; }
+.ui-icon-search { background-position: -160px -112px; }
+.ui-icon-wrench { background-position: -176px -112px; }
+.ui-icon-gear { background-position: -192px -112px; }
+.ui-icon-heart { background-position: -208px -112px; }
+.ui-icon-star { background-position: -224px -112px; }
+.ui-icon-link { background-position: -240px -112px; }
+.ui-icon-cancel { background-position: 0 -128px; }
+.ui-icon-plus { background-position: -16px -128px; }
+.ui-icon-plusthick { background-position: -32px -128px; }
+.ui-icon-minus { background-position: -48px -128px; }
+.ui-icon-minusthick { background-position: -64px -128px; }
+.ui-icon-close { background-position: -80px -128px; }
+.ui-icon-closethick { background-position: -96px -128px; }
+.ui-icon-key { background-position: -112px -128px; }
+.ui-icon-lightbulb { background-position: -128px -128px; }
+.ui-icon-scissors { background-position: -144px -128px; }
+.ui-icon-clipboard { background-position: -160px -128px; }
+.ui-icon-copy { background-position: -176px -128px; }
+.ui-icon-contact { background-position: -192px -128px; }
+.ui-icon-image { background-position: -208px -128px; }
+.ui-icon-video { background-position: -224px -128px; }
+.ui-icon-script { background-position: -240px -128px; }
+.ui-icon-alert { background-position: 0 -144px; }
+.ui-icon-info { background-position: -16px -144px; }
+.ui-icon-notice { background-position: -32px -144px; }
+.ui-icon-help { background-position: -48px -144px; }
+.ui-icon-check { background-position: -64px -144px; }
+.ui-icon-bullet { background-position: -80px -144px; }
+.ui-icon-radio-off { background-position: -96px -144px; }
+.ui-icon-radio-on { background-position: -112px -144px; }
+.ui-icon-pin-w { background-position: -128px -144px; }
+.ui-icon-pin-s { background-position: -144px -144px; }
+.ui-icon-play { background-position: 0 -160px; }
+.ui-icon-pause { background-position: -16px -160px; }
+.ui-icon-seek-next { background-position: -32px -160px; }
+.ui-icon-seek-prev { background-position: -48px -160px; }
+.ui-icon-seek-end { background-position: -64px -160px; }
+.ui-icon-seek-start { background-position: -80px -160px; }
+/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */
+.ui-icon-seek-first { background-position: -80px -160px; }
+.ui-icon-stop { background-position: -96px -160px; }
+.ui-icon-eject { background-position: -112px -160px; }
+.ui-icon-volume-off { background-position: -128px -160px; }
+.ui-icon-volume-on { background-position: -144px -160px; }
+.ui-icon-power { background-position: 0 -176px; }
+.ui-icon-signal-diag { background-position: -16px -176px; }
+.ui-icon-signal { background-position: -32px -176px; }
+.ui-icon-battery-0 { background-position: -48px -176px; }
+.ui-icon-battery-1 { background-position: -64px -176px; }
+.ui-icon-battery-2 { background-position: -80px -176px; }
+.ui-icon-battery-3 { background-position: -96px -176px; }
+.ui-icon-circle-plus { background-position: 0 -192px; }
+.ui-icon-circle-minus { background-position: -16px -192px; }
+.ui-icon-circle-close { background-position: -32px -192px; }
+.ui-icon-circle-triangle-e { background-position: -48px -192px; }
+.ui-icon-circle-triangle-s { background-position: -64px -192px; }
+.ui-icon-circle-triangle-w { background-position: -80px -192px; }
+.ui-icon-circle-triangle-n { background-position: -96px -192px; }
+.ui-icon-circle-arrow-e { background-position: -112px -192px; }
+.ui-icon-circle-arrow-s { background-position: -128px -192px; }
+.ui-icon-circle-arrow-w { background-position: -144px -192px; }
+.ui-icon-circle-arrow-n { background-position: -160px -192px; }
+.ui-icon-circle-zoomin { background-position: -176px -192px; }
+.ui-icon-circle-zoomout { background-position: -192px -192px; }
+.ui-icon-circle-check { background-position: -208px -192px; }
+.ui-icon-circlesmall-plus { background-position: 0 -208px; }
+.ui-icon-circlesmall-minus { background-position: -16px -208px; }
+.ui-icon-circlesmall-close { background-position: -32px -208px; }
+.ui-icon-squaresmall-plus { background-position: -48px -208px; }
+.ui-icon-squaresmall-minus { background-position: -64px -208px; }
+.ui-icon-squaresmall-close { background-position: -80px -208px; }
+.ui-icon-grip-dotted-vertical { background-position: 0 -224px; }
+.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; }
+.ui-icon-grip-solid-vertical { background-position: -32px -224px; }
+.ui-icon-grip-solid-horizontal { background-position: -48px -224px; }
+.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; }
+.ui-icon-grip-diagonal-se { background-position: -80px -224px; }
+
+
+/* Misc visuals
+----------------------------------*/
+
+/* Corner radius */
+.ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; -khtml-border-top-left-radius: 3px; border-top-left-radius: 3px; }
+.ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; -khtml-border-top-right-radius: 3px; border-top-right-radius: 3px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; -khtml-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; }
+.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 3px; -webkit-border-bottom-right-radius: 3px; -khtml-border-bottom-right-radius: 3px; border-bottom-right-radius: 3px; }
+
+/* Overlays */
+.ui-widget-overlay { background: #e6b900 url(ui-bg_flat_0_e6b900_40x100.png) 50% 50% repeat-x; opacity: .30;filter:Alpha(Opacity=30); }
+.ui-widget-shadow { margin: 6px 0 0 6px; padding: 0px; background: #e69700 url(ui-bg_flat_0_e69700_40x100.png) 50% 50% repeat-x; opacity: .20;filter:Alpha(Opacity=20); -moz-border-radius: 3px; -khtml-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; }/*!
+ * jQuery UI Resizable 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Resizable#theming
+ */
+.ui-resizable { position: relative;}
+.ui-resizable-handle { position: absolute;font-size: 0.1px; display: block; }
+.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
+.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; }
+.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; }
+.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; }
+.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; }
+.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
+.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
+.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
+.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/*!
+ * jQuery UI Selectable 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Selectable#theming
+ */
+.ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; }
+/*!
+ * jQuery UI Accordion 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Accordion#theming
+ */
+/* IE/Win - Fix animation bug - #4615 */
+.ui-accordion { width: 100%; }
+.ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; }
+.ui-accordion .ui-accordion-li-fix { display: inline; }
+.ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; }
+.ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; }
+.ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; }
+.ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; }
+.ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; }
+.ui-accordion .ui-accordion-content-active { display: block; }
+/*!
+ * jQuery UI Autocomplete 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Autocomplete#theming
+ */
+.ui-autocomplete { position: absolute; cursor: default; }	
+
+/* workarounds */
+* html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
+
+/*
+ * jQuery UI Menu 1.8.23
+ *
+ * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Menu#theming
+ */
+.ui-menu {
+	list-style:none;
+	padding: 2px;
+	margin: 0;
+	display:block;
+	float: left;
+}
+.ui-menu .ui-menu {
+	margin-top: -3px;
+}
+.ui-menu .ui-menu-item {
+	margin:0;
+	padding: 0;
+	zoom: 1;
+	float: left;
+	clear: left;
+	width: 100%;
+}
+.ui-menu .ui-menu-item a {
+	text-decoration:none;
+	display:block;
+	padding:.2em .4em;
+	line-height:1.5;
+	zoom:1;
+}
+.ui-menu .ui-menu-item a.ui-state-hover,
+.ui-menu .ui-menu-item a.ui-state-active {
+	font-weight: normal;
+	margin: -1px;
+}
+/*!
+ * jQuery UI Button 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Button#theming
+ */
+.ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
+.ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
+button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
+.ui-button-icons-only { width: 3.4em; } 
+button.ui-button-icons-only { width: 3.7em; } 
+
+/*button text element */
+.ui-button .ui-button-text { display: block; line-height: 1.4;  }
+.ui-button-text-only .ui-button-text { padding: .4em 1em; }
+.ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; }
+.ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; }
+.ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; }
+.ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
+/* no icon support for input elements, provide padding by default */
+input.ui-button { padding: .4em 1em; }
+
+/*button icon element(s) */
+.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; }
+.ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; }
+.ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; }
+.ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+.ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; }
+
+/*button sets*/
+.ui-buttonset { margin-right: 7px; }
+.ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; }
+
+/* workarounds */
+button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
+/*!
+ * jQuery UI Dialog 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Dialog#theming
+ */
+.ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; }
+.ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative;  }
+.ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } 
+.ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; }
+.ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; }
+.ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; }
+.ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; }
+.ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; }
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; }
+.ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; }
+.ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; }
+.ui-draggable .ui-dialog-titlebar { cursor: move; }
+/*!
+ * jQuery UI Slider 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Slider#theming
+ */
+.ui-slider { position: relative; text-align: left; }
+.ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; }
+.ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; }
+
+.ui-slider-horizontal { height: .8em; }
+.ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; }
+.ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; }
+.ui-slider-horizontal .ui-slider-range-min { left: 0; }
+.ui-slider-horizontal .ui-slider-range-max { right: 0; }
+
+.ui-slider-vertical { width: .8em; height: 100px; }
+.ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; }
+.ui-slider-vertical .ui-slider-range { left: 0; width: 100%; }
+.ui-slider-vertical .ui-slider-range-min { bottom: 0; }
+.ui-slider-vertical .ui-slider-range-max { top: 0; }/*!
+ * jQuery UI Tabs 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Tabs#theming
+ */
+.ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */
+.ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; }
+.ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; }
+.ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; }
+.ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; }
+.ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */
+.ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; }
+.ui-tabs .ui-tabs-hide { display: none !important; }
+/*!
+ * jQuery UI Datepicker 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Datepicker#theming
+ */
+.ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; }
+.ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; }
+.ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; }
+.ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; }
+.ui-datepicker .ui-datepicker-prev { left:2px; }
+.ui-datepicker .ui-datepicker-next { right:2px; }
+.ui-datepicker .ui-datepicker-prev-hover { left:1px; }
+.ui-datepicker .ui-datepicker-next-hover { right:1px; }
+.ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px;  }
+.ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; }
+.ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; }
+.ui-datepicker select.ui-datepicker-month-year {width: 100%;}
+.ui-datepicker select.ui-datepicker-month, 
+.ui-datepicker select.ui-datepicker-year { width: 49%;}
+.ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; }
+.ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0;  }
+.ui-datepicker td { border: 0; padding: 1px; }
+.ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; }
+.ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; }
+.ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
+.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; }
+
+/* with multiple calendars */
+.ui-datepicker.ui-datepicker-multi { width:auto; }
+.ui-datepicker-multi .ui-datepicker-group { float:left; }
+.ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; }
+.ui-datepicker-multi-2 .ui-datepicker-group { width:50%; }
+.ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; }
+.ui-datepicker-multi-4 .ui-datepicker-group { width:25%; }
+.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; }
+.ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; }
+.ui-datepicker-row-break { clear:both; width:100%; font-size:0em; }
+
+/* RTL support */
+.ui-datepicker-rtl { direction: rtl; }
+.ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; }
+.ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; }
+.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group { float:right; }
+.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; }
+
+/* IE6 IFRAME FIX (taken from datepicker 1.5.3 */
+.ui-datepicker-cover {
+    position: absolute; /*must have*/
+    z-index: -1; /*must have*/
+    filter: mask(); /*must have*/
+    top: -4px; /*must have*/
+    left: -4px; /*must have*/
+    width: 200px; /*must have*/
+    height: 200px; /*must have*/
+}/*!
+ * jQuery UI Progressbar 1.8.23
+ *
+ * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * http://docs.jquery.com/UI/Progressbar#theming
+ */
+.ui-progressbar { height:2em; text-align: left; overflow: hidden; }
+.ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; }
\ No newline at end of file
diff --git a/server-webapp/src/main/resources/static/jquery-ui-1.8.23.custom.min.js b/server-webapp/src/main/resources/static/jquery-ui-1.8.23.custom.min.js
new file mode 100644
index 0000000..7835454
--- /dev/null
+++ b/server-webapp/src/main/resources/static/jquery-ui-1.8.23.custom.min.js
@@ -0,0 +1,125 @@
+/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.core.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){function c(b,c){var e=b.nodeName.toLowerCase();if("area"===e){var f=b.parentNode,g=f.name,h;return!b.href||!g||f.nodeName.toLowerCase()!=="map"?!1:(h=a("img[usemap=#"+g+"]")[0],!!h&&d(h))}return(/input|select|textarea|button|object/.test(e)?!b.disabled:"a"==e?b.href||c:c)&&d(b)}function d(b){return!a(b).parents().andSelf().filter(function(){return a.curCSS(this,"visibility")==="hidden"||a.expr.filters.hidden(this)}).length}a.ui=a.ui||{};if(a.ui.version)return;a.extend(a.ui,{version:"1.8.23",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}}),a.fn.extend({propAttr:a.fn.prop||a.fn.attr,_focus:a.fn.focus,focus:function(b,c){return typeof b=="number"?this.each(function(){var d=this;setTimeout(function(){a(d).focus(),c&&c.call(d)},b)}):this._focus.apply(this,arguments)},scrollParent:function(){var b;return a.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?b=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(a.curCSS(this,"position",1))&&/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0):b=this.parents().filter(function(){return/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0),/fixed/.test(this.css("position"))||!b.length?a(document):b},zIndex:function(c){if(c!==b)return this.css("zIndex",c);if(this.length){var d=a(this[0]),e,f;while(d.length&&d[0]!==document){e=d.css("position");if(e==="absolute"||e==="relative"||e==="fixed"){f=parseInt(d.css("zIndex"),10);if(!isNaN(f)&&f!==0)return f}d=d.parent()}}return 0},disableSelection:function(){return this.bind((a.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),a("<a>").outerWidth(1).jquery||a.each(["Width","Height"],function(c,d){function h(b,c,d,f){return a.each(e,function(){c-=parseFloat(a.curCSS(b,"padding"+this,!0))||0,d&&(c-=parseFloat(a.curCSS(b,"border"+this+"Width",!0))||0),f&&(c-=parseFloat(a.curCSS(b,"margin"+this,!0))||0)}),c}var e=d==="Width"?["Left","Right"]:["Top","Bottom"],f=d.toLowerCase(),g={innerWidth:a.fn.innerWidth,innerHeight:a.fn.innerHeight,outerWidth:a.fn.outerWidth,outerHeight:a.fn.outerHeight};a.fn["inner"+d]=function(c){return c===b?g["inner"+d].call(this):this.each(function(){a(this).css(f,h(this,c)+"px")})},a.fn["outer"+d]=function(b,c){return typeof b!="number"?g["outer"+d].call(this,b):this.each(function(){a(this).css(f,h(this,b,!0,c)+"px")})}}),a.extend(a.expr[":"],{data:a.expr.createPseudo?a.expr.createPseudo(function(b){return function(c){return!!a.data(c,b)}}):function(b,c,d){return!!a.data(b,d[3])},focusable:function(b){return c(b,!isNaN(a.attr(b,"tabindex")))},tabbable:function(b){var d=a.attr(b,"tabindex"),e=isNaN(d);return(e||d>=0)&&c(b,!e)}}),a(function(){var b=document.body,c=b.appendChild(c=document.createElement("div"));c.offsetHeight,a.extend(c.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),a.support.minHeight=c.offsetHeight===100,a.support.selectstart="onselectstart"in c,b.removeChild(c).style.display="none"}),a.curCSS||(a.curCSS=a.css),a.extend(a.ui,{plugin:{add:function(b,c,d){var e=a.ui[b].prototype;for(var f in d)e.plugins[f]=e.plugins[f]||[],e.plugins[f].push([c,d[f]])},call:function(a,b,c){var d=a.plugins[b];if(!d||!a.element[0].parentNode)return;for(var e=0;e<d.length;e++)a.options[d[e][0]]&&d[e][1].apply(a.element,c)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(b,c){if(a(b).css("overflow")==="hidden")return!1;var d=c&&c==="left"?"scrollLeft":"scrollTop",e=!1;return b[d]>0?!0:(b[d]=1,e=b[d]>0,b[d]=0,e)},isOverAxis:function(a,b,c){return a>b&&a<b+c},isOver:function(b,c,d,e,f,g){return a.ui.isOverAxis(b,d,f)&&a.ui.isOverAxis(c,e,g)}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.widget.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){if(a.cleanData){var c=a.cleanData;a.cleanData=function(b){for(var d=0,e;(e=b[d])!=null;d++)try{a(e).triggerHandler("remove")}catch(f){}c(b)}}else{var d=a.fn.remove;a.fn.remove=function(b,c){return this.each(function(){return c||(!b||a.filter(b,[this]).length)&&a("*",this).add([this]).each(function(){try{a(this).triggerHandler("remove")}catch(b){}}),d.call(a(this),b,c)})}}a.widget=function(b,c,d){var e=b.split(".")[0],f;b=b.split(".")[1],f=e+"-"+b,d||(d=c,c=a.Widget),a.expr[":"][f]=function(c){return!!a.data(c,b)},a[e]=a[e]||{},a[e][b]=function(a,b){arguments.length&&this._createWidget(a,b)};var g=new c;g.options=a.extend(!0,{},g.options),a[e][b].prototype=a.extend(!0,g,{namespace:e,widgetName:b,widgetEventPrefix:a[e][b].prototype.widgetEventPrefix||b,widgetBaseClass:f},d),a.widget.bridge(b,a[e][b])},a.widget.bridge=function(c,d){a.fn[c]=function(e){var f=typeof e=="string",g=Array.prototype.slice.call(arguments,1),h=this;return e=!f&&g.length?a.extend.apply(null,[!0,e].concat(g)):e,f&&e.charAt(0)==="_"?h:(f?this.each(function(){var d=a.data(this,c),f=d&&a.isFunction(d[e])?d[e].apply(d,g):d;if(f!==d&&f!==b)return h=f,!1}):this.each(function(){var b=a.data(this,c);b?b.option(e||{})._init():a.data(this,c,new d(e,this))}),h)}},a.Widget=function(a,b){arguments.length&&this._createWidget(a,b)},a.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",options:{disabled:!1},_createWidget:function(b,c){a.data(c,this.widgetName,this),this.element=a(c),this.options=a.extend(!0,{},this.options,this._getCreateOptions(),b);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()}),this._create(),this._trigger("create"),this._init()},_getCreateOptions:function(){return a.metadata&&a.metadata.get(this.element[0])[this.widgetName]},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName),this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+"-disabled "+"ui-state-disabled")},widget:function(){return this.element},option:function(c,d){var e=c;if(arguments.length===0)return a.extend({},this.options);if(typeof c=="string"){if(d===b)return this.options[c];e={},e[c]=d}return this._setOptions(e),this},_setOptions:function(b){var c=this;return a.each(b,function(a,b){c._setOption(a,b)}),this},_setOption:function(a,b){return this.options[a]=b,a==="disabled"&&this.widget()[b?"addClass":"removeClass"](this.widgetBaseClass+"-disabled"+" "+"ui-state-disabled").attr("aria-disabled",b),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_trigger:function(b,c,d){var e,f,g=this.options[b];d=d||{},c=a.Event(c),c.type=(b===this.widgetEventPrefix?b:this.widgetEventPrefix+b).toLowerCase(),c.target=this.element[0],f=c.originalEvent;if(f)for(e in f)e in c||(c[e]=f[e]);return this.element.trigger(c,d),!(a.isFunction(g)&&g.call(this.element[0],c,d)===!1||c.isDefaultPrevented())}}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.mouse.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){var c=!1;a(document).mouseup(function(a){c=!1}),a.widget("ui.mouse",{options:{cancel:":input,option",distance:1,delay:0},_mouseInit:function(){var b=this;this.element.bind("mousedown."+this.widgetName,function(a){return b._mouseDown(a)}).bind("click."+this.widgetName,function(c){if(!0===a.data(c.target,b.widgetName+".preventClickEvent"))return a.removeData(c.target,b.widgetName+".preventClickEvent"),c.stopImmediatePropagation(),!1}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(b){if(c)return;this._mouseStarted&&this._mouseUp(b),this._mouseDownEvent=b;var d=this,e=b.which==1,f=typeof this.options.cancel=="string"&&b.target.nodeName?a(b.target).closest(this.options.cancel).length:!1;if(!e||f||!this._mouseCapture(b))return!0;this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){d.mouseDelayMet=!0},this.options.delay));if(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)){this._mouseStarted=this._mouseStart(b)!==!1;if(!this._mouseStarted)return b.preventDefault(),!0}return!0===a.data(b.target,this.widgetName+".preventClickEvent")&&a.removeData(b.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(a){return d._mouseMove(a)},this._mouseUpDelegate=function(a){return d._mouseUp(a)},a(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),b.preventDefault(),c=!0,!0},_mouseMove:function(b){return!a.browser.msie||document.documentMode>=9||!!b.button?this._mouseStarted?(this._mouseDrag(b),b.preventDefault()):(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,b)!==!1,this._mouseStarted?this._mouseDrag(b):this._mouseUp(b)),!this._mouseStarted):this._mouseUp(b)},_mouseUp:function(b){return a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,b.target==this._mouseDownEvent.target&&a.data(b.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(b)),!1},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(a){return this.mouseDelayMet},_mouseStart:function(a){},_mouseDrag:function(a){},_mouseStop:function(a){},_mouseCapture:function(a){return!0}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.position.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.ui=a.ui||{};var c=/left|center|right/,d=/top|center|bottom/,e="center",f={},g=a.fn.position,h=a.fn.offset;a.fn.position=function(b){if(!b||!b.of)return g.apply(this,arguments);b=a.extend({},b);var h=a(b.of),i=h[0],j=(b.collision||"flip").split(" "),k=b.offset?b.offset.split(" "):[0,0],l,m,n;return i.nodeType===9?(l=h.width(),m=h.height(),n={top:0,left:0}):i.setTimeout?(l=h.width(),m=h.height(),n={top:h.scrollTop(),left:h.scrollLeft()}):i.preventDefault?(b.at="left top",l=m=0,n={top:b.of.pageY,left:b.of.pageX}):(l=h.outerWidth(),m=h.outerHeight(),n=h.offset()),a.each(["my","at"],function(){var a=(b[this]||"").split(" ");a.length===1&&(a=c.test(a[0])?a.concat([e]):d.test(a[0])?[e].concat(a):[e,e]),a[0]=c.test(a[0])?a[0]:e,a[1]=d.test(a[1])?a[1]:e,b[this]=a}),j.length===1&&(j[1]=j[0]),k[0]=parseInt(k[0],10)||0,k.length===1&&(k[1]=k[0]),k[1]=parseInt(k[1],10)||0,b.at[0]==="right"?n.left+=l:b.at[0]===e&&(n.left+=l/2),b.at[1]==="bottom"?n.top+=m:b.at[1]===e&&(n.top+=m/2),n.left+=k[0],n.top+=k[1],this.each(function(){var c=a(this),d=c.outerWidth(),g=c.outerHeight(),h=parseInt(a.curCSS(this,"marginLeft",!0))||0,i=parseInt(a.curCSS(this,"marginTop",!0))||0,o=d+h+(parseInt(a.curCSS(this,"marginRight",!0))||0),p=g+i+(parseInt(a.curCSS(this,"marginBottom",!0))||0),q=a.extend({},n),r;b.my[0]==="right"?q.left-=d:b.my[0]===e&&(q.left-=d/2),b.my[1]==="bottom"?q.top-=g:b.my[1]===e&&(q.top-=g/2),f.fractions||(q.left=Math.round(q.left),q.top=Math.round(q.top)),r={left:q.left-h,top:q.top-i},a.each(["left","top"],function(c,e){a.ui.position[j[c]]&&a.ui.position[j[c]][e](q,{targetWidth:l,targetHeight:m,elemWidth:d,elemHeight:g,collisionPosition:r,collisionWidth:o,collisionHeight:p,offset:k,my:b.my,at:b.at})}),a.fn.bgiframe&&c.bgiframe(),c.offset(a.extend(q,{using:b.using}))})},a.ui.position={fit:{left:function(b,c){var d=a(window),e=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft();b.left=e>0?b.left-e:Math.max(b.left-c.collisionPosition.left,b.left)},top:function(b,c){var d=a(window),e=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop();b.top=e>0?b.top-e:Math.max(b.top-c.collisionPosition.top,b.top)}},flip:{left:function(b,c){if(c.at[0]===e)return;var d=a(window),f=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft(),g=c.my[0]==="left"?-c.elemWidth:c.my[0]==="right"?c.elemWidth:0,h=c.at[0]==="left"?c.targetWidth:-c.targetWidth,i=-2*c.offset[0];b.left+=c.collisionPosition.left<0?g+h+i:f>0?g+h+i:0},top:function(b,c){if(c.at[1]===e)return;var d=a(window),f=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop(),g=c.my[1]==="top"?-c.elemHeight:c.my[1]==="bottom"?c.elemHeight:0,h=c.at[1]==="top"?c.targetHeight:-c.targetHeight,i=-2*c.offset[1];b.top+=c.collisionPosition.top<0?g+h+i:f>0?g+h+i:0}}},a.offset.setOffset||(a.offset.setOffset=function(b,c){/static/.test(a.curCSS(b,"position"))&&(b.style.position="relative");var d=a(b),e=d.offset(),f=parseInt(a.curCSS(b,"top",!0),10)||0,g=parseInt(a.curCSS(b,"left",!0),10)||0,h={top:c.top-e.top+f,left:c.left-e.left+g};"using"in c?c.using.call(b,h):d.css(h)},a.fn.offset=function(b){var c=this[0];return!c||!c.ownerDocument?null:b?a.isFunction(b)?this.each(function(c){a(this).offset(b.call(this,c,a(this).offset()))}):this.each(function(){a.offset.setOffset(this,b)}):h.call(this)}),a.curCSS||(a.curCSS=a.css),function(){var b=document.getElementsByTagName("body")[0],c=document.createElement("div"),d,e,g,h,i;d=document.createElement(b?"div":"body"),g={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},b&&a.extend(g,{position:"absolute",left:"-1000px",top:"-1000px"});for(var j in g)d.style[j]=g[j];d.appendChild(c),e=b||document.documentElement,e.insertBefore(d,e.firstChild),c.style.cssText="position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;",h=a(c).offset(function(a,b){return b}).offset(),d.innerHTML="",e.removeChild(d),i=h.top+h.left+(b?2e3:0),f.fractions=i>21&&i<22}()})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.draggable.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.widget("ui.draggable",a.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1},_create:function(){this.options.helper=="original"&&!/^(?:r|a|f)/.test(this.element.css("position"))&&(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},destroy:function(){if(!this.element.data("draggable"))return;return this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy(),this},_mouseCapture:function(b){var c=this.options;return this.helper||c.disabled||a(b.target).is(".ui-resizable-handle")?!1:(this.handle=this._getHandle(b),this.handle?(c.iframeFix&&a(c.iframeFix===!0?"iframe":c.iframeFix).each(function(){a('<div class="ui-draggable-iframeFix" style="background: #fff;"></div>').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(a(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(b){var c=this.options;return this.helper=this._createHelper(b),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),a.ui.ddmanager&&(a.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,c.cursorAt&&this._adjustOffsetFromHelper(c.cursorAt),c.containment&&this._setContainment(),this._trigger("start",b)===!1?(this._clear(),!1):(this._cacheHelperProportions(),a.ui.ddmanager&&!c.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this._mouseDrag(b,!0),a.ui.ddmanager&&a.ui.ddmanager.dragStart(this,b),!0)},_mouseDrag:function(b,c){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute");if(!c){var d=this._uiHash();if(this._trigger("drag",b,d)===!1)return this._mouseUp({}),!1;this.position=d.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";return a.ui.ddmanager&&a.ui.ddmanager.drag(this,b),!1},_mouseStop:function(b){var c=!1;a.ui.ddmanager&&!this.options.dropBehaviour&&(c=a.ui.ddmanager.drop(this,b)),this.dropped&&(c=this.dropped,this.dropped=!1);var d=this.element[0],e=!1;while(d&&(d=d.parentNode))d==document&&(e=!0);if(!e&&this.options.helper==="original")return!1;if(this.options.revert=="invalid"&&!c||this.options.revert=="valid"&&c||this.options.revert===!0||a.isFunction(this.options.revert)&&this.options.revert.call(this.element,c)){var f=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){f._trigger("stop",b)!==!1&&f._clear()})}else this._trigger("stop",b)!==!1&&this._clear();return!1},_mouseUp:function(b){return this.options.iframeFix===!0&&a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),a.ui.ddmanager&&a.ui.ddmanager.dragStop(this,b),a.ui.mouse.prototype._mouseUp.call(this,b)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?!0:!1;return a(this.options.handle,this.element).find("*").andSelf().each(function(){this==b.target&&(c=!0)}),c},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b])):c.helper=="clone"?this.element.clone().removeAttr("id"):this.element;return d.parents("body").length||d.appendTo(c.appendTo=="parent"?this.element[0].parentNode:c.appendTo),d[0]!=this.element[0]&&!/(fixed|absolute)/.test(d.css("position"))&&d.css("position","absolute"),d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[b.containment=="document"?0:a(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,b.containment=="document"?0:a(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,(b.containment=="document"?0:a(window).scrollLeft())+a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(b.containment=="document"?0:a(window).scrollTop())+(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)&&b.containment.constructor!=Array){var c=a(b.containment),d=c[0];if(!d)return;var e=c.offset(),f=a(d).css("overflow")!="hidden";this.containment=[(parseInt(a(d).css("borderLeftWidth"),10)||0)+(parseInt(a(d).css("paddingLeft"),10)||0),(parseInt(a(d).css("borderTopWidth"),10)||0)+(parseInt(a(d).css("paddingTop"),10)||0),(f?Math.max(d.scrollWidth,d.offsetWidth):d.offsetWidth)-(parseInt(a(d).css("borderLeftWidth"),10)||0)-(parseInt(a(d).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(f?Math.max(d.scrollHeight,d.offsetHeight):d.offsetHeight)-(parseInt(a(d).css("borderTopWidth"),10)||0)-(parseInt(a(d).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=c}else b.containment.constructor==Array&&(this.containment=b.containment)},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName),f=b.pageX,g=b.pageY;if(this.originalPosition){var h;if(this.containment){if(this.relative_container){var i=this.relative_container.offset();h=[this.containment[0]+i.left,this.containment[1]+i.top,this.containment[2]+i.left,this.containment[3]+i.top]}else h=this.containment;b.pageX-this.offset.click.left<h[0]&&(f=h[0]+this.offset.click.left),b.pageY-this.offset.click.top<h[1]&&(g=h[1]+this.offset.click.top),b.pageX-this.offset.click.left>h[2]&&(f=h[2]+this.offset.click.left),b.pageY-this.offset.click.top>h[3]&&(g=h[3]+this.offset.click.top)}if(c.grid){var j=c.grid[1]?this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1]:this.originalPageY;g=h?j-this.offset.click.top<h[1]||j-this.offset.click.top>h[3]?j-this.offset.click.top<h[1]?j+c.grid[1]:j-c.grid[1]:j:j;var k=c.grid[0]?this.originalPageX+Math.round((f-this.originalPageX)/c.grid[0])*c.grid[0]:this.originalPageX;f=h?k-this.offset.click.left<h[0]||k-this.offset.click.left>h[2]?k-this.offset.click.left<h[0]?k+c.grid[0]:k-c.grid[0]:k:k}}return{top:g-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:d.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:d.scrollLeft())}},_clear:function(){this.helper.removeClass("ui-draggable-dragging"),this.helper[0]!=this.element[0]&&!this.cancelHelperRemoval&&this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1},_trigger:function(b,c,d){return d=d||this._uiHash(),a.ui.plugin.call(this,b,[c,d]),b=="drag"&&(this.positionAbs=this._convertPositionTo("absolute")),a.Widget.prototype._trigger.call(this,b,c,d)},plugins:{},_uiHash:function(a){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),a.extend(a.ui.draggable,{version:"1.8.23"}),a.ui.plugin.add("draggable","connectToSortable",{start:function(b,c){var d=a(this).data("draggable"),e=d.options,f=a.extend({},c,{item:d.element});d.sortables=[],a(e.connectToSortable).each(function(){var c=a.data(this,"sortable");c&&!c.options.disabled&&(d.sortables.push({instance:c,shouldRevert:c.options.revert}),c.refreshPositions(),c._trigger("activate",b,f))})},stop:function(b,c){var d=a(this).data("draggable"),e=a.extend({},c,{item:d.element});a.each(d.sortables,function(){this.instance.isOver?(this.instance.isOver=0,d.cancelHelperRemoval=!0,this.instance.cancelHelperRemoval=!1,this.shouldRevert&&(this.instance.options.revert=!0),this.instance._mouseStop(b),this.instance.options.helper=this.instance.options._helper,d.options.helper=="original"&&this.instance.currentItem.css({top:"auto",left:"auto"})):(this.instance.cancelHelperRemoval=!1,this.instance._trigger("deactivate",b,e))})},drag:function(b,c){var d=a(this).data("draggable"),e=this,f=function(b){var c=this.offset.click.top,d=this.offset.click.left,e=this.positionAbs.top,f=this.positionAbs.left,g=b.height,h=b.width,i=b.top,j=b.left;return a.ui.isOver(e+c,f+d,i,j,g,h)};a.each(d.sortables,function(f){this.instance.positionAbs=d.positionAbs,this.instance.helperProportions=d.helperProportions,this.instance.offset.click=d.offset.click,this.instance._intersectsWith(this.instance.containerCache)?(this.instance.isOver||(this.instance.isOver=1,this.instance.currentItem=a(e).clone().removeAttr("id").appendTo(this.instance.element).data("sortable-item",!0),this.instance.options._helper=this.instance.options.helper,this.instance.options.helper=function(){return c.helper[0]},b.target=this.instance.currentItem[0],this.instance._mouseCapture(b,!0),this.instance._mouseStart(b,!0,!0),this.instance.offset.click.top=d.offset.click.top,this.instance.offset.click.left=d.offset.click.left,this.instance.offset.parent.left-=d.offset.parent.left-this.instance.offset.parent.left,this.instance.offset.parent.top-=d.offset.parent.top-this.instance.offset.parent.top,d._trigger("toSortable",b),d.dropped=this.instance.element,d.currentItem=d.element,this.instance.fromOutside=d),this.instance.currentItem&&this.instance._mouseDrag(b)):this.instance.isOver&&(this.instance.isOver=0,this.instance.cancelHelperRemoval=!0,this.instance.options.revert=!1,this.instance._trigger("out",b,this.instance._uiHash(this.instance)),this.instance._mouseStop(b,!0),this.instance.options.helper=this.instance.options._helper,this.instance.currentItem.remove(),this.instance.placeholder&&this.instance.placeholder.remove(),d._trigger("fromSortable",b),d.dropped=!1)})}}),a.ui.plugin.add("draggable","cursor",{start:function(b,c){var d=a("body"),e=a(this).data("draggable").options;d.css("cursor")&&(e._cursor=d.css("cursor")),d.css("cursor",e.cursor)},stop:function(b,c){var d=a(this).data("draggable").options;d._cursor&&a("body").css("cursor",d._cursor)}}),a.ui.plugin.add("draggable","opacity",{start:function(b,c){var d=a(c.helper),e=a(this).data("draggable").options;d.css("opacity")&&(e._opacity=d.css("opacity")),d.css("opacity",e.opacity)},stop:function(b,c){var d=a(this).data("draggable").options;d._opacity&&a(c.helper).css("opacity",d._opacity)}}),a.ui.plugin.add("draggable","scroll",{start:function(b,c){var d=a(this).data("draggable");d.scrollParent[0]!=document&&d.scrollParent[0].tagName!="HTML"&&(d.overflowOffset=d.scrollParent.offset())},drag:function(b,c){var d=a(this).data("draggable"),e=d.options,f=!1;if(d.scrollParent[0]!=document&&d.scrollParent[0].tagName!="HTML"){if(!e.axis||e.axis!="x")d.overflowOffset.top+d.scrollParent[0].offsetHeight-b.pageY<e.scrollSensitivity?d.scrollParent[0].scrollTop=f=d.scrollParent[0].scrollTop+e.scrollSpeed:b.pageY-d.overflowOffset.top<e.scrollSensitivity&&(d.scrollParent[0].scrollTop=f=d.scrollParent[0].scrollTop-e.scrollSpeed);if(!e.axis||e.axis!="y")d.overflowOffset.left+d.scrollParent[0].offsetWidth-b.pageX<e.scrollSensitivity?d.scrollParent[0].scrollLeft=f=d.scrollParent[0].scrollLeft+e.scrollSpeed:b.pageX-d.overflowOffset.left<e.scrollSensitivity&&(d.scrollParent[0].scrollLeft=f=d.scrollParent[0].scrollLeft-e.scrollSpeed)}else{if(!e.axis||e.axis!="x")b.pageY-a(document).scrollTop()<e.scrollSensitivity?f=a(document).scrollTop(a(document).scrollTop()-e.scrollSpeed):a(window).height()-(b.pageY-a(document).scrollTop())<e.scrollSensitivity&&(f=a(document).scrollTop(a(document).scrollTop()+e.scrollSpeed));if(!e.axis||e.axis!="y")b.pageX-a(document).scrollLeft()<e.scrollSensitivity?f=a(document).scrollLeft(a(document).scrollLeft()-e.scrollSpeed):a(window).width()-(b.pageX-a(document).scrollLeft())<e.scrollSensitivity&&(f=a(document).scrollLeft(a(document).scrollLeft()+e.scrollSpeed))}f!==!1&&a.ui.ddmanager&&!e.dropBehaviour&&a.ui.ddmanager.prepareOffsets(d,b)}}),a.ui.plugin.add("draggable","snap",{start:function(b,c){var d=a(this).data("draggable"),e=d.options;d.snapElements=[],a(e.snap.constructor!=String?e.snap.items||":data(draggable)":e.snap).each(function(){var b=a(this),c=b.offset();this!=d.element[0]&&d.snapElements.push({item:this,width:b.outerWidth(),height:b.outerHeight(),top:c.top,left:c.left})})},drag:function(b,c){var d=a(this).data("draggable"),e=d.options,f=e.snapTolerance,g=c.offset.left,h=g+d.helperProportions.width,i=c.offset.top,j=i+d.helperProportions.height;for(var k=d.snapElements.length-1;k>=0;k--){var l=d.snapElements[k].left,m=l+d.snapElements[k].width,n=d.snapElements[k].top,o=n+d.snapElements[k].height;if(!(l-f<g&&g<m+f&&n-f<i&&i<o+f||l-f<g&&g<m+f&&n-f<j&&j<o+f||l-f<h&&h<m+f&&n-f<i&&i<o+f||l-f<h&&h<m+f&&n-f<j&&j<o+f)){d.snapElements[k].snapping&&d.options.snap.release&&d.options.snap.release.call(d.element,b,a.extend(d._uiHash(),{snapItem:d.snapElements[k].item})),d.snapElements[k].snapping=!1;continue}if(e.snapMode!="inner"){var p=Math.abs(n-j)<=f,q=Math.abs(o-i)<=f,r=Math.abs(l-h)<=f,s=Math.abs(m-g)<=f;p&&(c.position.top=d._convertPositionTo("relative",{top:n-d.helperProportions.height,left:0}).top-d.margins.top),q&&(c.position.top=d._convertPositionTo("relative",{top:o,left:0}).top-d.margins.top),r&&(c.position.left=d._convertPositionTo("relative",{top:0,left:l-d.helperProportions.width}).left-d.margins.left),s&&(c.position.left=d._convertPositionTo("relative",{top:0,left:m}).left-d.margins.left)}var t=p||q||r||s;if(e.snapMode!="outer"){var p=Math.abs(n-i)<=f,q=Math.abs(o-j)<=f,r=Math.abs(l-g)<=f,s=Math.abs(m-h)<=f;p&&(c.position.top=d._convertPositionTo("relative",{top:n,left:0}).top-d.margins.top),q&&(c.position.top=d._convertPositionTo("relative",{top:o-d.helperProportions.height,left:0}).top-d.margins.top),r&&(c.position.left=d._convertPositionTo("relative",{top:0,left:l}).left-d.margins.left),s&&(c.position.left=d._convertPositionTo("relative",{top:0,left:m-d.helperProportions.width}).left-d.margins.left)}!d.snapElements[k].snapping&&(p||q||r||s||t)&&d.options.snap.snap&&d.options.snap.snap.call(d.element,b,a.extend(d._uiHash(),{snapItem:d.snapElements[k].item})),d.snapElements[k].snapping=p||q||r||s||t}}}),a.ui.plugin.add("draggable","stack",{start:function(b,c){var d=a(this).data("draggable").options,e=a.makeArray(a(d.stack)).sort(function(b,c){return(parseInt(a(b).css("zIndex"),10)||0)-(parseInt(a(c).css("zIndex"),10)||0)});if(!e.length)return;var f=parseInt(e[0].style.zIndex)||0;a(e).each(function(a){this.style.zIndex=f+a}),this[0].style.zIndex=f+e.length}}),a.ui.plugin.add("draggable","zIndex",{start:function(b,c){var d=a(c.helper),e=a(this).data("draggable").options;d.css("zIndex")&&(e._zIndex=d.css("zIndex")),d.css("zIndex",e.zIndex)},stop:function(b,c){var d=a(this).data("draggable").options;d._zIndex&&a(c.helper).css("zIndex",d._zIndex)}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.droppable.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.widget("ui.droppable",{widgetEventPrefix:"drop",options:{accept:"*",activeClass:!1,addClasses:!0,greedy:!1,hoverClass:!1,scope:"default",tolerance:"intersect"},_create:function(){var b=this.options,c=b.accept;this.isover=0,this.isout=1,this.accept=a.isFunction(c)?c:function(a){return a.is(c)},this.proportions={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight},a.ui.ddmanager.droppables[b.scope]=a.ui.ddmanager.droppables[b.scope]||[],a.ui.ddmanager.droppables[b.scope].push(this),b.addClasses&&this.element.addClass("ui-droppable")},destroy:function(){var b=a.ui.ddmanager.droppables[this.options.scope];for(var c=0;c<b.length;c++)b[c]==this&&b.splice(c,1);return this.element.removeClass("ui-droppable ui-droppable-disabled").removeData("droppable").unbind(".droppable"),this},_setOption:function(b,c){b=="accept"&&(this.accept=a.isFunction(c)?c:function(a){return a.is(c)}),a.Widget.prototype._setOption.apply(this,arguments)},_activate:function(b){var c=a.ui.ddmanager.current;this.options.activeClass&&this.element.addClass(this.options.activeClass),c&&this._trigger("activate",b,this.ui(c))},_deactivate:function(b){var c=a.ui.ddmanager.current;this.options.activeClass&&this.element.removeClass(this.options.activeClass),c&&this._trigger("deactivate",b,this.ui(c))},_over:function(b){var c=a.ui.ddmanager.current;if(!c||(c.currentItem||c.element)[0]==this.element[0])return;this.accept.call(this.element[0],c.currentItem||c.element)&&(this.options.hoverClass&&this.element.addClass(this.options.hoverClass),this._trigger("over",b,this.ui(c)))},_out:function(b){var c=a.ui.ddmanager.current;if(!c||(c.currentItem||c.element)[0]==this.element[0])return;this.accept.call(this.element[0],c.currentItem||c.element)&&(this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("out",b,this.ui(c)))},_drop:function(b,c){var d=c||a.ui.ddmanager.current;if(!d||(d.currentItem||d.element)[0]==this.element[0])return!1;var e=!1;return this.element.find(":data(droppable)").not(".ui-draggable-dragging").each(function(){var b=a.data(this,"droppable");if(b.options.greedy&&!b.options.disabled&&b.options.scope==d.options.scope&&b.accept.call(b.element[0],d.currentItem||d.element)&&a.ui.intersect(d,a.extend(b,{offset:b.element.offset()}),b.options.tolerance))return e=!0,!1}),e?!1:this.accept.call(this.element[0],d.currentItem||d.element)?(this.options.activeClass&&this.element.removeClass(this.options.activeClass),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass),this._trigger("drop",b,this.ui(d)),this.element):!1},ui:function(a){return{draggable:a.currentItem||a.element,helper:a.helper,position:a.position,offset:a.positionAbs}}}),a.extend(a.ui.droppable,{version:"1.8.23"}),a.ui.intersect=function(b,c,d){if(!c.offset)return!1;var e=(b.positionAbs||b.position.absolute).left,f=e+b.helperProportions.width,g=(b.positionAbs||b.position.absolute).top,h=g+b.helperProportions.height,i=c.offset.left,j=i+c.proportions.width,k=c.offset.top,l=k+c.proportions.height;switch(d){case"fit":return i<=e&&f<=j&&k<=g&&h<=l;case"intersect":return i<e+b.helperProportions.width/2&&f-b.helperProportions.width/2<j&&k<g+b.helperProportions.height/2&&h-b.helperProportions.height/2<l;case"pointer":var m=(b.positionAbs||b.position.absolute).left+(b.clickOffset||b.offset.click).left,n=(b.positionAbs||b.position.absolute).top+(b.clickOffset||b.offset.click).top,o=a.ui.isOver(n,m,k,i,c.proportions.height,c.proportions.width);return o;case"touch":return(g>=k&&g<=l||h>=k&&h<=l||g<k&&h>l)&&(e>=i&&e<=j||f>=i&&f<=j||e<i&&f>j);default:return!1}},a.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(b,c){var d=a.ui.ddmanager.droppables[b.options.scope]||[],e=c?c.type:null,f=(b.currentItem||b.element).find(":data(droppable)").andSelf();g:for(var h=0;h<d.length;h++){if(d[h].options.disabled||b&&!d[h].accept.call(d[h].element[0],b.currentItem||b.element))continue;for(var i=0;i<f.length;i++)if(f[i]==d[h].element[0]){d[h].proportions.height=0;continue g}d[h].visible=d[h].element.css("display")!="none";if(!d[h].visible)continue;e=="mousedown"&&d[h]._activate.call(d[h],c),d[h].offset=d[h].element.offset(),d[h].proportions={width:d[h].element[0].offsetWidth,height:d[h].element[0].offsetHeight}}},drop:function(b,c){var d=!1;return a.each(a.ui.ddmanager.droppables[b.options.scope]||[],function(){if(!this.options)return;!this.options.disabled&&this.visible&&a.ui.intersect(b,this,this.options.tolerance)&&(d=this._drop.call(this,c)||d),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],b.currentItem||b.element)&&(this.isout=1,this.isover=0,this._deactivate.call(this,c))}),d},dragStart:function(b,c){b.element.parents(":not(body,html)").bind("scroll.droppable",function(){b.options.refreshPositions||a.ui.ddmanager.prepareOffsets(b,c)})},drag:function(b,c){b.options.refreshPositions&&a.ui.ddmanager.prepareOffsets(b,c),a.each(a.ui.ddmanager.droppables[b.options.scope]||[],function(){if(this.options.disabled||this.greedyChild||!this.visible)return;var d=a.ui.intersect(b,this,this.options.tolerance),e=!d&&this.isover==1?"isout":d&&this.isover==0?"isover":null;if(!e)return;var f;if(this.options.greedy){var g=this.element.parents(":data(droppable):eq(0)");g.length&&(f=a.data(g[0],"droppable"),f.greedyChild=e=="isover"?1:0)}f&&e=="isover"&&(f.isover=0,f.isout=1,f._out.call(f,c)),this[e]=1,this[e=="isout"?"isover":"isout"]=0,this[e=="isover"?"_over":"_out"].call(this,c),f&&e=="isout"&&(f.isout=0,f.isover=1,f._over.call(f,c))})},dragStop:function(b,c){b.element.parents(":not(body,html)").unbind("scroll.droppable"),b.options.refreshPositions||a.ui.ddmanager.prepareOffsets(b,c)}}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.resizable.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.widget("ui.resizable",a.ui.mouse,{widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:1e3},_create:function(){var b=this,c=this.options;this.element.addClass("ui-resizable"),a.extend(this,{_aspectRatio:!!c.aspectRatio,aspectRatio:c.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:c.helper||c.ghost||c.animate?c.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/canvas|textarea|input|select|button|img/i)&&(this.element.wrap(a('<div class="ui-wrapper" style="overflow: hidden;"></div>').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("resizable",this.element.data("resizable")),this.elementIsWrapper=!0,this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")}),this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0}),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css({margin:this.originalElement.css("margin")}),this._proportionallyResize()),this.handles=c.handles||(a(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se");if(this.handles.constructor==String){this.handles=="all"&&(this.handles="n,e,s,w,se,sw,ne,nw");var d=this.handles.split(",");this.handles={};for(var e=0;e<d.length;e++){var f=a.trim(d[e]),g="ui-resizable-"+f,h=a('<div class="ui-resizable-handle '+g+'"></div>');h.css({zIndex:c.zIndex}),"se"==f&&h.addClass("ui-icon ui-icon-gripsmall-diagonal-se"),this.handles[f]=".ui-resizable-"+f,this.element.append(h)}}this._renderAxis=function(b){b=b||this.element;for(var c in this.handles){this.handles[c].constructor==String&&(this.handles[c]=a(this.handles[c],this.element).show());if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var d=a(this.handles[c],this.element),e=0;e=/sw|ne|nw|se|n|s/.test(c)?d.outerHeight():d.outerWidth();var f=["padding",/ne|nw|n/.test(c)?"Top":/se|sw|s/.test(c)?"Bottom":/^e$/.test(c)?"Right":"Left"].join("");b.css(f,e),this._proportionallyResize()}if(!a(this.handles[c]).length)continue}},this._renderAxis(this.element),this._handles=a(".ui-resizable-handle",this.element).disableSelection(),this._handles.mouseover(function(){if(!b.resizing){if(this.className)var a=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=a&&a[1]?a[1]:"se"}}),c.autoHide&&(this._handles.hide(),a(this.element).addClass("ui-resizable-autohide").hover(function(){if(c.disabled)return;a(this).removeClass("ui-resizable-autohide"),b._handles.show()},function(){if(c.disabled)return;b.resizing||(a(this).addClass("ui-resizable-autohide"),b._handles.hide())})),this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(b){a(b).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()};if(this.elementIsWrapper){b(this.element);var c=this.element;c.after(this.originalElement.css({position:c.css("position"),width:c.outerWidth(),height:c.outerHeight(),top:c.css("top"),left:c.css("left")})).remove()}return this.originalElement.css("resize",this.originalResizeStyle),b(this.originalElement),this},_mouseCapture:function(b){var c=!1;for(var d in this.handles)a(this.handles[d])[0]==b.target&&(c=!0);return!this.options.disabled&&c},_mouseStart:function(b){var d=this.options,e=this.element.position(),f=this.element;this.resizing=!0,this.documentScroll={top:a(document).scrollTop(),left:a(document).scrollLeft()},(f.is(".ui-draggable")||/absolute/.test(f.css("position")))&&f.css({position:"absolute",top:e.top,left:e.left}),this._renderProxy();var g=c(this.helper.css("left")),h=c(this.helper.css("top"));d.containment&&(g+=a(d.containment).scrollLeft()||0,h+=a(d.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:g,top:h},this.size=this._helper?{width:f.outerWidth(),height:f.outerHeight()}:{width:f.width(),height:f.height()},this.originalSize=this._helper?{width:f.outerWidth(),height:f.outerHeight()}:{width:f.width(),height:f.height()},this.originalPosition={left:g,top:h},this.sizeDiff={width:f.outerWidth()-f.width(),height:f.outerHeight()-f.height()},this.originalMousePosition={left:b.pageX,top:b.pageY},this.aspectRatio=typeof d.aspectRatio=="number"?d.aspectRatio:this.originalSize.width/this.originalSize.height||1;var i=a(".ui-resizable-"+this.axis).css("cursor");return a("body").css("cursor",i=="auto"?this.axis+"-resize":i),f.addClass("ui-resizable-resizing"),this._propagate("start",b),!0},_mouseDrag:function(b){var c=this.helper,d=this.options,e={},f=this,g=this.originalMousePosition,h=this.axis,i=b.pageX-g.left||0,j=b.pageY-g.top||0,k=this._change[h];if(!k)return!1;var l=k.apply(this,[b,i,j]),m=a.browser.msie&&a.browser.version<7,n=this.sizeDiff;this._updateVirtualBoundaries(b.shiftKey);if(this._aspectRatio||b.shiftKey)l=this._updateRatio(l,b);return l=this._respectSize(l,b),this._propagate("resize",b),c.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"}),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),this._updateCache(l),this._trigger("resize",b,this.ui()),!1},_mouseStop:function(b){this.resizing=!1;var c=this.options,d=this;if(this._helper){var e=this._proportionallyResizeElements,f=e.length&&/textarea/i.test(e[0].nodeName),g=f&&a.ui.hasScroll(e[0],"left")?0:d.sizeDiff.height,h=f?0:d.sizeDiff.width,i={width:d.helper.width()-h,height:d.helper.height()-g},j=parseInt(d.element.css("left"),10)+(d.position.left-d.originalPosition.left)||null,k=parseInt(d.element.css("top"),10)+(d.position.top-d.originalPosition.top)||null;c.animate||this.element.css(a.extend(i,{top:k,left:j})),d.helper.height(d.size.height),d.helper.width(d.size.width),this._helper&&!c.animate&&this._proportionallyResize()}return a("body").css("cursor","auto"),this.element.removeClass("ui-resizable-resizing"),this._propagate("stop",b),this._helper&&this.helper.remove(),!1},_updateVirtualBoundaries:function(a){var b=this.options,c,e,f,g,h;h={minWidth:d(b.minWidth)?b.minWidth:0,maxWidth:d(b.maxWidth)?b.maxWidth:Infinity,minHeight:d(b.minHeight)?b.minHeight:0,maxHeight:d(b.maxHeight)?b.maxHeight:Infinity};if(this._aspectRatio||a)c=h.minHeight*this.aspectRatio,f=h.minWidth/this.aspectRatio,e=h.maxHeight*this.aspectRatio,g=h.maxWidth/this.aspectRatio,c>h.minWidth&&(h.minWidth=c),f>h.minHeight&&(h.minHeight=f),e<h.maxWidth&&(h.maxWidth=e),g<h.maxHeight&&(h.maxHeight=g);this._vBoundaries=h},_updateCache:function(a){var b=this.options;this.offset=this.helper.offset(),d(a.left)&&(this.position.left=a.left),d(a.top)&&(this.position.top=a.top),d(a.height)&&(this.size.height=a.height),d(a.width)&&(this.size.width=a.width)},_updateRatio:function(a,b){var c=this.options,e=this.position,f=this.size,g=this.axis;return d(a.height)?a.width=a.height*this.aspectRatio:d(a.width)&&(a.height=a.width/this.aspectRatio),g=="sw"&&(a.left=e.left+(f.width-a.width),a.top=null),g=="nw"&&(a.top=e.top+(f.height-a.height),a.left=e.left+(f.width-a.width)),a},_respectSize:function(a,b){var c=this.helper,e=this._vBoundaries,f=this._aspectRatio||b.shiftKey,g=this.axis,h=d(a.width)&&e.maxWidth&&e.maxWidth<a.width,i=d(a.height)&&e.maxHeight&&e.maxHeight<a.height,j=d(a.width)&&e.minWidth&&e.minWidth>a.width,k=d(a.height)&&e.minHeight&&e.minHeight>a.height;j&&(a.width=e.minWidth),k&&(a.height=e.minHeight),h&&(a.width=e.maxWidth),i&&(a.height=e.maxHeight);var l=this.originalPosition.left+this.originalSize.width,m=this.position.top+this.size.height,n=/sw|nw|w/.test(g),o=/nw|ne|n/.test(g);j&&n&&(a.left=l-e.minWidth),h&&n&&(a.left=l-e.maxWidth),k&&o&&(a.top=m-e.minHeight),i&&o&&(a.top=m-e.maxHeight);var p=!a.width&&!a.height;return p&&!a.left&&a.top?a.top=null:p&&!a.top&&a.left&&(a.left=null),a},_proportionallyResize:function(){var b=this.options;if(!this._proportionallyResizeElements.length)return;var c=this.helper||this.element;for(var d=0;d<this._proportionallyResizeElements.length;d++){var e=this._proportionallyResizeElements[d];if(!this.borderDif){var f=[e.css("borderTopWidth"),e.css("borderRightWidth"),e.css("borderBottomWidth"),e.css("borderLeftWidth")],g=[e.css("paddingTop"),e.css("paddingRight"),e.css("paddingBottom"),e.css("paddingLeft")];this.borderDif=a.map(f,function(a,b){var c=parseInt(a,10)||0,d=parseInt(g[b],10)||0;return c+d})}if(!a.browser.msie||!a(c).is(":hidden")&&!a(c).parents(":hidden").length)e.css({height:c.height()-this.borderDif[0]-this.borderDif[2]||0,width:c.width()-this.borderDif[1]-this.borderDif[3]||0});else continue}},_renderProxy:function(){var b=this.element,c=this.options;this.elementOffset=b.offset();if(this._helper){this.helper=this.helper||a('<div style="overflow:hidden;"></div>');var d=a.browser.msie&&a.browser.version<7,e=d?1:0,f=d?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+f,height:this.element.outerHeight()+f,position:"absolute",left:this.elementOffset.left-e+"px",top:this.elementOffset.top-e+"px",zIndex:++c.zIndex}),this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(a,b,c){return{width:this.originalSize.width+b}},w:function(a,b,c){var d=this.options,e=this.originalSize,f=this.originalPosition;return{left:f.left+b,width:e.width-b}},n:function(a,b,c){var d=this.options,e=this.originalSize,f=this.originalPosition;return{top:f.top+c,height:e.height-c}},s:function(a,b,c){return{height:this.originalSize.height+c}},se:function(b,c,d){return a.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,c,d]))},sw:function(b,c,d){return a.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,c,d]))},ne:function(b,c,d){return a.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[b,c,d]))},nw:function(b,c,d){return a.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,c,d]))}},_propagate:function(b,c){a.ui.plugin.call(this,b,[c,this.ui()]),b!="resize"&&this._trigger(b,c,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),a.extend(a.ui.resizable,{version:"1.8.23"}),a.ui.plugin.add("resizable","alsoResize",{start:function(b,c){var d=a(this).data("resizable"),e=d.options,f=function(b){a(b).each(function(){var b=a(this);b.data("resizable-alsoresize",{width:parseInt(b.width(),10),height:parseInt(b.height(),10),left:parseInt(b.css("left"),10),top:parseInt(b.css("top"),10)})})};typeof e.alsoResize=="object"&&!e.alsoResize.parentNode?e.alsoResize.length?(e.alsoResize=e.alsoResize[0],f(e.alsoResize)):a.each(e.alsoResize,function(a){f(a)}):f(e.alsoResize)},resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.originalSize,g=d.originalPosition,h={height:d.size.height-f.height||0,width:d.size.width-f.width||0,top:d.position.top-g.top||0,left:d.position.left-g.left||0},i=function(b,d){a(b).each(function(){var b=a(this),e=a(this).data("resizable-alsoresize"),f={},g=d&&d.length?d:b.parents(c.originalElement[0]).length?["width","height"]:["width","height","top","left"];a.each(g,function(a,b){var c=(e[b]||0)+(h[b]||0);c&&c>=0&&(f[b]=c||null)}),b.css(f)})};typeof e.alsoResize=="object"&&!e.alsoResize.nodeType?a.each(e.alsoResize,function(a,b){i(a,b)}):i(e.alsoResize)},stop:function(b,c){a(this).removeData("resizable-alsoresize")}}),a.ui.plugin.add("resizable","animate",{stop:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d._proportionallyResizeElements,g=f.length&&/textarea/i.test(f[0].nodeName),h=g&&a.ui.hasScroll(f[0],"left")?0:d.sizeDiff.height,i=g?0:d.sizeDiff.width,j={width:d.size.width-i,height:d.size.height-h},k=parseInt(d.element.css("left"),10)+(d.position.left-d.originalPosition.left)||null,l=parseInt(d.element.css("top"),10)+(d.position.top-d.originalPosition.top)||null;d.element.animate(a.extend(j,l&&k?{top:l,left:k}:{}),{duration:e.animateDuration,easing:e.animateEasing,step:function(){var c={width:parseInt(d.element.css("width"),10),height:parseInt(d.element.css("height"),10),top:parseInt(d.element.css("top"),10),left:parseInt(d.element.css("left"),10)};f&&f.length&&a(f[0]).css({width:c.width,height:c.height}),d._updateCache(c),d._propagate("resize",b)}})}}),a.ui.plugin.add("resizable","containment",{start:function(b,d){var e=a(this).data("resizable"),f=e.options,g=e.element,h=f.containment,i=h instanceof a?h.get(0):/parent/.test(h)?g.parent().get(0):h;if(!i)return;e.containerElement=a(i);if(/document/.test(h)||h==document)e.containerOffset={left:0,top:0},e.containerPosition={left:0,top:0},e.parentData={element:a(document),left:0,top:0,width:a(document).width(),height:a(document).height()||document.body.parentNode.scrollHeight};else{var j=a(i),k=[];a(["Top","Right","Left","Bottom"]).each(function(a,b){k[a]=c(j.css("padding"+b))}),e.containerOffset=j.offset(),e.containerPosition=j.position(),e.containerSize={height:j.innerHeight()-k[3],width:j.innerWidth()-k[1]};var l=e.containerOffset,m=e.containerSize.height,n=e.containerSize.width,o=a.ui.hasScroll(i,"left")?i.scrollWidth:n,p=a.ui.hasScroll(i)?i.scrollHeight:m;e.parentData={element:i,left:l.left,top:l.top,width:o,height:p}}},resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.containerSize,g=d.containerOffset,h=d.size,i=d.position,j=d._aspectRatio||b.shiftKey,k={top:0,left:0},l=d.containerElement;l[0]!=document&&/static/.test(l.css("position"))&&(k=g),i.left<(d._helper?g.left:0)&&(d.size.width=d.size.width+(d._helper?d.position.left-g.left:d.position.left-k.left),j&&(d.size.height=d.size.width/d.aspectRatio),d.position.left=e.helper?g.left:0),i.top<(d._helper?g.top:0)&&(d.size.height=d.size.height+(d._helper?d.position.top-g.top:d.position.top),j&&(d.size.width=d.size.height*d.aspectRatio),d.position.top=d._helper?g.top:0),d.offset.left=d.parentData.left+d.position.left,d.offset.top=d.parentData.top+d.position.top;var m=Math.abs((d._helper?d.offset.left-k.left:d.offset.left-k.left)+d.sizeDiff.width),n=Math.abs((d._helper?d.offset.top-k.top:d.offset.top-g.top)+d.sizeDiff.height),o=d.containerElement.get(0)==d.element.parent().get(0),p=/relative|absolute/.test(d.containerElement.css("position"));o&&p&&(m-=d.parentData.left),m+d.size.width>=d.parentData.width&&(d.size.width=d.parentData.width-m,j&&(d.size.height=d.size.width/d.aspectRatio)),n+d.size.height>=d.parentData.height&&(d.size.height=d.parentData.height-n,j&&(d.size.width=d.size.height*d.aspectRatio))},stop:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.position,g=d.containerOffset,h=d.containerPosition,i=d.containerElement,j=a(d.helper),k=j.offset(),l=j.outerWidth()-d.sizeDiff.width,m=j.outerHeight()-d.sizeDiff.height;d._helper&&!e.animate&&/relative/.test(i.css("position"))&&a(this).css({left:k.left-h.left-g.left,width:l,height:m}),d._helper&&!e.animate&&/static/.test(i.css("position"))&&a(this).css({left:k.left-h.left-g.left,width:l,height:m})}}),a.ui.plugin.add("resizable","ghost",{start:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.size;d.ghost=d.originalElement.clone(),d.ghost.css({opacity:.25,display:"block",position:"relative",height:f.height,width:f.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof e.ghost=="string"?e.ghost:""),d.ghost.appendTo(d.helper)},resize:function(b,c){var d=a(this).data("resizable"),e=d.options;d.ghost&&d.ghost.css({position:"relative",height:d.size.height,width:d.size.width})},stop:function(b,c){var d=a(this).data("resizable"),e=d.options;d.ghost&&d.helper&&d.helper.get(0).removeChild(d.ghost.get(0))}}),a.ui.plugin.add("resizable","grid",{resize:function(b,c){var d=a(this).data("resizable"),e=d.options,f=d.size,g=d.originalSize,h=d.originalPosition,i=d.axis,j=e._aspectRatio||b.shiftKey;e.grid=typeof e.grid=="number"?[e.grid,e.grid]:e.grid;var k=Math.round((f.width-g.width)/(e.grid[0]||1))*(e.grid[0]||1),l=Math.round((f.height-g.height)/(e.grid[1]||1))*(e.grid[1]||1);/^(se|s|e)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l):/^(ne)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l,d.position.top=h.top-l):/^(sw)$/.test(i)?(d.size.width=g.width+k,d.size.height=g.height+l,d.position.left=h.left-k):(d.size.width=g.width+k,d.size.height=g.height+l,d.position.top=h.top-l,d.position.left=h.left-k)}});var c=function(a){return parseInt(a,10)||0},d=function(a){return!isNaN(parseInt(a,10))}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.selectable.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.widget("ui.selectable",a.ui.mouse,{options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch"},_create:function(){var b=this;this.element.addClass("ui-selectable"),this.dragged=!1;var c;this.refresh=function(){c=a(b.options.filter,b.element[0]),c.addClass("ui-selectee"),c.each(function(){var b=a(this),c=b.offset();a.data(this,"selectable-item",{element:this,$element:b,left:c.left,top:c.top,right:c.left+b.outerWidth(),bottom:c.top+b.outerHeight(),startselected:!1,selected:b.hasClass("ui-selected"),selecting:b.hasClass("ui-selecting"),unselecting:b.hasClass("ui-unselecting")})})},this.refresh(),this.selectees=c.addClass("ui-selectee"),this._mouseInit(),this.helper=a("<div class='ui-selectable-helper'></div>")},destroy:function(){return this.selectees.removeClass("ui-selectee").removeData("selectable-item"),this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable"),this._mouseDestroy(),this},_mouseStart:function(b){var c=this;this.opos=[b.pageX,b.pageY];if(this.options.disabled)return;var d=this.options;this.selectees=a(d.filter,this.element[0]),this._trigger("start",b),a(d.appendTo).append(this.helper),this.helper.css({left:b.clientX,top:b.clientY,width:0,height:0}),d.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var d=a.data(this,"selectable-item");d.startselected=!0,!b.metaKey&&!b.ctrlKey&&(d.$element.removeClass("ui-selected"),d.selected=!1,d.$element.addClass("ui-unselecting"),d.unselecting=!0,c._trigger("unselecting",b,{unselecting:d.element}))}),a(b.target).parents().andSelf().each(function(){var d=a.data(this,"selectable-item");if(d){var e=!b.metaKey&&!b.ctrlKey||!d.$element.hasClass("ui-selected");return d.$element.removeClass(e?"ui-unselecting":"ui-selected").addClass(e?"ui-selecting":"ui-unselecting"),d.unselecting=!e,d.selecting=e,d.selected=e,e?c._trigger("selecting",b,{selecting:d.element}):c._trigger("unselecting",b,{unselecting:d.element}),!1}})},_mouseDrag:function(b){var c=this;this.dragged=!0;if(this.options.disabled)return;var d=this.options,e=this.opos[0],f=this.opos[1],g=b.pageX,h=b.pageY;if(e>g){var i=g;g=e,e=i}if(f>h){var i=h;h=f,f=i}return this.helper.css({left:e,top:f,width:g-e,height:h-f}),this.selectees.each(function(){var i=a.data(this,"selectable-item");if(!i||i.element==c.element[0])return;var j=!1;d.tolerance=="touch"?j=!(i.left>g||i.right<e||i.top>h||i.bottom<f):d.tolerance=="fit"&&(j=i.left>e&&i.right<g&&i.top>f&&i.bottom<h),j?(i.selected&&(i.$element.removeClass("ui-selected"),i.selected=!1),i.unselecting&&(i.$element.removeClass("ui-unselecting"),i.unselecting=!1),i.selecting||(i.$element.addClass("ui-selecting"),i.selecting=!0,c._trigger("selecting",b,{selecting:i.element}))):(i.selecting&&((b.metaKey||b.ctrlKey)&&i.startselected?(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.$element.addClass("ui-selected"),i.selected=!0):(i.$element.removeClass("ui-selecting"),i.selecting=!1,i.startselected&&(i.$element.addClass("ui-unselecting"),i.unselecting=!0),c._trigger("unselecting",b,{unselecting:i.element}))),i.selected&&!b.metaKey&&!b.ctrlKey&&!i.startselected&&(i.$element.removeClass("ui-selected"),i.selected=!1,i.$element.addClass("ui-unselecting"),i.unselecting=!0,c._trigger("unselecting",b,{unselecting:i.element})))}),!1},_mouseStop:function(b){var c=this;this.dragged=!1;var d=this.options;return a(".ui-unselecting",this.element[0]).each(function(){var d=a.data(this,"selectable-item");d.$element.removeClass("ui-unselecting"),d.unselecting=!1,d.startselected=!1,c._trigger("unselected",b,{unselected:d.element})}),a(".ui-selecting",this.element[0]).each(function(){var d=a.data(this,"selectable-item");d.$element.removeClass("ui-selecting").addClass("ui-selected"),d.selecting=!1,d.selected=!0,d.startselected=!0,c._trigger("selected",b,{selected:d.element})}),this._trigger("stop",b),this.helper.remove(),!1}}),a.extend(a.ui.selectable,{version:"1.8.23"})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.sortable.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.widget("ui.sortable",a.ui.mouse,{widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3},_create:function(){var a=this.options;this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.floating=this.items.length?a.axis==="x"||/left|right/.test(this.items[0].item.css("float"))||/inline|table-cell/.test(this.items[0].item.css("display")):!1,this.offset=this.element.offset(),this._mouseInit(),this.ready=!0},destroy:function(){a.Widget.prototype.destroy.call(this),this.element.removeClass("ui-sortable ui-sortable-disabled"),this._mouseDestroy();for(var b=this.items.length-1;b>=0;b--)this.items[b].item.removeData(this.widgetName+"-item");return this},_setOption:function(b,c){b==="disabled"?(this.options[b]=c,this.widget()[c?"addClass":"removeClass"]("ui-sortable-disabled")):a.Widget.prototype._setOption.apply(this,arguments)},_mouseCapture:function(b,c){var d=this;if(this.reverting)return!1;if(this.options.disabled||this.options.type=="static")return!1;this._refreshItems(b);var e=null,f=this,g=a(b.target).parents().each(function(){if(a.data(this,d.widgetName+"-item")==f)return e=a(this),!1});a.data(b.target,d.widgetName+"-item")==f&&(e=a(b.target));if(!e)return!1;if(this.options.handle&&!c){var h=!1;a(this.options.handle,e).find("*").andSelf().each(function(){this==b.target&&(h=!0)});if(!h)return!1}return this.currentItem=e,this._removeCurrentsFromItems(),!0},_mouseStart:function(b,c,d){var e=this.options,f=this;this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(b),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,e.cursorAt&&this._adjustOffsetFromHelper(e.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!=this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),e.containment&&this._setContainment(),e.cursor&&(a("body").css("cursor")&&(this._storedCursor=a("body").css("cursor")),a("body").css("cursor",e.cursor)),e.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",e.opacity)),e.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",e.zIndex)),this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",b,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions();if(!d)for(var g=this.containers.length-1;g>=0;g--)this.containers[g]._trigger("activate",b,f._uiHash(this));return a.ui.ddmanager&&(a.ui.ddmanager.current=this),a.ui.ddmanager&&!e.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(b),!0},_mouseDrag:function(b){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs);if(this.options.scroll){var c=this.options,d=!1;this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-b.pageY<c.scrollSensitivity?this.scrollParent[0].scrollTop=d=this.scrollParent[0].scrollTop+c.scrollSpeed:b.pageY-this.overflowOffset.top<c.scrollSensitivity&&(this.scrollParent[0].scrollTop=d=this.scrollParent[0].scrollTop-c.scrollSpeed),this.overflowOffset.left+this.scrollParent[0].offsetWidth-b.pageX<c.scrollSensitivity?this.scrollParent[0].scrollLeft=d=this.scrollParent[0].scrollLeft+c.scrollSpeed:b.pageX-this.overflowOffset.left<c.scrollSensitivity&&(this.scrollParent[0].scrollLeft=d=this.scrollParent[0].scrollLeft-c.scrollSpeed)):(b.pageY-a(document).scrollTop()<c.scrollSensitivity?d=a(document).scrollTop(a(document).scrollTop()-c.scrollSpeed):a(window).height()-(b.pageY-a(document).scrollTop())<c.scrollSensitivity&&(d=a(document).scrollTop(a(document).scrollTop()+c.scrollSpeed)),b.pageX-a(document).scrollLeft()<c.scrollSensitivity?d=a(document).scrollLeft(a(document).scrollLeft()-c.scrollSpeed):a(window).width()-(b.pageX-a(document).scrollLeft())<c.scrollSensitivity&&(d=a(document).scrollLeft(a(document).scrollLeft()+c.scrollSpeed))),d!==!1&&a.ui.ddmanager&&!c.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b)}this.positionAbs=this._convertPositionTo("absolute");if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";for(var e=this.items.length-1;e>=0;e--){var f=this.items[e],g=f.item[0],h=this._intersectsWithPointer(f);if(!h)continue;if(g!=this.currentItem[0]&&this.placeholder[h==1?"next":"prev"]()[0]!=g&&!a.ui.contains(this.placeholder[0],g)&&(this.options.type=="semi-dynamic"?!a.ui.contains(this.element[0],g):!0)){this.direction=h==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(f))this._rearrange(b,f);else break;this._trigger("change",b,this._uiHash());break}}return this._contactContainers(b),a.ui.ddmanager&&a.ui.ddmanager.drag(this,b),this._trigger("sort",b,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(b,c){if(!b)return;a.ui.ddmanager&&!this.options.dropBehaviour&&a.ui.ddmanager.drop(this,b);if(this.options.revert){var d=this,e=d.placeholder.offset();d.reverting=!0,a(this.helper).animate({left:e.left-this.offset.parent.left-d.margins.left+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollLeft),top:e.top-this.offset.parent.top-d.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){d._clear(b)})}else this._clear(b,c);return!1},cancel:function(){var b=this;if(this.dragging){this._mouseUp({target:null}),this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("deactivate",null,b._uiHash(this)),this.containers[c].containerCache.over&&(this.containers[c]._trigger("out",null,b._uiHash(this)),this.containers[c].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),a.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?a(this.domPosition.prev).after(this.currentItem):a(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(b){var c=this._getItemsAsjQuery(b&&b.connected),d=[];return b=b||{},a(c).each(function(){var c=(a(b.item||this).attr(b.attribute||"id")||"").match(b.expression||/(.+)[-=_](.+)/);c&&d.push((b.key||c[1]+"[]")+"="+(b.key&&b.expression?c[1]:c[2]))}),!d.length&&b.key&&d.push(b.key+"="),d.join("&")},toArray:function(b){var c=this._getItemsAsjQuery(b&&b.connected),d=[];return b=b||{},c.each(function(){d.push(a(b.item||this).attr(b.attribute||"id")||"")}),d},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,d=this.positionAbs.top,e=d+this.helperProportions.height,f=a.left,g=f+a.width,h=a.top,i=h+a.height,j=this.offset.click.top,k=this.offset.click.left,l=d+j>h&&d+j<i&&b+k>f&&b+k<g;return this.options.tolerance=="pointer"||this.options.forcePointerForContainers||this.options.tolerance!="pointer"&&this.helperProportions[this.floating?"width":"height"]>a[this.floating?"width":"height"]?l:f<b+this.helperProportions.width/2&&c-this.helperProportions.width/2<g&&h<d+this.helperProportions.height/2&&e-this.helperProportions.height/2<i},_intersectsWithPointer:function(b){var c=this.options.axis==="x"||a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,b.top,b.height),d=this.options.axis==="y"||a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,b.left,b.width),e=c&&d,f=this._getDragVerticalDirection(),g=this._getDragHorizontalDirection();return e?this.floating?g&&g=="right"||f=="down"?2:1:f&&(f=="down"?2:1):!1},_intersectsWithSides:function(b){var c=a.ui.isOverAxis(this.positionAbs.top+this.offset.click.top,b.top+b.height/2,b.height),d=a.ui.isOverAxis(this.positionAbs.left+this.offset.click.left,b.left+b.width/2,b.width),e=this._getDragVerticalDirection(),f=this._getDragHorizontalDirection();return this.floating&&f?f=="right"&&d||f=="left"&&!d:e&&(e=="down"&&c||e=="up"&&!c)},_getDragVerticalDirection:function(){var a=this.positionAbs.top-this.lastPositionAbs.top;return a!=0&&(a>0?"down":"up")},_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){return this._refreshItems(a),this.refreshPositions(),this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(b){var c=this,d=[],e=[],f=this._connectWith();if(f&&b)for(var g=f.length-1;g>=0;g--){var h=a(f[g]);for(var i=h.length-1;i>=0;i--){var j=a.data(h[i],this.widgetName);j&&j!=this&&!j.options.disabled&&e.push([a.isFunction(j.options.items)?j.options.items.call(j.element):a(j.options.items,j.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),j])}}e.push([a.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):a(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(var g=e.length-1;g>=0;g--)e[g][0].each(function(){d.push(this)});return a(d)},_removeCurrentsFromItems:function(){var a=this.currentItem.find(":data("+this.widgetName+"-item)");for(var b=0;b<this.items.length;b++)for(var c=0;c<a.length;c++)a[c]==this.items[b].item[0]&&this.items.splice(b,1)},_refreshItems:function(b){this.items=[],this.containers=[this];var c=this.items,d=this,e=[[a.isFunction(this.options.items)?this.options.items.call(this.element[0],b,{item:this.currentItem}):a(this.options.items,this.element),this]],f=this._connectWith();if(f&&this.ready)for(var g=f.length-1;g>=0;g--){var h=a(f[g]);for(var i=h.length-1;i>=0;i--){var j=a.data(h[i],this.widgetName);j&&j!=this&&!j.options.disabled&&(e.push([a.isFunction(j.options.items)?j.options.items.call(j.element[0],b,{item:this.currentItem}):a(j.options.items,j.element),j]),this.containers.push(j))}}for(var g=e.length-1;g>=0;g--){var k=e[g][1],l=e[g][0];for(var i=0,m=l.length;i<m;i++){var n=a(l[i]);n.data(this.widgetName+"-item",k),c.push({item:n,instance:k,width:0,height:0,left:0,top:0})}}},refreshPositions:function(b){this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());for(var c=this.items.length-1;c>=0;c--){var d=this.items[c];if(d.instance!=this.currentContainer&&this.currentContainer&&d.item[0]!=this.currentItem[0])continue;var e=this.options.toleranceElement?a(this.options.toleranceElement,d.item):d.item;b||(d.width=e.outerWidth(),d.height=e.outerHeight());var f=e.offset();d.left=f.left,d.top=f.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(var c=this.containers.length-1;c>=0;c--){var f=this.containers[c].element.offset();this.containers[c].containerCache.left=f.left,this.containers[c].containerCache.top=f.top,this.containers[c].containerCache.width=this.containers[c].element.outerWidth(),this.containers[c].containerCache.height=this.containers[c].element.outerHeight()}return this},_createPlaceholder:function(b){var c=b||this,d=c.options;if(!d.placeholder||d.placeholder.constructor==String){var e=d.placeholder;d.placeholder={element:function(){var b=a(document.createElement(c.currentItem[0].nodeName)).addClass(e||c.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];return e||(b.style.visibility="hidden"),b},update:function(a,b){if(e&&!d.forcePlaceholderSize)return;b.height()||b.height(c.currentItem.innerHeight()-parseInt(c.currentItem.css("paddingTop")||0,10)-parseInt(c.currentItem.css("paddingBottom")||0,10)),b.width()||b.width(c.currentItem.innerWidth()-parseInt(c.currentItem.css("paddingLeft")||0,10)-parseInt(c.currentItem.css("paddingRight")||0,10))}}}c.placeholder=a(d.placeholder.element.call(c.element,c.currentItem)),c.currentItem.after(c.placeholder),d.placeholder.update(c,c.placeholder)},_contactContainers:function(b){var c=null,d=null;for(var e=this.containers.length-1;e>=0;e--){if(a.ui.contains(this.currentItem[0],this.containers[e].element[0]))continue;if(this._intersectsWith(this.containers[e].containerCache)){if(c&&a.ui.contains(this.containers[e].element[0],c.element[0]))continue;c=this.containers[e],d=e}else this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",b,this._uiHash(this)),this.containers[e].containerCache.over=0)}if(!c)return;if(this.containers.length===1)this.containers[d]._trigger("over",b,this._uiHash(this)),this.containers[d].containerCache.over=1;else if(this.currentContainer!=this.containers[d]){var f=1e4,g=null,h=this.positionAbs[this.containers[d].floating?"left":"top"];for(var i=this.items.length-1;i>=0;i--){if(!a.ui.contains(this.containers[d].element[0],this.items[i].item[0]))continue;var j=this.containers[d].floating?this.items[i].item.offset().left:this.items[i].item.offset().top;Math.abs(j-h)<f&&(f=Math.abs(j-h),g=this.items[i],this.direction=j-h>0?"down":"up")}if(!g&&!this.options.dropOnEmpty)return;this.currentContainer=this.containers[d],g?this._rearrange(b,g,null,!0):this._rearrange(b,null,this.containers[d].element,!0),this._trigger("change",b,this._uiHash()),this.containers[d]._trigger("change",b,this._uiHash(this)),this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[d]._trigger("over",b,this._uiHash(this)),this.containers[d].containerCache.over=1}},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b,this.currentItem])):c.helper=="clone"?this.currentItem.clone():this.currentItem;return d.parents("body").length||a(c.appendTo!="parent"?c.appendTo:this.currentItem[0].parentNode)[0].appendChild(d[0]),d[0]==this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(d[0].style.width==""||c.forceHelperSize)&&d.width(this.currentItem.width()),(d[0].style.height==""||c.forceHelperSize)&&d.height(this.currentItem.height()),d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.currentItem.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)){var c=a(b.containment)[0],d=a(b.containment).offset(),e=a(c).css("overflow")!="hidden";this.containment=[d.left+(parseInt(a(c).css("borderLeftWidth"),10)||0)+(parseInt(a(c).css("paddingLeft"),10)||0)-this.margins.left,d.top+(parseInt(a(c).css("borderTopWidth"),10)||0)+(parseInt(a(c).css("paddingTop"),10)||0)-this.margins.top,d.left+(e?Math.max(c.scrollWidth,c.offsetWidth):c.offsetWidth)-(parseInt(a(c).css("borderLeftWidth"),10)||0)-(parseInt(a(c).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,d.top+(e?Math.max(c.scrollHeight,c.offsetHeight):c.offsetHeight)-(parseInt(a(c).css("borderTopWidth"),10)||0)-(parseInt(a(c).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName);this.cssPosition=="relative"&&(this.scrollParent[0]==document||this.scrollParent[0]==this.offsetParent[0])&&(this.offset.relative=this._getRelativeOffset());var f=b.pageX,g=b.pageY;if(this.originalPosition){this.containment&&(b.pageX-this.offset.click.left<this.containment[0]&&(f=this.containment[0]+this.offset.click.left),b.pageY-this.offset.click.top<this.containment[1]&&(g=this.containment[1]+this.offset.click.top),b.pageX-this.offset.click.left>this.containment[2]&&(f=this.containment[2]+this.offset.click.left),b.pageY-this.offset.click.top>this.containment[3]&&(g=this.containment[3]+this.offset.click.top));if(c.grid){var h=this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1];g=this.containment?h-this.offset.click.top<this.containment[1]||h-this.offset.click.top>this.containment[3]?h-this.offset.click.top<this.containment[1]?h+c.grid[1]:h-c.grid[1]:h:h;var i=this.originalPageX+Math.round((f-this.originalPageX)/c.grid[0])*c.grid[0];f=this.containment?i-this.offset.click.left<this.containment[0]||i-this.offset.click.left>this.containment[2]?i-this.offset.click.left<this.containment[0]?i+c.grid[0]:i-c.grid[0]:i:i}}return{top:g-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+(a.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollTop():e?0:d.scrollTop()),left:f-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+(a.browser.safari&&this.cssPosition=="fixed"?0:this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():e?0:d.scrollLeft())}},_rearrange:function(a,b,c,d){c?c[0].appendChild(this.placeholder[0]):b.item[0].parentNode.insertBefore(this.placeholder[0],this.direction=="down"?b.item[0]:b.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var e=this,f=this.counter;window.setTimeout(function(){f==e.counter&&e.refreshPositions(!d)},0)},_clear:function(b,c){this.reverting=!1;var d=[],e=this;!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null;if(this.helper[0]==this.currentItem[0]){for(var f in this._storedCSS)if(this._storedCSS[f]=="auto"||this._storedCSS[f]=="static")this._storedCSS[f]="";this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();this.fromOutside&&!c&&d.push(function(a){this._trigger("receive",a,this._uiHash(this.fromOutside))}),(this.fromOutside||this.domPosition.prev!=this.currentItem.prev().not(".ui-sortable-helper")[0]||this.domPosition.parent!=this.currentItem.parent()[0])&&!c&&d.push(function(a){this._trigger("update",a,this._uiHash())});if(!a.ui.contains(this.element[0],this.currentItem[0])){c||d.push(function(a){this._trigger("remove",a,this._uiHash())});for(var f=this.containers.length-1;f>=0;f--)a.ui.contains(this.containers[f].element[0],this.currentItem[0])&&!c&&(d.push(function(a){return function(b){a._trigger("receive",b,this._uiHash(this))}}.call(this,this.containers[f])),d.push(function(a){return function(b){a._trigger("update",b,this._uiHash(this))}}.call(this,this.containers[f])))}for(var f=this.containers.length-1;f>=0;f--)c||d.push(function(a){return function(b){a._trigger("deactivate",b,this._uiHash(this))}}.call(this,this.containers[f])),this.containers[f].containerCache.over&&(d.push(function(a){return function(b){a._trigger("out",b,this._uiHash(this))}}.call(this,this.containers[f])),this.containers[f].containerCache.over=0);this._storedCursor&&a("body").css("cursor",this._storedCursor),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex),this.dragging=!1;if(this.cancelHelperRemoval){if(!c){this._trigger("beforeStop",b,this._uiHash());for(var f=0;f<d.length;f++)d[f].call(this,b);this._trigger("stop",b,this._uiHash())}return this.fromOutside=!1,!1}c||this._trigger("beforeStop",b,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.helper[0]!=this.currentItem[0]&&this.helper.remove(),this.helper=null;if(!c){for(var f=0;f<d.length;f++)d[f].call(this,b);this._trigger("stop",b,this._uiHash())}return this.fromOutside=!1,!0},_trigger:function(){a.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(b){var c=b||this;return{helper:c.helper,placeholder:c.placeholder||a([]),position:c.position,originalPosition:c.originalPosition,offset:c.positionAbs,item:c.currentItem,sender:b?b.element:null}}}),a.extend(a.ui.sortable,{version:"1.8.23"})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.accordion.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.widget("ui.accordion",{options:{active:0,animated:"slide",autoHeight:!0,clearStyle:!1,collapsible:!1,event:"click",fillSpace:!1,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:!1,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var b=this,c=b.options;b.running=0,b.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"),b.headers=b.element.find(c.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){if(c.disabled)return;a(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){if(c.disabled)return;a(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){if(c.disabled)return;a(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){if(c.disabled)return;a(this).removeClass("ui-state-focus")}),b.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom");if(c.navigation){var d=b.element.find("a").filter(c.navigationFilter).eq(0);if(d.length){var e=d.closest(".ui-accordion-header");e.length?b.active=e:b.active=d.closest(".ui-accordion-content").prev()}}b.active=b._findActive(b.active||c.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all").toggleClass("ui-corner-top"),b.active.next().addClass("ui-accordion-content-active"),b._createIcons(),b.resize(),b.element.attr("role","tablist"),b.headers.attr("role","tab").bind("keydown.accordion",function(a){return b._keydown(a)}).next().attr("role","tabpanel"),b.headers.not(b.active||"").attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).next().hide(),b.active.length?b.active.attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}):b.headers.eq(0).attr("tabIndex",0),a.browser.safari||b.headers.find("a").attr("tabIndex",-1),c.event&&b.headers.bind(c.event.split(" ").join(".accordion ")+".accordion",function(a){b._clickHandler.call(b,a,this),a.preventDefault()})},_createIcons:function(){var b=this.options;b.icons&&(a("<span></span>").addClass("ui-icon "+b.icons.header).prependTo(this.headers),this.active.children(".ui-icon").toggleClass(b.icons.header).toggleClass(b.icons.headerSelected),this.element.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.children(".ui-icon").remove(),this.element.removeClass("ui-accordion-icons")},destroy:function(){var b=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("tabIndex"),this.headers.find("a").removeAttr("tabIndex"),this._destroyIcons();var c=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");return(b.autoHeight||b.fillHeight)&&c.css("height",""),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments),b=="active"&&this.activate(c),b=="icons"&&(this._destroyIcons(),c&&this._createIcons()),b=="disabled"&&this.headers.add(this.headers.next())[c?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(b){if(this.options.disabled||b.altKey||b.ctrlKey)return;var c=a.ui.keyCode,d=this.headers.length,e=this.headers.index(b.target),f=!1;switch(b.keyCode){case c.RIGHT:case c.DOWN:f=this.headers[(e+1)%d];break;case c.LEFT:case c.UP:f=this.headers[(e-1+d)%d];break;case c.SPACE:case c.ENTER:this._clickHandler({target:b.target},b.target),b.preventDefault()}return f?(a(b.target).attr("tabIndex",-1),a(f).attr("tabIndex",0),f.focus(),!1):!0},resize:function(){var b=this.options,c;if(b.fillSpace){if(a.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}c=this.element.parent().height(),a.browser.msie&&this.element.parent().css("overflow",d),this.headers.each(function(){c-=a(this).outerHeight(!0)}),this.headers.next().each(function(){a(this).height(Math.max(0,c-a(this).innerHeight()+a(this).height()))}).css("overflow","auto")}else b.autoHeight&&(c=0,this.headers.next().each(function(){c=Math.max(c,a(this).height("").height())}).height(c));return this},activate:function(a){this.options.active=a;var b=this._findActive(a)[0];return this._clickHandler({target:b},b),this},_findActive:function(b){return b?typeof b=="number"?this.headers.filter(":eq("+b+")"):this.headers.not(this.headers.not(b)):b===!1?a([]):this.headers.filter(":eq(0)")},_clickHandler:function(b,c){var d=this.options;if(d.disabled)return;if(!b.target){if(!d.collapsible)return;this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header),this.active.next().addClass("ui-accordion-content-active");var e=this.active.next(),f={options:d,newHeader:a([]),oldHeader:d.active,newContent:a([]),oldContent:e},g=this.active=a([]);this._toggle(g,e,f);return}var h=a(b.currentTarget||c),i=h[0]===this.active[0];d.active=d.collapsible&&i?!1:this.headers.index(h);if(this.running||!d.collapsible&&i)return;var j=this.active,g=h.next(),e=this.active.next(),f={options:d,newHeader:i&&d.collapsible?a([]):h,oldHeader:this.active,newContent:i&&d.collapsible?a([]):g,oldContent:e},k=this.headers.index(this.active[0])>this.headers.index(h[0]);this.active=i?a([]):h,this._toggle(g,e,f,i,k),j.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header),i||(h.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected),h.next().addClass("ui-accordion-content-active"));return},_toggle:function(b,c,d,e,f){var g=this,h=g.options;g.toShow=b,g.toHide=c,g.data=d;var i=function(){if(!g)return;return g._completed.apply(g,arguments)};g._trigger("changestart",null,g.data),g.running=c.size()===0?b.size():c.size();if(h.animated){var j={};h.collapsible&&e?j={toShow:a([]),toHide:c,complete:i,down:f,autoHeight:h.autoHeight||h.fillSpace}:j={toShow:b,toHide:c,complete:i,down:f,autoHeight:h.autoHeight||h.fillSpace},h.proxied||(h.proxied=h.animated),h.proxiedDuration||(h.proxiedDuration=h.duration),h.animated=a.isFunction(h.proxied)?h.proxied(j):h.proxied,h.duration=a.isFunction(h.proxiedDuration)?h.proxiedDuration(j):h.proxiedDuration;var k=a.ui.accordion.animations,l=h.duration,m=h.animated;m&&!k[m]&&!a.easing[m]&&(m="slide"),k[m]||(k[m]=function(a){this.slide(a,{easing:m,duration:l||700})}),k[m](j)}else h.collapsible&&e?b.toggle():(c.hide(),b.show()),i(!0);c.prev().attr({"aria-expanded":"false","aria-selected":"false",tabIndex:-1}).blur(),b.prev().attr({"aria-expanded":"true","aria-selected":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(this.running)return;this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""}),this.toHide.removeClass("ui-accordion-content-active"),this.toHide.length&&(this.toHide.parent()[0].className=this.toHide.parent()[0].className),this._trigger("change",null,this.data)}}),a.extend(a.ui.accordion,{version:"1.8.23",animations:{slide:function(b,c){b=a.extend({easing:"swing",duration:300},b,c);if(!b.toHide.size()){b.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},b);return}if(!b.toShow.size()){b.toHide.animate({height:"hide",paddingTop:"hide",paddingBottom:"hide"},b);return}var d=b.toShow.css("overflow"),e=0,f={},g={},h=["height","paddingTop","paddingBottom"],i,j=b.toShow;i=j[0].style.width,j.width(j.parent().width()-parseFloat(j.css("paddingLeft"))-parseFloat(j.css("paddingRight"))-(parseFloat(j.css("borderLeftWidth"))||0)-(parseFloat(j.css("borderRightWidth"))||0)),a.each(h,function(c,d){g[d]="hide";var e=(""+a.css(b.toShow[0],d)).match(/^([\d+-.]+)(.*)$/);f[d]={value:e[1],unit:e[2]||"px"}}),b.toShow.css({height:0,overflow:"hidden"}).show(),b.toHide.filter(":hidden").each(b.complete).end().filter(":visible").animate(g,{step:function(a,c){c.prop=="height"&&(e=c.end-c.start===0?0:(c.now-c.start)/(c.end-c.start)),b.toShow[0].style[c.prop]=e*f[c.prop].value+f[c.prop].unit},duration:b.duration,easing:b.easing,complete:function(){b.autoHeight||b.toShow.css("height",""),b.toShow.css({width:i,overflow:d}),b.complete()}})},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1e3:200})}}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.autocomplete.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){var c=0;a.widget("ui.autocomplete",{options:{appendTo:"body",autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},pending:0,_create:function(){var b=this,c=this.element[0].ownerDocument,d;this.isMultiLine=this.element.is("textarea"),this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(b.options.disabled||b.element.propAttr("readOnly"))return;d=!1;var e=a.ui.keyCode;switch(c.keyCode){case e.PAGE_UP:b._move("previousPage",c);break;case e.PAGE_DOWN:b._move("nextPage",c);break;case e.UP:b._keyEvent("previous",c);break;case e.DOWN:b._keyEvent("next",c);break;case e.ENTER:case e.NUMPAD_ENTER:b.menu.active&&(d=!0,c.preventDefault());case e.TAB:if(!b.menu.active)return;b.menu.select(c);break;case e.ESCAPE:b.element.val(b.term),b.close(c);break;default:clearTimeout(b.searching),b.searching=setTimeout(function(){b.term!=b.element.val()&&(b.selectedItem=null,b.search(null,c))},b.options.delay)}}).bind("keypress.autocomplete",function(a){d&&(d=!1,a.preventDefault())}).bind("focus.autocomplete",function(){if(b.options.disabled)return;b.selectedItem=null,b.previous=b.element.val()}).bind("blur.autocomplete",function(a){if(b.options.disabled)return;clearTimeout(b.searching),b.closing=setTimeout(function(){b.close(a),b._change(a)},150)}),this._initSource(),this.menu=a("<ul></ul>").addClass("ui-autocomplete").appendTo(a(this.options.appendTo||"body",c)[0]).mousedown(function(c){var d=b.menu.element[0];a(c.target).closest(".ui-menu-item").length||setTimeout(function(){a(document).one("mousedown",function(c){c.target!==b.element[0]&&c.target!==d&&!a.ui.contains(d,c.target)&&b.close()})},1),setTimeout(function(){clearTimeout(b.closing)},13)}).menu({focus:function(a,c){var d=c.item.data("item.autocomplete");!1!==b._trigger("focus",a,{item:d})&&/^key/.test(a.originalEvent.type)&&b.element.val(d.value)},selected:function(a,d){var e=d.item.data("item.autocomplete"),f=b.previous;b.element[0]!==c.activeElement&&(b.element.focus(),b.previous=f,setTimeout(function(){b.previous=f,b.selectedItem=e},1)),!1!==b._trigger("select",a,{item:e})&&b.element.val(e.value),b.term=b.element.val(),b.close(a),b.selectedItem=e},blur:function(a,c){b.menu.element.is(":visible")&&b.element.val()!==b.term&&b.element.val(b.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu"),a.fn.bgiframe&&this.menu.element.bgiframe(),b.beforeunloadHandler=function(){b.element.removeAttr("autocomplete")},a(window).bind("beforeunload",b.beforeunloadHandler)},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup"),this.menu.element.remove(),a(window).unbind("beforeunload",this.beforeunloadHandler),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments),b==="source"&&this._initSource(),b==="appendTo"&&this.menu.element.appendTo(a(c||"body",this.element[0].ownerDocument)[0]),b==="disabled"&&c&&this.xhr&&this.xhr.abort()},_initSource:function(){var b=this,c,d;a.isArray(this.options.source)?(c=this.options.source,this.source=function(b,d){d(a.ui.autocomplete.filter(c,b.term))}):typeof this.options.source=="string"?(d=this.options.source,this.source=function(c,e){b.xhr&&b.xhr.abort(),b.xhr=a.ajax({url:d,data:c,dataType:"json",success:function(a,b){e(a)},error:function(){e([])}})}):this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val(),this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search",b)===!1)return;return this._search(a)},_search:function(a){this.pending++,this.element.addClass("ui-autocomplete-loading"),this.source({term:a},this._response())},_response:function(){var a=this,b=++c;return function(d){b===c&&a.__response(d),a.pending--,a.pending||a.element.removeClass("ui-autocomplete-loading")}},__response:function(a){!this.options.disabled&&a&&a.length?(a=this._normalize(a),this._suggest(a),this._trigger("open")):this.close()},close:function(a){clearTimeout(this.closing),this.menu.element.is(":visible")&&(this.menu.element.hide(),this.menu.deactivate(),this._trigger("close",a))},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(b){return b.length&&b[0].label&&b[0].value?b:a.map(b,function(b){return typeof b=="string"?{label:b,value:b}:a.extend({label:b.label||b.value,value:b.value||b.label},b)})},_suggest:function(b){var c=this.menu.element.empty().zIndex(this.element.zIndex()+1);this._renderMenu(c,b),this.menu.deactivate(),this.menu.refresh(),c.show(),this._resizeMenu(),c.position(a.extend({of:this.element},this.options.position)),this.options.autoFocus&&this.menu.next(new a.Event("mouseover"))},_resizeMenu:function(){var a=this.menu.element;a.outerWidth(Math.max(a.width("").outerWidth()+1,this.element.outerWidth()))},_renderMenu:function(b,c){var d=this;a.each(c,function(a,c){d._renderItem(b,c)})},_renderItem:function(b,c){return a("<li></li>").data("item.autocomplete",c).append(a("<a></a>").text(c.label)).appendTo(b)},_move:function(a,b){if(!this.menu.element.is(":visible")){this.search(null,b);return}if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term),this.menu.deactivate();return}this.menu[a](b)},widget:function(){return this.menu.element},_keyEvent:function(a,b){if(!this.isMultiLine||this.menu.element.is(":visible"))this._move(a,b),b.preventDefault()}}),a.extend(a.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")},filter:function(b,c){var d=new RegExp(a.ui.autocomplete.escapeRegex(c),"i");return a.grep(b,function(a){return d.test(a.label||a.value||a)})}})})(jQuery),function(a){a.widget("ui.menu",{_create:function(){var b=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(c){if(!a(c.target).closest(".ui-menu-item a").length)return;c.preventDefault(),b.select(c)}),this.refresh()},refresh:function(){var b=this,c=this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem");c.children("a").addClass("ui-corner-all").attr("tabindex",-1).mouseenter(function(c){b.activate(c,a(this).parent())}).mouseleave(function(){b.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.scrollTop(),e=this.element.height();c<0?this.element.scrollTop(d+c):c>=e&&this.element.scrollTop(d+c-e+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end(),this._trigger("focus",a,{item:b})},deactivate:function(){if(!this.active)return;this.active.children("a").removeClass("ui-state-hover").removeAttr("id"),this._trigger("blur"),this.active=null},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(!this.active){this.activate(c,this.element.children(b));return}var d=this.active[a+"All"](".ui-menu-item").eq(0);d.length?this.activate(c,d):this.activate(c,this.element.children(b))},nextPage:function(b){if(this.hasScroll()){if(!this.active||this.last()){this.activate(b,this.element.children(".ui-menu-item:first"));return}var c=this.active.offset().top,d=this.element.height(),e=this.element.children(".ui-menu-item").filter(function(){var b=a(this).offset().top-c-d+a(this).height();return b<10&&b>-10});e.length||(e=this.element.children(".ui-menu-item:last")),this.activate(b,e)}else this.activate(b,this.element.children(".ui-menu-item").filter(!this.active||this.last()?":first":":last"))},previousPage:function(b){if(this.hasScroll()){if(!this.active||this.first()){this.activate(b,this.element.children(".ui-menu-item:last"));return}var c=this.active.offset().top,d=this.element.height(),e=this.element.children(".ui-menu-item").filter(function(){var b=a(this).offset().top-c+d-a(this).height();return b<10&&b>-10});e.length||(e=this.element.children(".ui-menu-item:first")),this.activate(b,e)}else this.activate(b,this.element.children(".ui-menu-item").filter(!this.active||this.first()?":last":":first"))},hasScroll:function(){return this.element.height()<this.element[a.fn.prop?"prop":"attr"]("scrollHeight")},select:function(a){this._trigger("selected",a,{item:this.active})}})}(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.button.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){var c,d,e,f,g="ui-button ui-widget ui-state-default ui-corner-all",h="ui-state-hover ui-state-active ",i="ui-button-icons-only ui-button-icon-only ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary ui-button-text-only",j=function(){var b=a(this).find(":ui-button");setTimeout(function(){b.button("refresh")},1)},k=function(b){var c=b.name,d=b.form,e=a([]);return c&&(d?e=a(d).find("[name='"+c+"']"):e=a("[name='"+c+"']",b.ownerDocument).filter(function(){return!this.form})),e};a.widget("ui.button",{options:{disabled:null,text:!0,label:null,icons:{primary:null,secondary:null}},_create:function(){this.element.closest("form").unbind("reset.button").bind("reset.button",j),typeof this.options.disabled!="boolean"?this.options.disabled=!!this.element.propAttr("disabled"):this.element.propAttr("disabled",this.options.disabled),this._determineButtonType(),this.hasTitle=!!this.buttonElement.attr("title");var b=this,h=this.options,i=this.type==="checkbox"||this.type==="radio",l="ui-state-hover"+(i?"":" ui-state-active"),m="ui-state-focus";h.label===null&&(h.label=this.buttonElement.html()),this.buttonElement.addClass(g).attr("role","button").bind("mouseenter.button",function(){if(h.disabled)return;a(this).addClass("ui-state-hover"),this===c&&a(this).addClass("ui-state-active")}).bind("mouseleave.button",function(){if(h.disabled)return;a(this).removeClass(l)}).bind("click.button",function(a){h.disabled&&(a.preventDefault(),a.stopImmediatePropagation())}),this.element.bind("focus.button",function(){b.buttonElement.addClass(m)}).bind("blur.button",function(){b.buttonElement.removeClass(m)}),i&&(this.element.bind("change.button",function(){if(f)return;b.refresh()}),this.buttonElement.bind("mousedown.button",function(a){if(h.disabled)return;f=!1,d=a.pageX,e=a.pageY}).bind("mouseup.button",function(a){if(h.disabled)return;if(d!==a.pageX||e!==a.pageY)f=!0})),this.type==="checkbox"?this.buttonElement.bind("click.button",function(){if(h.disabled||f)return!1;a(this).toggleClass("ui-state-active"),b.buttonElement.attr("aria-pressed",b.element[0].checked)}):this.type==="radio"?this.buttonElement.bind("click.button",function(){if(h.disabled||f)return!1;a(this).addClass("ui-state-active"),b.buttonElement.attr("aria-pressed","true");var c=b.element[0];k(c).not(c).map(function(){return a(this).button("widget")[0]}).removeClass("ui-state-active").attr("aria-pressed","false")}):(this.buttonElement.bind("mousedown.button",function(){if(h.disabled)return!1;a(this).addClass("ui-state-active"),c=this,a(document).one("mouseup",function(){c=null})}).bind("mouseup.button",function(){if(h.disabled)return!1;a(this).removeClass("ui-state-active")}).bind("keydown.button",function(b){if(h.disabled)return!1;(b.keyCode==a.ui.keyCode.SPACE||b.keyCode==a.ui.keyCode.ENTER)&&a(this).addClass("ui-state-active")}).bind("keyup.button",function(){a(this).removeClass("ui-state-active")}),this.buttonElement.is("a")&&this.buttonElement.keyup(function(b){b.keyCode===a.ui.keyCode.SPACE&&a(this).click()})),this._setOption("disabled",h.disabled),this._resetButton()},_determineButtonType:function(){this.element.is(":checkbox")?this.type="checkbox":this.element.is(":radio")?this.type="radio":this.element.is("input")?this.type="input":this.type="button";if(this.type==="checkbox"||this.type==="radio"){var a=this.element.parents().filter(":last"),b="label[for='"+this.element.attr("id")+"']";this.buttonElement=a.find(b),this.buttonElement.length||(a=a.length?a.siblings():this.element.siblings(),this.buttonElement=a.filter(b),this.buttonElement.length||(this.buttonElement=a.find(b))),this.element.addClass("ui-helper-hidden-accessible");var c=this.element.is(":checked");c&&this.buttonElement.addClass("ui-state-active"),this.buttonElement.attr("aria-pressed",c)}else this.buttonElement=this.element},widget:function(){return this.buttonElement},destroy:function(){this.element.removeClass("ui-helper-hidden-accessible"),this.buttonElement.removeClass(g+" "+h+" "+i).removeAttr("role").removeAttr("aria-pressed").html(this.buttonElement.find(".ui-button-text").html()),this.hasTitle||this.buttonElement.removeAttr("title"),a.Widget.prototype.destroy.call(this)},_setOption:function(b,c){a.Widget.prototype._setOption.apply(this,arguments);if(b==="disabled"){c?this.element.propAttr("disabled",!0):this.element.propAttr("disabled",!1);return}this._resetButton()},refresh:function(){var b=this.element.is(":disabled");b!==this.options.disabled&&this._setOption("disabled",b),this.type==="radio"?k(this.element[0]).each(function(){a(this).is(":checked")?a(this).button("widget").addClass("ui-state-active").attr("aria-pressed","true"):a(this).button("widget").removeClass("ui-state-active").attr("aria-pressed","false")}):this.type==="checkbox"&&(this.element.is(":checked")?this.buttonElement.addClass("ui-state-active").attr("aria-pressed","true"):this.buttonElement.removeClass("ui-state-active").attr("aria-pressed","false"))},_resetButton:function(){if(this.type==="input"){this.options.label&&this.element.val(this.options.label);return}var b=this.buttonElement.removeClass(i),c=a("<span></span>",this.element[0].ownerDocument).addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary,f=[];d.primary||d.secondary?(this.options.text&&f.push("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary")),d.primary&&b.prepend("<span class='ui-button-icon-primary ui-icon "+d.primary+"'></span>"),d.secondary&&b.append("<span class='ui-button-icon-secondary ui-icon "+d.secondary+"'></span>"),this.options.text||(f.push(e?"ui-button-icons-only":"ui-button-icon-only"),this.hasTitle||b.attr("title",c))):f.push("ui-button-text-only"),b.addClass(f.join(" "))}}),a.widget("ui.buttonset",{options:{items:":button, :submit, :reset, :checkbox, :radio, a, :data(button)"},_create:function(){this.element.addClass("ui-buttonset")},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c),a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){var b=this.element.css("direction")==="rtl";this.buttons=this.element.find(this.options.items).filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":first").addClass(b?"ui-corner-right":"ui-corner-left").end().filter(":last").addClass(b?"ui-corner-left":"ui-corner-right").end().end()},destroy:function(){this.element.removeClass("ui-buttonset"),this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy"),a.Widget.prototype.destroy.call(this)}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.dialog.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){var c="ui-dialog ui-widget ui-widget-content ui-corner-all ",d={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},e={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};a.widget("ui.dialog",{options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",collision:"fit",using:function(b){var c=a(this).css(b).offset().top;c<0&&a(this).css("top",b.top-c)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),typeof this.originalTitle!="string"&&(this.originalTitle=""),this.options.title=this.options.title||this.originalTitle;var b=this,d=b.options,e=d.title||"&#160;",f=a.ui.dialog.getTitleId(b.element),g=(b.uiDialog=a("<div></div>")).appendTo(document.body).hide().addClass(c+d.dialogClass).css({zIndex:d.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(c){d.closeOnEscape&&!c.isDefaultPrevented()&&c.keyCode&&c.keyCode===a.ui.keyCode.ESCAPE&&(b.close(c),c.preventDefault())}).attr({role:"dialog","aria-labelledby":f}).mousedown(function(a){b.moveToTop(!1,a)}),h=b.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g),i=(b.uiDialogTitlebar=a("<div></div>")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),j=a('<a href="#"></a>').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){j.addClass("ui-state-hover")},function(){j.removeClass("ui-state-hover")}).focus(function(){j.addClass("ui-state-focus")}).blur(function(){j.removeClass("ui-state-focus")}).click(function(a){return b.close(a),!1}).appendTo(i),k=(b.uiDialogTitlebarCloseText=a("<span></span>")).addClass("ui-icon ui-icon-closethick").text(d.closeText).appendTo(j),l=a("<span></span>").addClass("ui-dialog-title").attr("id",f).html(e).prependTo(i);a.isFunction(d.beforeclose)&&!a.isFunction(d.beforeClose)&&(d.beforeClose=d.beforeclose),i.find("*").add(i).disableSelection(),d.draggable&&a.fn.draggable&&b._makeDraggable(),d.resizable&&a.fn.resizable&&b._makeResizable(),b._createButtons(d.buttons),b._isOpen=!1,a.fn.bgiframe&&g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;return a.overlay&&a.overlay.destroy(),a.uiDialog.hide(),a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),a.uiDialog.remove(),a.originalTitle&&a.element.attr("title",a.originalTitle),a},widget:function(){return this.uiDialog},close:function(b){var c=this,d,e;if(!1===c._trigger("beforeClose",b))return;return c.overlay&&c.overlay.destroy(),c.uiDialog.unbind("keypress.ui-dialog"),c._isOpen=!1,c.options.hide?c.uiDialog.hide(c.options.hide,function(){c._trigger("close",b)}):(c.uiDialog.hide(),c._trigger("close",b)),a.ui.dialog.overlay.resize(),c.options.modal&&(d=0,a(".ui-dialog").each(function(){this!==c.uiDialog[0]&&(e=a(this).css("z-index"),isNaN(e)||(d=Math.max(d,e)))}),a.ui.dialog.maxZ=d),c},isOpen:function(){return this._isOpen},moveToTop:function(b,c){var d=this,e=d.options,f;return e.modal&&!b||!e.stack&&!e.modal?d._trigger("focus",c):(e.zIndex>a.ui.dialog.maxZ&&(a.ui.dialog.maxZ=e.zIndex),d.overlay&&(a.ui.dialog.maxZ+=1,d.overlay.$el.css("z-index",a.ui.dialog.overlay.maxZ=a.ui.dialog.maxZ)),f={scrollTop:d.element.scrollTop(),scrollLeft:d.element.scrollLeft()},a.ui.dialog.maxZ+=1,d.uiDialog.css("z-index",a.ui.dialog.maxZ),d.element.attr(f),d._trigger("focus",c),d)},open:function(){if(this._isOpen)return;var b=this,c=b.options,d=b.uiDialog;return b.overlay=c.modal?new a.ui.dialog.overlay(b):null,b._size(),b._position(c.position),d.show(c.show),b.moveToTop(!0),c.modal&&d.bind("keydown.ui-dialog",function(b){if(b.keyCode!==a.ui.keyCode.TAB)return;var c=a(":tabbable",this),d=c.filter(":first"),e=c.filter(":last");if(b.target===e[0]&&!b.shiftKey)return d.focus(1),!1;if(b.target===d[0]&&b.shiftKey)return e.focus(1),!1}),a(b.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus(),b._isOpen=!0,b._trigger("open"),b},_createButtons:function(b){var c=this,d=!1,e=a("<div></div>").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),f=a("<div></div>").addClass("ui-dialog-buttonset").appendTo(e);c.uiDialog.find(".ui-dialog-buttonpane").remove(),typeof b=="object"&&b!==null&&a.each(b,function(){return!(d=!0)}),d&&(a.each(b,function(b,d){d=a.isFunction(d)?{click:d,text:b}:d;var e=a('<button type="button"></button>').click(function(){d.click.apply(c.element[0],arguments)}).appendTo(f);a.each(d,function(a,b){if(a==="click")return;a in e?e[a](b):e.attr(a,b)}),a.fn.button&&e.button()}),e.appendTo(c.uiDialog))},_makeDraggable:function(){function f(a){return{position:a.position,offset:a.offset}}var b=this,c=b.options,d=a(document),e;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(d,g){e=c.height==="auto"?"auto":a(this).height(),a(this).height(a(this).height()).addClass("ui-dialog-dragging"),b._trigger("dragStart",d,f(g))},drag:function(a,c){b._trigger("drag",a,f(c))},stop:function(g,h){c.position=[h.position.left-d.scrollLeft(),h.position.top-d.scrollTop()],a(this).removeClass("ui-dialog-dragging").height(e),b._trigger("dragStop",g,f(h)),a.ui.dialog.overlay.resize()}})},_makeResizable:function(c){function h(a){return{originalPosition:a.originalPosition,originalSize:a.originalSize,position:a.position,size:a.size}}c=c===b?this.options.resizable:c;var d=this,e=d.options,f=d.uiDialog.css("position"),g=typeof c=="string"?c:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:e.maxWidth,maxHeight:e.maxHeight,minWidth:e.minWidth,minHeight:d._minHeight(),handles:g,start:function(b,c){a(this).addClass("ui-dialog-resizing"),d._trigger("resizeStart",b,h(c))},resize:function(a,b){d._trigger("resize",a,h(b))},stop:function(b,c){a(this).removeClass("ui-dialog-resizing"),e.height=a(this).height(),e.width=a(this).width(),d._trigger("resizeStop",b,h(c)),a.ui.dialog.overlay.resize()}}).css("position",f).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight,a.height)},_position:function(b){var c=[],d=[0,0],e;if(b){if(typeof b=="string"||typeof b=="object"&&"0"in b)c=b.split?b.split(" "):[b[0],b[1]],c.length===1&&(c[1]=c[0]),a.each(["left","top"],function(a,b){+c[a]===c[a]&&(d[a]=c[a],c[a]=b)}),b={my:c.join(" "),at:c.join(" "),offset:d.join(" ")};b=a.extend({},a.ui.dialog.prototype.options.position,b)}else b=a.ui.dialog.prototype.options.position;e=this.uiDialog.is(":visible"),e||this.uiDialog.show(),this.uiDialog.css({top:0,left:0}).position(a.extend({of:window},b)),e||this.uiDialog.hide()},_setOptions:function(b){var c=this,f={},g=!1;a.each(b,function(a,b){c._setOption(a,b),a in d&&(g=!0),a in e&&(f[a]=b)}),g&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",f)},_setOption:function(b,d){var e=this,f=e.uiDialog;switch(b){case"beforeclose":b="beforeClose";break;case"buttons":e._createButtons(d);break;case"closeText":e.uiDialogTitlebarCloseText.text(""+d);break;case"dialogClass":f.removeClass(e.options.dialogClass).addClass(c+d);break;case"disabled":d?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case"draggable":var g=f.is(":data(draggable)");g&&!d&&f.draggable("destroy"),!g&&d&&e._makeDraggable();break;case"position":e._position(d);break;case"resizable":var h=f.is(":data(resizable)");h&&!d&&f.resizable("destroy"),h&&typeof d=="string"&&f.resizable("option","handles",d),!h&&d!==!1&&e._makeResizable(d);break;case"title":a(".ui-dialog-title",e.uiDialogTitlebar).html(""+(d||"&#160;"))}a.Widget.prototype._setOption.apply(e,arguments)},_size:function(){var b=this.options,c,d,e=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),b.minWidth>b.width&&(b.width=b.minWidth),c=this.uiDialog.css({height:"auto",width:b.width}).height(),d=Math.max(0,b.minHeight-c);if(b.height==="auto")if(a.support.minHeight)this.element.css({minHeight:d,height:"auto"});else{this.uiDialog.show();var f=this.element.css("height","auto").height();e||this.uiDialog.hide(),this.element.height(Math.max(f,d))}else this.element.height(Math.max(b.height-c,0));this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),a.extend(a.ui.dialog,{version:"1.8.23",uuid:0,maxZ:0,getTitleId:function(a){var b=a.attr("id");return b||(this.uuid+=1,b=this.uuid),"ui-dialog-title-"+b},overlay:function(b){this.$el=a.ui.dialog.overlay.create(b)}}),a.extend(a.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:a.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(a){return a+".dialog-overlay"}).join(" "),create:function(b){this.instances.length===0&&(setTimeout(function(){a.ui.dialog.overlay.instances.length&&a(document).bind(a.ui.dialog.overlay.events,function(b){if(a(b.target).zIndex()<a.ui.dialog.overlay.maxZ)return!1})},1),a(document).bind("keydown.dialog-overlay",function(c){b.options.closeOnEscape&&!c.isDefaultPrevented()&&c.keyCode&&c.keyCode===a.ui.keyCode.ESCAPE&&(b.close(c),c.preventDefault())}),a(window).bind("resize.dialog-overlay",a.ui.dialog.overlay.resize));var c=(this.oldInstances.pop()||a("<div></div>").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});return a.fn.bgiframe&&c.bgiframe(),this.instances.push(c),c},destroy:function(b){var c=a.inArray(b,this.instances);c!=-1&&this.oldInstances.push(this.instances.splice(c,1)[0]),this.instances.length===0&&a([document,window]).unbind(".dialog-overlay"),b.remove();var d=0;a.each(this.instances,function(){d=Math.max(d,this.css("z-index"))}),this.maxZ=d},height:function(){var b,c;return a.browser.msie&&a.browser.version<7?(b=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),c=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight),b<c?a(window).height()+"px":b+"px"):a(document).height()+"px"},width:function(){var b,c;return a.browser.msie?(b=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth),c=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth),b<c?a(window).width()+"px":b+"px"):a(document).width()+"px"},resize:function(){var b=a([]);a.each(a.ui.dialog.overlay.instances,function(){b=b.add(this)}),b.css({width:0,height:0}).css({width:a.ui.dialog.overlay.width(),height:a.ui.dialog.overlay.height()})}}),a.extend(a.ui.dialog.overlay.prototype,{destroy:function(){a.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.slider.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){var c=5;a.widget("ui.slider",a.ui.mouse,{widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null},_create:function(){var b=this,d=this.options,e=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),f="<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>",g=d.values&&d.values.length||1,h=[];this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"+(d.disabled?" ui-slider-disabled ui-disabled":"")),this.range=a([]),d.range&&(d.range===!0&&(d.values||(d.values=[this._valueMin(),this._valueMin()]),d.values.length&&d.values.length!==2&&(d.values=[d.values[0],d.values[0]])),this.range=a("<div></div>").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+(d.range==="min"||d.range==="max"?" ui-slider-range-"+d.range:"")));for(var i=e.length;i<g;i+=1)h.push(f);this.handles=e.add(a(h.join("")).appendTo(b.element)),this.handle=this.handles.eq(0),this.handles.add(this.range).filter("a").click(function(a){a.preventDefault()}).hover(function(){d.disabled||a(this).addClass("ui-state-hover")},function(){a(this).removeClass("ui-state-hover")}).focus(function(){d.disabled?a(this).blur():(a(".ui-slider .ui-state-focus").removeClass("ui-state-focus"),a(this).addClass("ui-state-focus"))}).blur(function(){a(this).removeClass("ui-state-focus")}),this.handles.each(function(b){a(this).data("index.ui-slider-handle",b)}),this.handles.keydown(function(d){var e=a(this).data("index.ui-slider-handle"),f,g,h,i;if(b.options.disabled)return;switch(d.keyCode){case a.ui.keyCode.HOME:case a.ui.keyCode.END:case a.ui.keyCode.PAGE_UP:case a.ui.keyCode.PAGE_DOWN:case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:d.preventDefault();if(!b._keySliding){b._keySliding=!0,a(this).addClass("ui-state-active"),f=b._start(d,e);if(f===!1)return}}i=b.options.step,b.options.values&&b.options.values.length?g=h=b.values(e):g=h=b.value();switch(d.keyCode){case a.ui.keyCode.HOME:h=b._valueMin();break;case a.ui.keyCode.END:h=b._valueMax();break;case a.ui.keyCode.PAGE_UP:h=b._trimAlignValue(g+(b._valueMax()-b._valueMin())/c);break;case a.ui.keyCode.PAGE_DOWN:h=b._trimAlignValue(g-(b._valueMax()-b._valueMin())/c);break;case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:if(g===b._valueMax())return;h=b._trimAlignValue(g+i);break;case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:if(g===b._valueMin())return;h=b._trimAlignValue(g-i)}b._slide(d,e,h)}).keyup(function(c){var d=a(this).data("index.ui-slider-handle");b._keySliding&&(b._keySliding=!1,b._stop(c,d),b._change(c,d),a(this).removeClass("ui-state-active"))}),this._refreshValue(),this._animateOff=!1},destroy:function(){return this.handles.remove(),this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"),this._mouseDestroy(),this},_mouseCapture:function(b){var c=this.options,d,e,f,g,h,i,j,k,l;return c.disabled?!1:(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),d={x:b.pageX,y:b.pageY},e=this._normValueFromMouse(d),f=this._valueMax()-this._valueMin()+1,h=this,this.handles.each(function(b){var c=Math.abs(e-h.values(b));f>c&&(f=c,g=a(this),i=b)}),c.range===!0&&this.values(1)===c.min&&(i+=1,g=a(this.handles[i])),j=this._start(b,i),j===!1?!1:(this._mouseSliding=!0,h._handleIndex=i,g.addClass("ui-state-active").focus(),k=g.offset(),l=!a(b.target).parents().andSelf().is(".ui-slider-handle"),this._clickOffset=l?{left:0,top:0}:{left:b.pageX-k.left-g.width()/2,top:b.pageY-k.top-g.height()/2-(parseInt(g.css("borderTopWidth"),10)||0)-(parseInt(g.css("borderBottomWidth"),10)||0)+(parseInt(g.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(b,i,e),this._animateOff=!0,!0))},_mouseStart:function(a){return!0},_mouseDrag:function(a){var b={x:a.pageX,y:a.pageY},c=this._normValueFromMouse(b);return this._slide(a,this._handleIndex,c),!1},_mouseStop:function(a){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(a,this._handleIndex),this._change(a,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b,c,d,e,f;return this.orientation==="horizontal"?(b=this.elementSize.width,c=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(b=this.elementSize.height,c=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),d=c/b,d>1&&(d=1),d<0&&(d=0),this.orientation==="vertical"&&(d=1-d),e=this._valueMax()-this._valueMin(),f=this._valueMin()+d*e,this._trimAlignValue(f)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};return this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("start",a,c)},_slide:function(a,b,c){var d,e,f;this.options.values&&this.options.values.length?(d=this.values(b?0:1),this.options.values.length===2&&this.options.range===!0&&(b===0&&c>d||b===1&&c<d)&&(c=d),c!==this.values(b)&&(e=this.values(),e[b]=c,f=this._trigger("slide",a,{handle:this.handles[b],value:c,values:e}),d=this.values(b?0:1),f!==!1&&this.values(b,c,!0))):c!==this.value()&&(f=this._trigger("slide",a,{handle:this.handles[b],value:c}),f!==!1&&this.value(c))},_stop:function(a,b){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("stop",a,c)},_change:function(a,b){if(!this._keySliding&&!this._mouseSliding){var c={handle:this.handles[b],value:this.value()};this.options.values&&this.options.values.length&&(c.value=this.values(b),c.values=this.values()),this._trigger("change",a,c)}},value:function(a){if(arguments.length){this.options.value=this._trimAlignValue(a),this._refreshValue(),this._change(null,0);return}return this._value()},values:function(b,c){var d,e,f;if(arguments.length>1){this.options.values[b]=this._trimAlignValue(c),this._refreshValue(),this._change(null,b);return}if(!arguments.length)return this._values();if(!a.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(b):this.value();d=this.options.values,e=arguments[0];for(f=0;f<d.length;f+=1)d[f]=this._trimAlignValue(e[f]),this._change(null,f);this._refreshValue()},_setOption:function(b,c){var d,e=0;a.isArray(this.options.values)&&(e=this.options.values.length),a.Widget.prototype._setOption.apply(this,arguments);switch(b){case"disabled":c?(this.handles.filter(".ui-state-focus").blur(),this.handles.removeClass("ui-state-hover"),this.handles.propAttr("disabled",!0),this.element.addClass("ui-disabled")):(this.handles.propAttr("disabled",!1),this.element.removeClass("ui-disabled"));break;case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":this._animateOff=!0,this._refreshValue();for(d=0;d<e;d+=1)this._change(null,d);this._animateOff=!1}},_value:function(){var a=this.options.value;return a=this._trimAlignValue(a),a},_values:function(a){var b,c,d;if(arguments.length)return b=this.options.values[a],b=this._trimAlignValue(b),b;c=this.options.values.slice();for(d=0;d<c.length;d+=1)c[d]=this._trimAlignValue(c[d]);return c},_trimAlignValue:function(a){if(a<=this._valueMin())return this._valueMin();if(a>=this._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=(a-this._valueMin())%b,d=a-c;return Math.abs(c)*2>=b&&(d+=c>0?b:-b),parseFloat(d.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var b=this.options.range,c=this.options,d=this,e=this._animateOff?!1:c.animate,f,g={},h,i,j,k;this.options.values&&this.options.values.length?this.handles.each(function(b,i){f=(d.values(b)-d._valueMin())/(d._valueMax()-d._valueMin())*100,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",a(this).stop(1,1)[e?"animate":"css"](g,c.animate),d.options.range===!0&&(d.orientation==="horizontal"?(b===0&&d.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({width:f-h+"%"},{queue:!1,duration:c.animate})):(b===0&&d.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},c.animate),b===1&&d.range[e?"animate":"css"]({height:f-h+"%"},{queue:!1,duration:c.animate}))),h=f}):(i=this.value(),j=this._valueMin(),k=this._valueMax(),f=k!==j?(i-j)/(k-j)*100:0,g[d.orientation==="horizontal"?"left":"bottom"]=f+"%",this.handle.stop(1,1)[e?"animate":"css"](g,c.animate),b==="min"&&this.orientation==="horizontal"&&this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"},c.animate),b==="max"&&this.orientation==="horizontal"&&this.range[e?"animate":"css"]({width:100-f+"%"},{queue:!1,duration:c.animate}),b==="min"&&this.orientation==="vertical"&&this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},c.animate),b==="max"&&this.orientation==="vertical"&&this.range[e?"animate":"css"]({height:100-f+"%"},{queue:!1,duration:c.animate}))}}),a.extend(a.ui.slider,{version:"1.8.23"})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.tabs.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){function e(){return++c}function f(){return++d}var c=0,d=0;a.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:!1,cookie:null,collapsible:!1,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"<div></div>",remove:null,select:null,show:null,spinner:"<em>Loading&#8230;</em>",tabTemplate:"<li><a href='#{href}'><span>#{label}</span></a></li>"},_create:function(){this._tabify(!0)},_setOption:function(a,b){if(a=="selected"){if(this.options.collapsible&&b==this.options.selected)return;this.select(b)}else this.options[a]=b,this._tabify()},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+e()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var b=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+f());return a.cookie.apply(null,[b].concat(a.makeArray(arguments)))},_ui:function(a,b){return{tab:a,panel:b,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var b=a(this);b.html(b.data("label.tabs")).removeData("label.tabs")})},_tabify:function(c){function m(b,c){b.css("display",""),!a.support.opacity&&c.opacity&&b[0].style.removeAttribute("filter")}var d=this,e=this.options,f=/^#.+/;this.list=this.element.find("ol,ul").eq(0),this.lis=a(" > li:has(a[href])",this.list),this.anchors=this.lis.map(function(){return a("a",this)[0]}),this.panels=a([]),this.anchors.each(function(b,c){var g=a(c).attr("href"),h=g.split("#")[0],i;h&&(h===location.toString().split("#")[0]||(i=a("base")[0])&&h===i.href)&&(g=c.hash,c.href=g);if(f.test(g))d.panels=d.panels.add(d.element.find(d._sanitizeSelector(g)));else if(g&&g!=="#"){a.data(c,"href.tabs",g),a.data(c,"load.tabs",g.replace(/#.*$/,""));var j=d._tabId(c);c.href="#"+j;var k=d.element.find("#"+j);k.length||(k=a(e.panelTemplate).attr("id",j).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(d.panels[b-1]||d.list),k.data("destroy.tabs",!0)),d.panels=d.panels.add(k)}else e.disabled.push(b)}),c?(this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"),this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.lis.addClass("ui-state-default ui-corner-top"),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom"),e.selected===b?(location.hash&&this.anchors.each(function(a,b){if(b.hash==location.hash)return e.selected=a,!1}),typeof e.selected!="number"&&e.cookie&&(e.selected=parseInt(d._cookie(),10)),typeof e.selected!="number"&&this.lis.filter(".ui-tabs-selected").length&&(e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"))),e.selected=e.selected||(this.lis.length?0:-1)):e.selected===null&&(e.selected=-1),e.selected=e.selected>=0&&this.anchors[e.selected]||e.selected<0?e.selected:0,e.disabled=a.unique(e.disabled.concat(a.map(this.lis.filter(".ui-state-disabled"),function(a,b){return d.lis.index(a)}))).sort(),a.inArray(e.selected,e.disabled)!=-1&&e.disabled.splice(a.inArray(e.selected,e.disabled),1),this.panels.addClass("ui-tabs-hide"),this.lis.removeClass("ui-tabs-selected ui-state-active"),e.selected>=0&&this.anchors.length&&(d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash)).removeClass("ui-tabs-hide"),this.lis.eq(e.selected).addClass("ui-tabs-selected ui-state-active"),d.element.queue("tabs",function(){d._trigger("show",null,d._ui(d.anchors[e.selected],d.element.find(d._sanitizeSelector(d.anchors[e.selected].hash))[0]))}),this.load(e.selected)),a(window).bind("unload",function(){d.lis.add(d.anchors).unbind(".tabs"),d.lis=d.anchors=d.panels=null})):e.selected=this.lis.index(this.lis.filter(".ui-tabs-selected")),this.element[e.collapsible?"addClass":"removeClass"]("ui-tabs-collapsible"),e.cookie&&this._cookie(e.selected,e.cookie);for(var g=0,h;h=this.lis[g];g++)a(h)[a.inArray(g,e.disabled)!=-1&&!a(h).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");e.cache===!1&&this.anchors.removeData("cache.tabs"),this.lis.add(this.anchors).unbind(".tabs");if(e.event!=="mouseover"){var i=function(a,b){b.is(":not(.ui-state-disabled)")&&b.addClass("ui-state-"+a)},j=function(a,b){b.removeClass("ui-state-"+a)};this.lis.bind("mouseover.tabs",function(){i("hover",a(this))}),this.lis.bind("mouseout.tabs",function(){j("hover",a(this))}),this.anchors.bind("focus.tabs",function(){i("focus",a(this).closest("li"))}),this.anchors.bind("blur.tabs",function(){j("focus",a(this).closest("li"))})}var k,l;e.fx&&(a.isArray(e.fx)?(k=e.fx[0],l=e.fx[1]):k=l=e.fx);var n=l?function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.hide().removeClass("ui-tabs-hide").animate(l,l.duration||"normal",function(){m(c,l),d._trigger("show",null,d._ui(b,c[0]))})}:function(b,c){a(b).closest("li").addClass("ui-tabs-selected ui-state-active"),c.removeClass("ui-tabs-hide"),d._trigger("show",null,d._ui(b,c[0]))},o=k?function(a,b){b.animate(k,k.duration||"normal",function(){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),m(b,k),d.element.dequeue("tabs")})}:function(a,b,c){d.lis.removeClass("ui-tabs-selected ui-state-active"),b.addClass("ui-tabs-hide"),d.element.dequeue("tabs")};this.anchors.bind(e.event+".tabs",function(){var b=this,c=a(b).closest("li"),f=d.panels.filter(":not(.ui-tabs-hide)"),g=d.element.find(d._sanitizeSelector(b.hash));if(c.hasClass("ui-tabs-selected")&&!e.collapsible||c.hasClass("ui-state-disabled")||c.hasClass("ui-state-processing")||d.panels.filter(":animated").length||d._trigger("select",null,d._ui(this,g[0]))===!1)return this.blur(),!1;e.selected=d.anchors.index(this),d.abort();if(e.collapsible){if(c.hasClass("ui-tabs-selected"))return e.selected=-1,e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){o(b,f)}).dequeue("tabs"),this.blur(),!1;if(!f.length)return e.cookie&&d._cookie(e.selected,e.cookie),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this)),this.blur(),!1}e.cookie&&d._cookie(e.selected,e.cookie);if(g.length)f.length&&d.element.queue("tabs",function(){o(b,f)}),d.element.queue("tabs",function(){n(b,g)}),d.load(d.anchors.index(this));else throw"jQuery UI Tabs: Mismatching fragment identifier.";a.browser.msie&&this.blur()}),this.anchors.bind("click.tabs",function(){return!1})},_getIndex:function(a){return typeof a=="string"&&(a=this.anchors.index(this.anchors.filter("[href$='"+a+"']"))),a},destroy:function(){var b=this.options;return this.abort(),this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs"),this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all"),this.anchors.each(function(){var b=a.data(this,"href.tabs");b&&(this.href=b);var c=a(this).unbind(".tabs");a.each(["href","load","cache"],function(a,b){c.removeData(b+".tabs")})}),this.lis.unbind(".tabs").add(this.panels).each(function(){a.data(this,"destroy.tabs")?a(this).remove():a(this).removeClass(["ui-state-default","ui-corner-top","ui-tabs-selected","ui-state-active","ui-state-hover","ui-state-focus","ui-state-disabled","ui-tabs-panel","ui-widget-content","ui-corner-bottom","ui-tabs-hide"].join(" "))}),b.cookie&&this._cookie(null,b.cookie),this},add:function(c,d,e){e===b&&(e=this.anchors.length);var f=this,g=this.options,h=a(g.tabTemplate.replace(/#\{href\}/g,c).replace(/#\{label\}/g,d)),i=c.indexOf("#")?this._tabId(a("a",h)[0]):c.replace("#","");h.addClass("ui-state-default ui-corner-top").data("destroy.tabs",!0);var j=f.element.find("#"+i);return j.length||(j=a(g.panelTemplate).attr("id",i).data("destroy.tabs",!0)),j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide"),e>=this.lis.length?(h.appendTo(this.list),j.appendTo(this.list[0].parentNode)):(h.insertBefore(this.lis[e]),j.insertBefore(this.panels[e])),g.disabled=a.map(g.disabled,function(a,b){return a>=e?++a:a}),this._tabify(),this.anchors.length==1&&(g.selected=0,h.addClass("ui-tabs-selected ui-state-active"),j.removeClass("ui-tabs-hide"),this.element.queue("tabs",function(){f._trigger("show",null,f._ui(f.anchors[0],f.panels[0]))}),this.load(0)),this._trigger("add",null,this._ui(this.anchors[e],this.panels[e])),this},remove:function(b){b=this._getIndex(b);var c=this.options,d=this.lis.eq(b).remove(),e=this.panels.eq(b).remove();return d.hasClass("ui-tabs-selected")&&this.anchors.length>1&&this.select(b+(b+1<this.anchors.length?1:-1)),c.disabled=a.map(a.grep(c.disabled,function(a,c){return a!=b}),function(a,c){return a>=b?--a:a}),this._tabify(),this._trigger("remove",null,this._ui(d.find("a")[0],e[0])),this},enable:function(b){b=this._getIndex(b);var c=this.options;if(a.inArray(b,c.disabled)==-1)return;return this.lis.eq(b).removeClass("ui-state-disabled"),c.disabled=a.grep(c.disabled,function(a,c){return a!=b}),this._trigger("enable",null,this._ui(this.anchors[b],this.panels[b])),this},disable:function(a){a=this._getIndex(a);var b=this,c=this.options;return a!=c.selected&&(this.lis.eq(a).addClass("ui-state-disabled"),c.disabled.push(a),c.disabled.sort(),this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a]))),this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;return this.anchors.eq(a).trigger(this.options.event+".tabs"),this},load:function(b){b=this._getIndex(b);var c=this,d=this.options,e=this.anchors.eq(b)[0],f=a.data(e,"load.tabs");this.abort();if(!f||this.element.queue("tabs").length!==0&&a.data(e,"cache.tabs")){this.element.dequeue("tabs");return}this.lis.eq(b).addClass("ui-state-processing");if(d.spinner){var g=a("span",e);g.data("label.tabs",g.html()).html(d.spinner)}return this.xhr=a.ajax(a.extend({},d.ajaxOptions,{url:f,success:function(f,g){c.element.find(c._sanitizeSelector(e.hash)).html(f),c._cleanup(),d.cache&&a.data(e,"cache.tabs",!0),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.success(f,g)}catch(h){}},error:function(a,f,g){c._cleanup(),c._trigger("load",null,c._ui(c.anchors[b],c.panels[b]));try{d.ajaxOptions.error(a,f,b,e)}catch(g){}}})),c.element.dequeue("tabs"),this},abort:function(){return this.element.queue([]),this.panels.stop(!1,!0),this.element.queue("tabs",this.element.queue("tabs").splice(-2,2)),this.xhr&&(this.xhr.abort(),delete this.xhr),this._cleanup(),this},url:function(a,b){return this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",b),this},length:function(){return this.anchors.length}}),a.extend(a.ui.tabs,{version:"1.8.23"}),a.extend(a.ui.tabs.prototype,{rotation:null,rotate:function(a,b){var c=this,d=this.options,e=c._rotate||(c._rotate=function(b){clearTimeout(c.rotation),c.rotation=setTimeout(function(){var a=d.selected;c.select(++a<c.anchors.length?a:0)},a),b&&b.stopPropagation()}),f=c._unrotate||(c._unrotate=b?function(a){e()}:function(a){a.clientX&&c.rotate(null)});return a?(this.element.bind("tabsshow",e),this.anchors.bind(d.event+".tabs",f),e()):(clearTimeout(c.rotation),this.element.unbind("tabsshow",e),this.anchors.unbind(d.event+".tabs",f),delete this._rotate,delete this._unrotate),this}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.datepicker.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function($,undefined){function Datepicker(){this.debug=!1,this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},$.extend(this._defaults,this.regional[""]),this.dpDiv=bindHover($('<div id="'+this._mainDivId+'" class="ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>'))}function bindHover(a){var b="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return a.bind("mouseout",function(a){var c=$(a.target).closest(b);if(!c.length)return;c.removeClass("ui-state-hover ui-datepicker-prev-hover ui-datepicker-next-hover")}).bind("mouseover",function(c){var d=$(c.target).closest(b);if($.datepicker._isDisabledDatepicker(instActive.inline?a.parent()[0]:instActive.input[0])||!d.length)return;d.parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),d.addClass("ui-state-hover"),d.hasClass("ui-datepicker-prev")&&d.addClass("ui-datepicker-prev-hover"),d.hasClass("ui-datepicker-next")&&d.addClass("ui-datepicker-next-hover")})}function extendRemove(a,b){$.extend(a,b);for(var c in b)if(b[c]==null||b[c]==undefined)a[c]=b[c];return a}function isArray(a){return a&&($.browser.safari&&typeof a=="object"&&a.length||a.constructor&&a.constructor.toString().match(/\Array\(\)/))}$.extend($.ui,{datepicker:{version:"1.8.23"}});var PROP_NAME="datepicker",dpuuid=(new Date).getTime(),instActive;$.extend(Datepicker.prototype,{markerClassName:"hasDatepicker",maxRows:4,log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(a){return extendRemove(this._defaults,a||{}),this},_attachDatepicker:function(target,settings){var inlineSettings=null;for(var attrName in this._defaults){var attrValue=target.getAttribute("date:"+attrName);if(attrValue){inlineSettings=inlineSettings||{};try{inlineSettings[attrName]=eval(attrValue)}catch(err){inlineSettings[attrName]=attrValue}}}var nodeName=target.nodeName.toLowerCase(),inline=nodeName=="div"||nodeName=="span";target.id||(this.uuid+=1,target.id="dp"+this.uuid);var inst=this._newInst($(target),inline);inst.settings=$.extend({},settings||{},inlineSettings||{}),nodeName=="input"?this._connectDatepicker(target,inst):inline&&this._inlineDatepicker(target,inst)},_newInst:function(a,b){var c=a[0].id.replace(/([^A-Za-z0-9_-])/g,"\\\\$1");return{id:c,input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:b?bindHover($('<div class="'+this._inlineClass+' ui-datepicker ui-widget ui-widget-content ui-helper-clearfix ui-corner-all"></div>')):this.dpDiv}},_connectDatepicker:function(a,b){var c=$(a);b.append=$([]),b.trigger=$([]);if(c.hasClass(this.markerClassName))return;this._attachments(c,b),c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(a,c,d){b.settings[c]=d}).bind("getData.datepicker",function(a,c){return this._get(b,c)}),this._autoSize(b),$.data(a,PROP_NAME,b),b.settings.disabled&&this._disableDatepicker(a)},_attachments:function(a,b){var c=this._get(b,"appendText"),d=this._get(b,"isRTL");b.append&&b.append.remove(),c&&(b.append=$('<span class="'+this._appendClass+'">'+c+"</span>"),a[d?"before":"after"](b.append)),a.unbind("focus",this._showDatepicker),b.trigger&&b.trigger.remove();var e=this._get(b,"showOn");(e=="focus"||e=="both")&&a.focus(this._showDatepicker);if(e=="button"||e=="both"){var f=this._get(b,"buttonText"),g=this._get(b,"buttonImage");b.trigger=$(this._get(b,"buttonImageOnly")?$("<img/>").addClass(this._triggerClass).attr({src:g,alt:f,title:f}):$('<button type="button"></button>').addClass(this._triggerClass).html(g==""?f:$("<img/>").attr({src:g,alt:f,title:f}))),a[d?"before":"after"](b.trigger),b.trigger.click(function(){return $.datepicker._datepickerShowing&&$.datepicker._lastInput==a[0]?$.datepicker._hideDatepicker():$.datepicker._datepickerShowing&&$.datepicker._lastInput!=a[0]?($.datepicker._hideDatepicker(),$.datepicker._showDatepicker(a[0])):$.datepicker._showDatepicker(a[0]),!1})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var d=function(a){var b=0,c=0;for(var d=0;d<a.length;d++)a[d].length>b&&(b=a[d].length,c=d);return c};b.setMonth(d(this._get(a,c.match(/MM/)?"monthNames":"monthNamesShort"))),b.setDate(d(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,b){var c=$(a);if(c.hasClass(this.markerClassName))return;c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(a,c,d){b.settings[c]=d}).bind("getData.datepicker",function(a,c){return this._get(b,c)}),$.data(a,PROP_NAME,b),this._setDate(b,this._getDefaultDate(b),!0),this._updateDatepicker(b),this._updateAlternate(b),b.settings.disabled&&this._disableDatepicker(a),b.dpDiv.css("display","block")},_dialogDatepicker:function(a,b,c,d,e){var f=this._dialogInst;if(!f){this.uuid+=1;var g="dp"+this.uuid;this._dialogInput=$('<input type="text" id="'+g+'" style="position: absolute; top: -100px; width: 0px;"/>'),this._dialogInput.keydown(this._doKeyDown),$("body").append(this._dialogInput),f=this._dialogInst=this._newInst(this._dialogInput,!1),f.settings={},$.data(this._dialogInput[0],PROP_NAME,f)}extendRemove(f.settings,d||{}),b=b&&b.constructor==Date?this._formatDate(f,b):b,this._dialogInput.val(b),this._pos=e?e.length?e:[e.pageX,e.pageY]:null;if(!this._pos){var h=document.documentElement.clientWidth,i=document.documentElement.clientHeight,j=document.documentElement.scrollLeft||document.body.scrollLeft,k=document.documentElement.scrollTop||document.body.scrollTop;this._pos=[h/2-100+j,i/2-150+k]}return this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),f.settings.onSelect=c,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),$.blockUI&&$.blockUI(this.dpDiv),$.data(this._dialogInput[0],PROP_NAME,f),this},_destroyDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();$.removeData(a,PROP_NAME),d=="input"?(c.append.remove(),c.trigger.remove(),b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)):(d=="div"||d=="span")&&b.removeClass(this.markerClassName).empty()},_enableDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();if(d=="input")a.disabled=!1,c.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""});else if(d=="div"||d=="span"){var e=b.children("."+this._inlineClass);e.children().removeClass("ui-state-disabled"),e.find("select.ui-datepicker-month, select.ui-datepicker-year").removeAttr("disabled")}this._disabledInputs=$.map(this._disabledInputs,function(b){return b==a?null:b})},_disableDatepicker:function(a){var b=$(a),c=$.data(a,PROP_NAME);if(!b.hasClass(this.markerClassName))return;var d=a.nodeName.toLowerCase();if(d=="input")a.disabled=!0,c.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"});else if(d=="div"||d=="span"){var e=b.children("."+this._inlineClass);e.children().addClass("ui-state-disabled"),e.find("select.ui-datepicker-month, select.ui-datepicker-year").attr("disabled","disabled")}this._disabledInputs=$.map(this._disabledInputs,function(b){return b==a?null:b}),this._disabledInputs[this._disabledInputs.length]=a},_isDisabledDatepicker:function(a){if(!a)return!1;for(var b=0;b<this._disabledInputs.length;b++)if(this._disabledInputs[b]==a)return!0;return!1},_getInst:function(a){try{return $.data(a,PROP_NAME)}catch(b){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(a,b,c){var d=this._getInst(a);if(arguments.length==2&&typeof b=="string")return b=="defaults"?$.extend({},$.datepicker._defaults):d?b=="all"?$.extend({},d.settings):this._get(d,b):null;var e=b||{};typeof b=="string"&&(e={},e[b]=c);if(d){this._curInst==d&&this._hideDatepicker();var f=this._getDateDatepicker(a,!0),g=this._getMinMaxDate(d,"min"),h=this._getMinMaxDate(d,"max");extendRemove(d.settings,e),g!==null&&e.dateFormat!==undefined&&e.minDate===undefined&&(d.settings.minDate=this._formatDate(d,g)),h!==null&&e.dateFormat!==undefined&&e.maxDate===undefined&&(d.settings.maxDate=this._formatDate(d,h)),this._attachments($(a),d),this._autoSize(d),this._setDate(d,f),this._updateAlternate(d),this._updateDatepicker(d)}},_changeDatepicker:function(a,b,c){this._optionDatepicker(a,b,c)},_refreshDatepicker:function(a){var b=this._getInst(a);b&&this._updateDatepicker(b)},_setDateDatepicker:function(a,b){var c=this._getInst(a);c&&(this._setDate(c,b),this._updateDatepicker(c),this._updateAlternate(c))},_getDateDatepicker:function(a,b){var c=this._getInst(a);return c&&!c.inline&&this._setDateFromField(c,b),c?this._getDate(c):null},_doKeyDown:function(a){var b=$.datepicker._getInst(a.target),c=!0,d=b.dpDiv.is(".ui-datepicker-rtl");b._keyEvent=!0;if($.datepicker._datepickerShowing)switch(a.keyCode){case 9:$.datepicker._hideDatepicker(),c=!1;break;case 13:var e=$("td."+$.datepicker._dayOverClass+":not(."+$.datepicker._currentClass+")",b.dpDiv);e[0]&&$.datepicker._selectDay(a.target,b.selectedMonth,b.selectedYear,e[0]);var f=$.datepicker._get(b,"onSelect");if(f){var g=$.datepicker._formatDate(b);f.apply(b.input?b.input[0]:null,[g,b])}else $.datepicker._hideDatepicker();return!1;case 27:$.datepicker._hideDatepicker();break;case 33:$.datepicker._adjustDate(a.target,a.ctrlKey?-$.datepicker._get(b,"stepBigMonths"):-$.datepicker._get(b,"stepMonths"),"M");break;case 34:$.datepicker._adjustDate(a.target,a.ctrlKey?+$.datepicker._get(b,"stepBigMonths"):+$.datepicker._get(b,"stepMonths"),"M");break;case 35:(a.ctrlKey||a.metaKey)&&$.datepicker._clearDate(a.target),c=a.ctrlKey||a.metaKey;break;case 36:(a.ctrlKey||a.metaKey)&&$.datepicker._gotoToday(a.target),c=a.ctrlKey||a.metaKey;break;case 37:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,d?1:-1,"D"),c=a.ctrlKey||a.metaKey,a.originalEvent.altKey&&$.datepicker._adjustDate(a.target,a.ctrlKey?-$.datepicker._get(b,"stepBigMonths"):-$.datepicker._get(b,"stepMonths"),"M");break;case 38:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,-7,"D"),c=a.ctrlKey||a.metaKey;break;case 39:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,d?-1:1,"D"),c=a.ctrlKey||a.metaKey,a.originalEvent.altKey&&$.datepicker._adjustDate(a.target,a.ctrlKey?+$.datepicker._get(b,"stepBigMonths"):+$.datepicker._get(b,"stepMonths"),"M");break;case 40:(a.ctrlKey||a.metaKey)&&$.datepicker._adjustDate(a.target,7,"D"),c=a.ctrlKey||a.metaKey;break;default:c=!1}else a.keyCode==36&&a.ctrlKey?$.datepicker._showDatepicker(this):c=!1;c&&(a.preventDefault(),a.stopPropagation())},_doKeyPress:function(a){var b=$.datepicker._getInst(a.target);if($.datepicker._get(b,"constrainInput")){var c=$.datepicker._possibleChars($.datepicker._get(b,"dateFormat")),d=String.fromCharCode(a.charCode==undefined?a.keyCode:a.charCode);return a.ctrlKey||a.metaKey||d<" "||!c||c.indexOf(d)>-1}},_doKeyUp:function(a){var b=$.datepicker._getInst(a.target);if(b.input.val()!=b.lastVal)try{var c=$.datepicker.parseDate($.datepicker._get(b,"dateFormat"),b.input?b.input.val():null,$.datepicker._getFormatConfig(b));c&&($.datepicker._setDateFromField(b),$.datepicker._updateAlternate(b),$.datepicker._updateDatepicker(b))}catch(d){$.datepicker.log(d)}return!0},_showDatepicker:function(a){a=a.target||a,a.nodeName.toLowerCase()!="input"&&(a=$("input",a.parentNode)[0]);if($.datepicker._isDisabledDatepicker(a)||$.datepicker._lastInput==a)return;var b=$.datepicker._getInst(a);$.datepicker._curInst&&$.datepicker._curInst!=b&&($.datepicker._curInst.dpDiv.stop(!0,!0),b&&$.datepicker._datepickerShowing&&$.datepicker._hideDatepicker($.datepicker._curInst.input[0]));var c=$.datepicker._get(b,"beforeShow"),d=c?c.apply(a,[a,b]):{};if(d===!1)return;extendRemove(b.settings,d),b.lastVal=null,$.datepicker._lastInput=a,$.datepicker._setDateFromField(b),$.datepicker._inDialog&&(a.value=""),$.datepicker._pos||($.datepicker._pos=$.datepicker._findPos(a),$.datepicker._pos[1]+=a.offsetHeight);var e=!1;$(a).parents().each(function(){return e|=$(this).css("position")=="fixed",!e}),e&&$.browser.opera&&($.datepicker._pos[0]-=document.documentElement.scrollLeft,$.datepicker._pos[1]-=document.documentElement.scrollTop);var f={left:$.datepicker._pos[0],top:$.datepicker._pos[1]};$.datepicker._pos=null,b.dpDiv.empty(),b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),$.datepicker._updateDatepicker(b),f=$.datepicker._checkOffset(b,f,e),b.dpDiv.css({position:$.datepicker._inDialog&&$.blockUI?"static":e?"fixed":"absolute",display:"none",left:f.left+"px",top:f.top+"px"});if(!b.inline){var g=$.datepicker._get(b,"showAnim"),h=$.datepicker._get(b,"duration"),i=function(){var a=b.dpDiv.find("iframe.ui-datepicker-cover");if(!!a.length){var c=$.datepicker._getBorders(b.dpDiv);a.css({left:-c[0],top:-c[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})}};b.dpDiv.zIndex($(a).zIndex()+1),$.datepicker._datepickerShowing=!0,$.effects&&$.effects[g]?b.dpDiv.show(g,$.datepicker._get(b,"showOptions"),h,i):b.dpDiv[g||"show"](g?h:null,i),(!g||!h)&&i(),b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus(),$.datepicker._curInst=b}},_updateDatepicker:function(a){var b=this;b.maxRows=4;var c=$.datepicker._getBorders(a.dpDiv);instActive=a,a.dpDiv.empty().append(this._generateHTML(a)),this._attachHandlers(a);var d=a.dpDiv.find("iframe.ui-datepicker-cover");!d.length||d.css({left:-c[0],top:-c[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}),a.dpDiv.find("."+this._dayOverClass+" a").mouseover();var e=this._getNumberOfMonths(a),f=e[1],g=17;a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),f>1&&a.dpDiv.addClass("ui-datepicker-multi-"+f).css("width",g*f+"em"),a.dpDiv[(e[0]!=1||e[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi"),a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),a==$.datepicker._curInst&&$.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&a.input[0]!=document.activeElement&&a.input.focus();if(a.yearshtml){var h=a.yearshtml;setTimeout(function(){h===a.yearshtml&&a.yearshtml&&a.dpDiv.find("select.ui-datepicker-year:first").replaceWith(a.yearshtml),h=a.yearshtml=null},0)}},_getBorders:function(a){var b=function(a){return{thin:1,medium:2,thick:3}[a]||a};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var d=a.dpDiv.outerWidth(),e=a.dpDiv.outerHeight(),f=a.input?a.input.outerWidth():0,g=a.input?a.input.outerHeight():0,h=document.documentElement.clientWidth+(c?0:$(document).scrollLeft()),i=document.documentElement.clientHeight+(c?0:$(document).scrollTop());return b.left-=this._get(a,"isRTL")?d-f:0,b.left-=c&&b.left==a.input.offset().left?$(document).scrollLeft():0,b.top-=c&&b.top==a.input.offset().top+g?$(document).scrollTop():0,b.left-=Math.min(b.left,b.left+d>h&&h>d?Math.abs(b.left+d-h):0),b.top-=Math.min(b.top,b.top+e>i&&i>e?Math.abs(e+g):0),b},_findPos:function(a){var b=this._getInst(a),c=this._get(b,"isRTL");while(a&&(a.type=="hidden"||a.nodeType!=1||$.expr.filters.hidden(a)))a=a[c?"previousSibling":"nextSibling"];var d=$(a).offset();return[d.left,d.top]},_hideDatepicker:function(a){var b=this._curInst;if(!b||a&&b!=$.data(a,PROP_NAME))return;if(this._datepickerShowing){var c=this._get(b,"showAnim"),d=this._get(b,"duration"),e=function(){$.datepicker._tidyDialog(b)};$.effects&&$.effects[c]?b.dpDiv.hide(c,$.datepicker._get(b,"showOptions"),d,e):b.dpDiv[c=="slideDown"?"slideUp":c=="fadeIn"?"fadeOut":"hide"](c?d:null,e),c||e(),this._datepickerShowing=!1;var f=this._get(b,"onClose");f&&f.apply(b.input?b.input[0]:null,[b.input?b.input.val():"",b]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),$.blockUI&&($.unblockUI(),$("body").append(this.dpDiv))),this._inDialog=!1}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(a){if(!$.datepicker._curInst)return;var b=$(a.target),c=$.datepicker._getInst(b[0]);(b[0].id!=$.datepicker._mainDivId&&b.parents("#"+$.datepicker._mainDivId).length==0&&!b.hasClass($.datepicker.markerClassName)&&!b.closest("."+$.datepicker._triggerClass).length&&$.datepicker._datepickerShowing&&(!$.datepicker._inDialog||!$.blockUI)||b.hasClass($.datepicker.markerClassName)&&$.datepicker._curInst!=c)&&$.datepicker._hideDatepicker()},_adjustDate:function(a,b,c){var d=$(a),e=this._getInst(d[0]);if(this._isDisabledDatepicker(d[0]))return;this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):0),c),this._updateDatepicker(e)},_gotoToday:function(a){var b=$(a),c=this._getInst(b[0]);if(this._get(c,"gotoCurrent")&&c.currentDay)c.selectedDay=c.currentDay,c.drawMonth=c.selectedMonth=c.currentMonth,c.drawYear=c.selectedYear=c.currentYear;else{var d=new Date;c.selectedDay=d.getDate(),c.drawMonth=c.selectedMonth=d.getMonth(),c.drawYear=c.selectedYear=d.getFullYear()}this._notifyChange(c),this._adjustDate(b)},_selectMonthYear:function(a,b,c){var d=$(a),e=this._getInst(d[0]);e["selected"+(c=="M"?"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10),this._notifyChange(e),this._adjustDate(d)},_selectDay:function(a,b,c,d){var e=$(a);if($(d).hasClass(this._unselectableClass)||this._isDisabledDatepicker(e[0]))return;var f=this._getInst(e[0]);f.selectedDay=f.currentDay=$("a",d).html(),f.selectedMonth=f.currentMonth=b,f.selectedYear=f.currentYear=c,this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))},_clearDate:function(a){var b=$(a),c=this._getInst(b[0]);this._selectDate(b,"")},_selectDate:function(a,b){var c=$(a),d=this._getInst(c[0]);b=b!=null?b:this._formatDate(d),d.input&&d.input.val(b),this._updateAlternate(d);var e=this._get(d,"onSelect");e?e.apply(d.input?d.input[0]:null,[b,d]):d.input&&d.input.trigger("change"),d.inline?this._updateDatepicker(d):(this._hideDatepicker(),this._lastInput=d.input[0],typeof d.input[0]!="object"&&d.input.focus(),this._lastInput=null)},_updateAlternate:function(a){var b=this._get(a,"altField");if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),d=this._getDate(a),e=this.formatDate(c,d,this._getFormatConfig(a));$(b).each(function(){$(this).val(e)})}},noWeekends:function(a){var b=a.getDay();return[b>0&&b<6,""]},iso8601Week:function(a){var b=new Date(a.getTime());b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b=="object"?b.toString():b+"";if(b=="")return null;var d=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff;d=typeof d!="string"?d:(new Date).getFullYear()%100+parseInt(d,10);var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,g=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,h=(c?c.monthNames:null)||this._defaults.monthNames,i=-1,j=-1,k=-1,l=-1,m=!1,n=function(b){var c=s+1<a.length&&a.charAt(s+1)==b;return c&&s++,c},o=function(a){var c=n(a),d=a=="@"?14:a=="!"?20:a=="y"&&c?4:a=="o"?3:2,e=new RegExp("^\\d{1,"+d+"}"),f=b.substring(r).match(e);if(!f)throw"Missing number at position "+r;return r+=f[0].length,parseInt(f[0],10)},p=function(a,c,d){var e=$.map(n(a)?d:c,function(a,b){return[[b,a]]}).sort(function(a,b){return-(a[1].length-b[1].length)}),f=-1;$.each(e,function(a,c){var d=c[1];if(b.substr(r,d.length).toLowerCase()==d.toLowerCase())return f=c[0],r+=d.length,!1});if(f!=-1)return f+1;throw"Unknown name at position "+r},q=function(){if(b.charAt(r)!=a.charAt(s))throw"Unexpected literal at position "+r;r++},r=0;for(var s=0;s<a.length;s++)if(m)a.charAt(s)=="'"&&!n("'")?m=!1:q();else switch(a.charAt(s)){case"d":k=o("d");break;case"D":p("D",e,f);break;case"o":l=o("o");break;case"m":j=o("m");break;case"M":j=p("M",g,h);break;case"y":i=o("y");break;case"@":var t=new Date(o("@"));i=t.getFullYear(),j=t.getMonth()+1,k=t.getDate();break;case"!":var t=new Date((o("!")-this._ticksTo1970)/1e4);i=t.getFullYear(),j=t.getMonth()+1,k=t.getDate();break;case"'":n("'")?q():m=!0;break;default:q()}if(r<b.length)throw"Extra/unparsed characters found in date: "+b.substring(r);i==-1?i=(new Date).getFullYear():i<100&&(i+=(new Date).getFullYear()-(new Date).getFullYear()%100+(i<=d?0:-100));if(l>-1){j=1,k=l;do{var u=this._getDaysInMonth(i,j-1);if(k<=u)break;j++,k-=u}while(!0)}var t=this._daylightSavingAdjust(new Date(i,j-1,k));if(t.getFullYear()!=i||t.getMonth()+1!=j||t.getDate()!=k)throw"Invalid date";return t},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24*60*60*1e7,formatDate:function(a,b,c){if(!b)return"";var d=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,e=(c?c.dayNames:null)||this._defaults.dayNames,f=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,h=function(b){var c=m+1<a.length&&a.charAt(m+1)==b;return c&&m++,c},i=function(a,b,c){var d=""+b;if(h(a))while(d.length<c)d="0"+d;return d},j=function(a,b,c,d){return h(a)?d[b]:c[b]},k="",l=!1;if(b)for(var m=0;m<a.length;m++)if(l)a.charAt(m)=="'"&&!h("'")?l=!1:k+=a.charAt(m);else switch(a.charAt(m)){case"d":k+=i("d",b.getDate(),2);break;case"D":k+=j("D",b.getDay(),d,e);break;case"o":k+=i("o",Math.round(((new Date(b.getFullYear(),b.getMonth(),b.getDate())).getTime()-(new Date(b.getFullYear(),0,0)).getTime())/864e5),3);break;case"m":k+=i("m",b.getMonth()+1,2);break;case"M":k+=j("M",b.getMonth(),f,g);break;case"y":k+=h("y")?b.getFullYear():(b.getYear()%100<10?"0":"")+b.getYear()%100;break;case"@":k+=b.getTime();break;case"!":k+=b.getTime()*1e4+this._ticksTo1970;break;case"'":h("'")?k+="'":l=!0;break;default:k+=a.charAt(m)}return k},_possibleChars:function(a){var b="",c=!1,d=function(b){var c=e+1<a.length&&a.charAt(e+1)==b;return c&&e++,c};for(var e=0;e<a.length;e++)if(c)a.charAt(e)=="'"&&!d("'")?c=!1:b+=a.charAt(e);else switch(a.charAt(e)){case"d":case"m":case"y":case"@":b+="0123456789";break;case"D":case"M":return null;case"'":d("'")?b+="'":c=!0;break;default:b+=a.charAt(e)}return b},_get:function(a,b){return a.settings[b]!==undefined?a.settings[b]:this._defaults[b]},_setDateFromField:function(a,b){if(a.input.val()==a.lastVal)return;var c=this._get(a,"dateFormat"),d=a.lastVal=a.input?a.input.val():null,e,f;e=f=this._getDefaultDate(a);var g=this._getFormatConfig(a);try{e=this.parseDate(c,d,g)||f}catch(h){this.log(h),d=b?"":d}a.selectedDay=e.getDate(),a.drawMonth=a.selectedMonth=e.getMonth(),a.drawYear=a.selectedYear=e.getFullYear(),a.currentDay=d?e.getDate():0,a.currentMonth=d?e.getMonth():0,a.currentYear=d?e.getFullYear():0,this._adjustInstDate(a)},_getDefaultDate:function(a){return this._restrictMinMax(a,this._determineDate(a,this._get(a,"defaultDate"),new Date))},_determineDate:function(a,b,c){var d=function(a){var b=new Date;return b.setDate(b.getDate()+a),b},e=function(b){try{return $.datepicker.parseDate($.datepicker._get(a,"dateFormat"),b,$.datepicker._getFormatConfig(a))}catch(c){}var d=(b.toLowerCase().match(/^c/)?$.datepicker._getDate(a):null)||new Date,e=d.getFullYear(),f=d.getMonth(),g=d.getDate(),h=/([+-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,i=h.exec(b);while(i){switch(i[2]||"d"){case"d":case"D":g+=parseInt(i[1],10);break;case"w":case"W":g+=parseInt(i[1],10)*7;break;case"m":case"M":f+=parseInt(i[1],10),g=Math.min(g,$.datepicker._getDaysInMonth(e,f));break;case"y":case"Y":e+=parseInt(i[1],10),g=Math.min(g,$.datepicker._getDaysInMonth(e,f))}i=h.exec(b)}return new Date(e,f,g)},f=b==null||b===""?c:typeof b=="string"?e(b):typeof b=="number"?isNaN(b)?c:d(b):new Date(b.getTime());return f=f&&f.toString()=="Invalid Date"?c:f,f&&(f.setHours(0),f.setMinutes(0),f.setSeconds(0),f.setMilliseconds(0)),this._daylightSavingAdjust(f)},_daylightSavingAdjust:function(a){return a?(a.setHours(a.getHours()>12?a.getHours()+2:0),a):null},_setDate:function(a,b,c){var d=!b,e=a.selectedMonth,f=a.selectedYear,g=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=g.getDate(),a.drawMonth=a.selectedMonth=a.currentMonth=g.getMonth(),a.drawYear=a.selectedYear=a.currentYear=g.getFullYear(),(e!=a.selectedMonth||f!=a.selectedYear)&&!c&&this._notifyChange(a),this._adjustInstDate(a),a.input&&a.input.val(d?"":this._formatDate(a))},_getDate:function(a){var b=!a.currentYear||a.input&&a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return b},_attachHandlers:function(a){var b=this._get(a,"stepMonths"),c="#"+a.id.replace(/\\\\/g,"\\");a.dpDiv.find("[data-handler]").map(function(){var a={prev:function(){window["DP_jQuery_"+dpuuid].datepicker._adjustDate(c,-b,"M")},next:function(){window["DP_jQuery_"+dpuuid].datepicker._adjustDate(c,+b,"M")},hide:function(){window["DP_jQuery_"+dpuuid].datepicker._hideDatepicker()},today:function(){window["DP_jQuery_"+dpuuid].datepicker._gotoToday(c)},selectDay:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectDay(c,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectMonthYear(c,this,"M"),!1},selectYear:function(){return window["DP_jQuery_"+dpuuid].datepicker._selectMonthYear(c,this,"Y"),!1}};$(this).bind(this.getAttribute("data-event"),a[this.getAttribute("data-handler")])})},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),d=this._get(a,"showButtonPanel"),e=this._get(a,"hideIfNoPrevNext"),f=this._get(a,"navigationAsDateFormat"),g=this._getNumberOfMonths(a),h=this._get(a,"showCurrentAtPos"),i=this._get(a,"stepMonths"),j=g[0]!=1||g[1]!=1,k=this._daylightSavingAdjust(a.currentDay?new Date(a.currentYear,a.currentMonth,a.currentDay):new Date(9999,9,9)),l=this._getMinMaxDate(a,"min"),m=this._getMinMaxDate(a,"max"),n=a.drawMonth-h,o=a.drawYear;n<0&&(n+=12,o--);if(m){var p=this._daylightSavingAdjust(new Date(m.getFullYear(),m.getMonth()-g[0]*g[1]+1,m.getDate()));p=l&&p<l?l:p;while(this._daylightSavingAdjust(new Date(o,n,1))>p)n--,n<0&&(n=11,o--)}a.drawMonth=n,a.drawYear=o;var q=this._get(a,"prevText");q=f?this.formatDate(q,this._daylightSavingAdjust(new Date(o,n-i,1)),this._getFormatConfig(a)):q;var r=this._canAdjustMonth(a,-1,o,n)?'<a class="ui-datepicker-prev ui-corner-all" data-handler="prev" data-event="click" title="'+q+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"e":"w")+'">'+q+"</span></a>":e?"":'<a class="ui-datepicker-prev ui-corner-all ui-state-disabled" title="'+q+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"e":"w")+'">'+q+"</span></a>",s=this._get(a,"nextText");s=f?this.formatDate(s,this._daylightSavingAdjust(new Date(o,n+i,1)),this._getFormatConfig(a)):s;var t=this._canAdjustMonth(a,1,o,n)?'<a class="ui-datepicker-next ui-corner-all" data-handler="next" data-event="click" title="'+s+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"w":"e")+'">'+s+"</span></a>":e?"":'<a class="ui-datepicker-next ui-corner-all ui-state-disabled" title="'+s+'"><span class="ui-icon ui-icon-circle-triangle-'+(c?"w":"e")+'">'+s+"</span></a>",u=this._get(a,"currentText"),v=this._get(a,"gotoCurrent")&&a.currentDay?k:b;u=f?this.formatDate(u,v,this._getFormatConfig(a)):u;var w=a.inline?"":'<button type="button" class="ui-datepicker-close ui-state-default ui-priority-primary ui-corner-all" data-handler="hide" data-event="click">'+this._get(a,"closeText")+"</button>",x=d?'<div class="ui-datepicker-buttonpane ui-widget-content">'+(c?w:"")+(this._isInRange(a,v)?'<button type="button" class="ui-datepicker-current ui-state-default ui-priority-secondary ui-corner-all" data-handler="today" data-event="click">'+u+"</button>":"")+(c?"":w)+"</div>":"",y=parseInt(this._get(a,"firstDay"),10);y=isNaN(y)?0:y;var z=this._get(a,"showWeek"),A=this._get(a,"dayNames"),B=this._get(a,"dayNamesShort"),C=this._get(a,"dayNamesMin"),D=this._get(a,"monthNames"),E=this._get(a,"monthNamesShort"),F=this._get(a,"beforeShowDay"),G=this._get(a,"showOtherMonths"),H=this._get(a,"selectOtherMonths"),I=this._get(a,"calculateWeek")||this.iso8601Week,J=this._getDefaultDate(a),K="";for(var L=0;L<g[0];L++){var M="";this.maxRows=4;for(var N=0;N<g[1];N++){var O=this._daylightSavingAdjust(new Date(o,n,a.selectedDay)),P=" ui-corner-all",Q="";if(j){Q+='<div class="ui-datepicker-group';if(g[1]>1)switch(N){case 0:Q+=" ui-datepicker-group-first",P=" ui-corner-"+(c?"right":"left");break;case g[1]-1:Q+=" ui-datepicker-group-last",P=" ui-corner-"+(c?"left":"right");break;default:Q+=" ui-datepicker-group-middle",P=""}Q+='">'}Q+='<div class="ui-datepicker-header ui-widget-header ui-helper-clearfix'+P+'">'+(/all|left/.test(P)&&L==0?c?t:r:"")+(/all|right/.test(P)&&L==0?c?r:t:"")+this._generateMonthYearHeader(a,n,o,l,m,L>0||N>0,D,E)+'</div><table class="ui-datepicker-calendar"><thead>'+"<tr>";var R=z?'<th class="ui-datepicker-week-col">'+this._get(a,"weekHeader")+"</th>":"";for(var S=0;S<7;S++){var T=(S+y)%7;R+="<th"+((S+y+6)%7>=5?' class="ui-datepicker-week-end"':"")+">"+'<span title="'+A[T]+'">'+C[T]+"</span></th>"}Q+=R+"</tr></thead><tbody>";var U=this._getDaysInMonth(o,n);o==a.selectedYear&&n==a.selectedMonth&&(a.selectedDay=Math.min(a.selectedDay,U));var V=(this._getFirstDayOfMonth(o,n)-y+7)%7,W=Math.ceil((V+U)/7),X=j?this.maxRows>W?this.maxRows:W:W;this.maxRows=X;var Y=this._daylightSavingAdjust(new Date(o,n,1-V));for(var Z=0;Z<X;Z++){Q+="<tr>";var _=z?'<td class="ui-datepicker-week-col">'+this._get(a,"calculateWeek")(Y)+"</td>":"";for(var S=0;S<7;S++){var ba=F?F.apply(a.input?a.input[0]:null,[Y]):[!0,""],bb=Y.getMonth()!=n,bc=bb&&!H||!ba[0]||l&&Y<l||m&&Y>m;_+='<td class="'+((S+y+6)%7>=5?" ui-datepicker-week-end":"")+(bb?" ui-datepicker-other-month":"")+(Y.getTime()==O.getTime()&&n==a.selectedMonth&&a._keyEvent||J.getTime()==Y.getTime()&&J.getTime()==O.getTime()?" "+this._dayOverClass:"")+(bc?" "+this._unselectableClass+" ui-state-disabled":"")+(bb&&!G?"":" "+ba[1]+(Y.getTime()==k.getTime()?" "+this._currentClass:"")+(Y.getTime()==b.getTime()?" ui-datepicker-today":""))+'"'+((!bb||G)&&ba[2]?' title="'+ba[2]+'"':"")+(bc?"":' data-handler="selectDay" data-event="click" data-month="'+Y.getMonth()+'" data-year="'+Y.getFullYear()+'"')+">"+(bb&&!G?"&#xa0;":bc?'<span class="ui-state-default">'+Y.getDate()+"</span>":'<a class="ui-state-default'+(Y.getTime()==b.getTime()?" ui-state-highlight":"")+(Y.getTime()==k.getTime()?" ui-state-active":"")+(bb?" ui-priority-secondary":"")+'" href="#">'+Y.getDate()+"</a>")+"</td>",Y.setDate(Y.getDate()+1),Y=this._daylightSavingAdjust(Y)}Q+=_+"</tr>"}n++,n>11&&(n=0,o++),Q+="</tbody></table>"+(j?"</div>"+(g[0]>0&&N==g[1]-1?'<div class="ui-datepicker-row-break"></div>':""):""),M+=Q}K+=M}return K+=x+($.browser.msie&&parseInt($.browser.version,10)<7&&!a.inline?'<iframe src="javascript:false;" class="ui-datepicker-cover" frameborder="0"></iframe>':""),a._keyEvent=!1,K},_generateMonthYearHeader:function(a,b,c,d,e,f,g,h){var i=this._get(a,"changeMonth"),j=this._get(a,"changeYear"),k=this._get(a,"showMonthAfterYear"),l='<div class="ui-datepicker-title">',m="";if(f||!i)m+='<span class="ui-datepicker-month">'+g[b]+"</span>";else{var n=d&&d.getFullYear()==c,o=e&&e.getFullYear()==c;m+='<select class="ui-datepicker-month" data-handler="selectMonth" data-event="change">';for(var p=0;p<12;p++)(!n||p>=d.getMonth())&&(!o||p<=e.getMonth())&&(m+='<option value="'+p+'"'+(p==b?' selected="selected"':"")+">"+h[p]+"</option>");m+="</select>"}k||(l+=m+(f||!i||!j?"&#xa0;":""));if(!a.yearshtml){a.yearshtml="";if(f||!j)l+='<span class="ui-datepicker-year">'+c+"</span>";else{var q=this._get(a,"yearRange").split(":"),r=(new Date).getFullYear(),s=function(a){var b=a.match(/c[+-].*/)?c+parseInt(a.substring(1),10):a.match(/[+-].*/)?r+parseInt(a,10):parseInt(a,10);return isNaN(b)?r:b},t=s(q[0]),u=Math.max(t,s(q[1]||""));t=d?Math.max(t,d.getFullYear()):t,u=e?Math.min(u,e.getFullYear()):u,a.yearshtml+='<select class="ui-datepicker-year" data-handler="selectYear" data-event="change">';for(;t<=u;t++)a.yearshtml+='<option value="'+t+'"'+(t==c?' selected="selected"':"")+">"+t+"</option>";a.yearshtml+="</select>",l+=a.yearshtml,a.yearshtml=null}}return l+=this._get(a,"yearSuffix"),k&&(l+=(f||!i||!j?"&#xa0;":"")+m),l+="</div>",l},_adjustInstDate:function(a,b,c){var d=a.drawYear+(c=="Y"?b:0),e=a.drawMonth+(c=="M"?b:0),f=Math.min(a.selectedDay,this._getDaysInMonth(d,e))+(c=="D"?b:0),g=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(d,e,f)));a.selectedDay=g.getDate(),a.drawMonth=a.selectedMonth=g.getMonth(),a.drawYear=a.selectedYear=g.getFullYear(),(c=="M"||c=="Y")&&this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max"),e=c&&b<c?c:b;return e=d&&e>d?d:e,e},_notifyChange:function(a){var b=this._get(a,"onChangeMonthYear");b&&b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){var b=this._get(a,"numberOfMonths");return b==null?[1,1]:typeof b=="number"?[1,b]:b},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-this._daylightSavingAdjust(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,d){var e=this._getNumberOfMonths(a),f=this._daylightSavingAdjust(new Date(c,d+(b<0?b:e[0]*e[1]),1));return b<0&&f.setDate(this._getDaysInMonth(f.getFullYear(),f.getMonth())),this._isInRange(a,f)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min"),d=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!d||b.getTime()<=d.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");return b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10),{shortYearCutoff:b,dayNamesShort:this._get(a,"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,d){b||(a.currentDay=a.selectedDay,a.currentMonth=a.selectedMonth,a.currentYear=a.selectedYear);var e=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(d,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),e,this._getFormatConfig(a))}}),$.fn.datepicker=function(a){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find("body").append($.datepicker.dpDiv),$.datepicker.initialized=!0);var b=Array.prototype.slice.call(arguments,1);return typeof a!="string"||a!="isDisabled"&&a!="getDate"&&a!="widget"?a=="option"&&arguments.length==2&&typeof arguments[1]=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b)):this.each(function(){typeof a=="string"?$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this].concat(b)):$.datepicker._attachDatepicker(this,a)}):$.datepicker["_"+a+"Datepicker"].apply($.datepicker,[this[0]].concat(b))},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.8.23",window["DP_jQuery_"+dpuuid]=$})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.ui.progressbar.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.widget("ui.progressbar",{options:{value:0,max:100},min:0,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.options.max,"aria-valuenow":this._value()}),this.valueDiv=a("<div class='ui-progressbar-value ui-widget-header ui-corner-left'></div>").appendTo(this.element),this.oldValue=this._value(),this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove(),a.Widget.prototype.destroy.apply(this,arguments)},value:function(a){return a===b?this._value():(this._setOption("value",a),this)},_setOption:function(b,c){b==="value"&&(this.options.value=c,this._refreshValue(),this._value()===this.options.max&&this._trigger("complete")),a.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;return typeof a!="number"&&(a=0),Math.min(this.options.max,Math.max(this.min,a))},_percentage:function(){return 100*this._value()/this.options.max},_refreshValue:function(){var a=this.value(),b=this._percentage();this.oldValue!==a&&(this.oldValue=a,this._trigger("change")),this.valueDiv.toggle(a>this.min).toggleClass("ui-corner-right",a===this.options.max).width(b.toFixed(0)+"%"),this.element.attr("aria-valuenow",a)}}),a.extend(a.ui.progressbar,{version:"1.8.23"})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.core.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+jQuery.effects||function(a,b){function c(b){var c;return b&&b.constructor==Array&&b.length==3?b:(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(b))?[parseInt(c[1],10),parseInt(c[2],10),parseInt(c[3],10)]:(c=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(b))?[parseFloat(c[1])*2.55,parseFloat(c[2])*2.55,parseFloat(c[3])*2.55]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(b))?[parseInt(c[1],16),parseInt(c[2],16),parseInt(c[3],16)]:(c=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(b))?[parseInt(c[1]+c[1],16),parseInt(c[2]+c[2],16),parseInt(c[3]+c[3],16)]:(c=/rgba\(0, 0, 0, 0\)/.exec(b))?e.transparent:e[a.trim(b).toLowerCase()]}function d(b,d){var e;do{e=(a.curCSS||a.css)(b,d);if(e!=""&&e!="transparent"||a.nodeName(b,"body"))break;d="backgroundColor"}while(b=b.parentNode);return c(e)}function h(){var a=document.defaultView?document.defaultView.getComputedStyle(this,null):this.currentStyle,b={},c,d;if(a&&a.length&&a[0]&&a[a[0]]){var e=a.length;while(e--)c=a[e],typeof a[c]=="string"&&(d=c.replace(/\-(\w)/g,function(a,b){return b.toUpperCase()}),b[d]=a[c])}else for(c in a)typeof a[c]=="string"&&(b[c]=a[c]);return b}function i(b){var c,d;for(c in b)d=b[c],(d==null||a.isFunction(d)||c in g||/scrollbar/.test(c)||!/color/i.test(c)&&isNaN(parseFloat(d)))&&delete b[c];return b}function j(a,b){var c={_:0},d;for(d in b)a[d]!=b[d]&&(c[d]=b[d]);return c}function k(b,c,d,e){typeof b=="object"&&(e=c,d=null,c=b,b=c.effect),a.isFunction(c)&&(e=c,d=null,c={});if(typeof c=="number"||a.fx.speeds[c])e=d,d=c,c={};return a.isFunction(d)&&(e=d,d=null),c=c||{},d=d||c.duration,d=a.fx.off?0:typeof d=="number"?d:d in a.fx.speeds?a.fx.speeds[d]:a.fx.speeds._default,e=e||c.complete,[b,c,d,e]}function l(b){return!b||typeof b=="number"||a.fx.speeds[b]?!0:typeof b=="string"&&!a.effects[b]?!0:!1}a.effects={},a.each(["backgroundColor","borderBottomColor","borderLeftColor","borderRightColor","borderTopColor","borderColor","color","outlineColor"],function(b,e){a.fx.step[e]=function(a){a.colorInit||(a.start=d(a.elem,e),a.end=c(a.end),a.colorInit=!0),a.elem.style[e]="rgb("+Math.max(Math.min(parseInt(a.pos*(a.end[0]-a.start[0])+a.start[0],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[1]-a.start[1])+a.start[1],10),255),0)+","+Math.max(Math.min(parseInt(a.pos*(a.end[2]-a.start[2])+a.start[2],10),255),0)+")"}});var e={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0],transparent:[255,255,255]},f=["add","remove","toggle"],g={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};a.effects.animateClass=function(b,c,d,e){return a.isFunction(d)&&(e=d,d=null),this.queue(function(){var g=a(this),k=g.attr("style")||" ",l=i(h.call(this)),m,n=g.attr("class")||"";a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),m=i(h.call(this)),g.attr("class",n),g.animate(j(l,m),{queue:!1,duration:c,easing:d,complete:function(){a.each(f,function(a,c){b[c]&&g[c+"Class"](b[c])}),typeof g.attr("style")=="object"?(g.attr("style").cssText="",g.attr("style").cssText=k):g.attr("style",k),e&&e.apply(this,arguments),a.dequeue(this)}})})},a.fn.extend({_addClass:a.fn.addClass,addClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{add:b},c,d,e]):this._addClass(b)},_removeClass:a.fn.removeClass,removeClass:function(b,c,d,e){return c?a.effects.animateClass.apply(this,[{remove:b},c,d,e]):this._removeClass(b)},_toggleClass:a.fn.toggleClass,toggleClass:function(c,d,e,f,g){return typeof d=="boolean"||d===b?e?a.effects.animateClass.apply(this,[d?{add:c}:{remove:c},e,f,g]):this._toggleClass(c,d):a.effects.animateClass.apply(this,[{toggle:c},d,e,f])},switchClass:function(b,c,d,e,f){return a.effects.animateClass.apply(this,[{add:c,remove:b},d,e,f])}}),a.extend(a.effects,{version:"1.8.23",save:function(a,b){for(var c=0;c<b.length;c++)b[c]!==null&&a.data("ec.storage."+b[c],a[0].style[b[c]])},restore:function(a,b){for(var c=0;c<b.length;c++)b[c]!==null&&a.css(b[c],a.data("ec.storage."+b[c]))},setMode:function(a,b){return b=="toggle"&&(b=a.is(":hidden")?"show":"hide"),b},getBaseline:function(a,b){var c,d;switch(a[0]){case"top":c=0;break;case"middle":c=.5;break;case"bottom":c=1;break;default:c=a[0]/b.height}switch(a[1]){case"left":d=0;break;case"center":d=.5;break;case"right":d=1;break;default:d=a[1]/b.width}return{x:d,y:c}},createWrapper:function(b){if(b.parent().is(".ui-effects-wrapper"))return b.parent();var c={width:b.outerWidth(!0),height:b.outerHeight(!0),"float":b.css("float")},d=a("<div></div>").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e=document.activeElement;try{e.id}catch(f){e=document.body}return b.wrap(d),(b[0]===e||a.contains(b[0],e))&&a(e).focus(),d=b.parent(),b.css("position")=="static"?(d.css({position:"relative"}),b.css({position:"relative"})):(a.extend(c,{position:b.css("position"),zIndex:b.css("z-index")}),a.each(["top","left","bottom","right"],function(a,d){c[d]=b.css(d),isNaN(parseInt(c[d],10))&&(c[d]="auto")}),b.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),d.css(c).show()},removeWrapper:function(b){var c,d=document.activeElement;return b.parent().is(".ui-effects-wrapper")?(c=b.parent().replaceWith(b),(b[0]===d||a.contains(b[0],d))&&a(d).focus(),c):b},setTransition:function(b,c,d,e){return e=e||{},a.each(c,function(a,c){var f=b.cssUnit(c);f[0]>0&&(e[c]=f[0]*d+f[1])}),e}}),a.fn.extend({effect:function(b,c,d,e){var f=k.apply(this,arguments),g={options:f[1],duration:f[2],callback:f[3]},h=g.options.mode,i=a.effects[b];return a.fx.off||!i?h?this[h](g.duration,g.callback):this.each(function(){g.callback&&g.callback.call(this)}):i.call(this,g)},_show:a.fn.show,show:function(a){if(l(a))return this._show.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="show",this.effect.apply(this,b)},_hide:a.fn.hide,hide:function(a){if(l(a))return this._hide.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="hide",this.effect.apply(this,b)},__toggle:a.fn.toggle,toggle:function(b){if(l(b)||typeof b=="boolean"||a.isFunction(b))return this.__toggle.apply(this,arguments);var c=k.apply(this,arguments);return c[1].mode="toggle",this.effect.apply(this,c)},cssUnit:function(b){var c=this.css(b),d=[];return a.each(["em","px","%","pt"],function(a,b){c.indexOf(b)>0&&(d=[parseFloat(c),b])}),d}});var m={};a.each(["Quad","Cubic","Quart","Quint","Expo"],function(a,b){m[b]=function(b){return Math.pow(b,a+2)}}),a.extend(m,{Sine:function(a){return 1-Math.cos(a*Math.PI/2)},Circ:function(a){return 1-Math.sqrt(1-a*a)},Elastic:function(a){return a===0||a===1?a:-Math.pow(2,8*(a-1))*Math.sin(((a-1)*80-7.5)*Math.PI/15)},Back:function(a){return a*a*(3*a-2)},Bounce:function(a){var b,c=4;while(a<((b=Math.pow(2,--c))-1)/11);return 1/Math.pow(4,3-c)-7.5625*Math.pow((b*3-2)/22-a,2)}}),a.each(m,function(b,c){a.easing["easeIn"+b]=c,a.easing["easeOut"+b]=function(a){return 1-c(1-a)},a.easing["easeInOut"+b]=function(a){return a<.5?c(a*2)/2:c(a*-2+2)/-2+1}})}(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.blind.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.blind=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"vertical";a.effects.save(c,d),c.show();var g=a.effects.createWrapper(c).css({overflow:"hidden"}),h=f=="vertical"?"height":"width",i=f=="vertical"?g.height():g.width();e=="show"&&g.css(h,0);var j={};j[h]=e=="show"?i:0,g.animate(j,b.duration,b.options.easing,function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.bounce.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.bounce=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"effect"),f=b.options.direction||"up",g=b.options.distance||20,h=b.options.times||5,i=b.duration||250;/show|hide/.test(e)&&d.push("opacity"),a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var j=f=="up"||f=="down"?"top":"left",k=f=="up"||f=="left"?"pos":"neg",g=b.options.distance||(j=="top"?c.outerHeight(!0)/3:c.outerWidth(!0)/3);e=="show"&&c.css("opacity",0).css(j,k=="pos"?-g:g),e=="hide"&&(g=g/(h*2)),e!="hide"&&h--;if(e=="show"){var l={opacity:1};l[j]=(k=="pos"?"+=":"-=")+g,c.animate(l,i/2,b.options.easing),g=g/2,h--}for(var m=0;m<h;m++){var n={},p={};n[j]=(k=="pos"?"-=":"+=")+g,p[j]=(k=="pos"?"+=":"-=")+g,c.animate(n,i/2,b.options.easing).animate(p,i/2,b.options.easing),g=e=="hide"?g*2:g/2}if(e=="hide"){var l={opacity:0};l[j]=(k=="pos"?"-=":"+=")+g,c.animate(l,i/2,b.options.easing,function(){c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)})}else{var n={},p={};n[j]=(k=="pos"?"-=":"+=")+g,p[j]=(k=="pos"?"+=":"-=")+g,c.animate(n,i/2,b.options.easing).animate(p,i/2,b.options.easing,function(){a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)})}c.queue("fx",function(){c.dequeue()}),c.dequeue()})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.clip.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.clip=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","height","width"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"vertical";a.effects.save(c,d),c.show();var g=a.effects.createWrapper(c).css({overflow:"hidden"}),h=c[0].tagName=="IMG"?g:c,i={size:f=="vertical"?"height":"width",position:f=="vertical"?"top":"left"},j=f=="vertical"?h.height():h.width();e=="show"&&(h.css(i.size,0),h.css(i.position,j/2));var k={};k[i.size]=e=="show"?j:0,k[i.position]=e=="show"?0:j/2,h.animate(k,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.drop.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.drop=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","opacity"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.direction||"left";a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var g=f=="up"||f=="down"?"top":"left",h=f=="up"||f=="left"?"pos":"neg",i=b.options.distance||(g=="top"?c.outerHeight(!0)/2:c.outerWidth(!0)/2);e=="show"&&c.css("opacity",0).css(g,h=="pos"?-i:i);var j={opacity:e=="show"?1:0};j[g]=(e=="show"?h=="pos"?"+=":"-=":h=="pos"?"-=":"+=")+i,c.animate(j,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.explode.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.explode=function(b){return this.queue(function(){var c=b.options.pieces?Math.round(Math.sqrt(b.options.pieces)):3,d=b.options.pieces?Math.round(Math.sqrt(b.options.pieces)):3;b.options.mode=b.options.mode=="toggle"?a(this).is(":visible")?"hide":"show":b.options.mode;var e=a(this).show().css("visibility","hidden"),f=e.offset();f.top-=parseInt(e.css("marginTop"),10)||0,f.left-=parseInt(e.css("marginLeft"),10)||0;var g=e.outerWidth(!0),h=e.outerHeight(!0);for(var i=0;i<c;i++)for(var j=0;j<d;j++)e.clone().appendTo("body").wrap("<div></div>").css({position:"absolute",visibility:"visible",left:-j*(g/d),top:-i*(h/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:g/d,height:h/c,left:f.left+j*(g/d)+(b.options.mode=="show"?(j-Math.floor(d/2))*(g/d):0),top:f.top+i*(h/c)+(b.options.mode=="show"?(i-Math.floor(c/2))*(h/c):0),opacity:b.options.mode=="show"?0:1}).animate({left:f.left+j*(g/d)+(b.options.mode=="show"?0:(j-Math.floor(d/2))*(g/d)),top:f.top+i*(h/c)+(b.options.mode=="show"?0:(i-Math.floor(c/2))*(h/c)),opacity:b.options.mode=="show"?1:0},b.duration||500);setTimeout(function(){b.options.mode=="show"?e.css({visibility:"visible"}):e.css({visibility:"visible"}).hide(),b.callback&&b.callback.apply(e[0]),e.dequeue(),a("div.ui-effects-explode").remove()},b.duration||500)})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.fade.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.fade=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"hide");c.animate({opacity:d},{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.fold.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.fold=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"hide"),f=b.options.size||15,g=!!b.options.horizFirst,h=b.duration?b.duration/2:a.fx.speeds._default/2;a.effects.save(c,d),c.show();var i=a.effects.createWrapper(c).css({overflow:"hidden"}),j=e=="show"!=g,k=j?["width","height"]:["height","width"],l=j?[i.width(),i.height()]:[i.height(),i.width()],m=/([0-9]+)%/.exec(f);m&&(f=parseInt(m[1],10)/100*l[e=="hide"?0:1]),e=="show"&&i.css(g?{height:0,width:f}:{height:f,width:0});var n={},p={};n[k[0]]=e=="show"?l[0]:f,p[k[1]]=e=="show"?l[1]:0,i.animate(n,h,b.options.easing).animate(p,h,b.options.easing,function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.highlight.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.highlight=function(b){return this.queue(function(){var c=a(this),d=["backgroundImage","backgroundColor","opacity"],e=a.effects.setMode(c,b.options.mode||"show"),f={backgroundColor:c.css("backgroundColor")};e=="hide"&&(f.opacity=0),a.effects.save(c,d),c.show().css({backgroundImage:"none",backgroundColor:b.options.color||"#ffff99"}).animate(f,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),e=="show"&&!a.support.opacity&&this.style.removeAttribute("filter"),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.pulsate.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.pulsate=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"show"),e=(b.options.times||5)*2-1,f=b.duration?b.duration/2:a.fx.speeds._default/2,g=c.is(":visible"),h=0;g||(c.css("opacity",0).show(),h=1),(d=="hide"&&g||d=="show"&&!g)&&e--;for(var i=0;i<e;i++)c.animate({opacity:h},f,b.options.easing),h=(h+1)%2;c.animate({opacity:h},f,b.options.easing,function(){h==0&&c.hide(),b.callback&&b.callback.apply(this,arguments)}),c.queue("fx",function(){c.dequeue()}).dequeue()})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.scale.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.puff=function(b){return this.queue(function(){var c=a(this),d=a.effects.setMode(c,b.options.mode||"hide"),e=parseInt(b.options.percent,10)||150,f=e/100,g={height:c.height(),width:c.width()};a.extend(b.options,{fade:!0,mode:d,percent:d=="hide"?e:100,from:d=="hide"?g:{height:g.height*f,width:g.width*f}}),c.effect("scale",b.options,b.duration,b.callback),c.dequeue()})},a.effects.scale=function(b){return this.queue(function(){var c=a(this),d=a.extend(!0,{},b.options),e=a.effects.setMode(c,b.options.mode||"effect"),f=parseInt(b.options.percent,10)||(parseInt(b.options.percent,10)==0?0:e=="hide"?0:100),g=b.options.direction||"both",h=b.options.origin;e!="effect"&&(d.origin=h||["middle","center"],d.restore=!0);var i={height:c.height(),width:c.width()};c.from=b.options.from||(e=="show"?{height:0,width:0}:i);var j={y:g!="horizontal"?f/100:1,x:g!="vertical"?f/100:1};c.to={height:i.height*j.y,width:i.width*j.x},b.options.fade&&(e=="show"&&(c.from.opacity=0,c.to.opacity=1),e=="hide"&&(c.from.opacity=1,c.to.opacity=0)),d.from=c.from,d.to=c.to,d.mode=e,c.effect("size",d,b.duration,b.callback),c.dequeue()})},a.effects.size=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right","width","height","overflow","opacity"],e=["position","top","bottom","left","right","overflow","opacity"],f=["width","height","overflow"],g=["fontSize"],h=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],i=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],j=a.effects.setMode(c,b.options.mode||"effect"),k=b.options.restore||!1,l=b.options.scale||"both",m=b.options.origin,n={height:c.height(),width:c.width()};c.from=b.options.from||n,c.to=b.options.to||n;if(m){var p=a.effects.getBaseline(m,n);c.from.top=(n.height-c.from.height)*p.y,c.from.left=(n.width-c.from.width)*p.x,c.to.top=(n.height-c.to.height)*p.y,c.to.left=(n.width-c.to.width)*p.x}var q={from:{y:c.from.height/n.height,x:c.from.width/n.width},to:{y:c.to.height/n.height,x:c.to.width/n.width}};if(l=="box"||l=="both")q.from.y!=q.to.y&&(d=d.concat(h),c.from=a.effects.setTransition(c,h,q.from.y,c.from),c.to=a.effects.setTransition(c,h,q.to.y,c.to)),q.from.x!=q.to.x&&(d=d.concat(i),c.from=a.effects.setTransition(c,i,q.from.x,c.from),c.to=a.effects.setTransition(c,i,q.to.x,c.to));(l=="content"||l=="both")&&q.from.y!=q.to.y&&(d=d.concat(g),c.from=a.effects.setTransition(c,g,q.from.y,c.from),c.to=a.effects.setTransition(c,g,q.to.y,c.to)),a.effects.save(c,k?d:e),c.show(),a.effects.createWrapper(c),c.css("overflow","hidden").css(c.from);if(l=="content"||l=="both")h=h.concat(["marginTop","marginBottom"]).concat(g),i=i.concat(["marginLeft","marginRight"]),f=d.concat(h).concat(i),c.find("*[width]").each(function(){var c=a(this);k&&a.effects.save(c,f);var d={height:c.height(),width:c.width()};c.from={height:d.height*q.from.y,width:d.width*q.from.x},c.to={height:d.height*q.to.y,width:d.width*q.to.x},q.from.y!=q.to.y&&(c.from=a.effects.setTransition(c,h,q.from.y,c.from),c.to=a.effects.setTransition(c,h,q.to.y,c.to)),q.from.x!=q.to.x&&(c.from=a.effects.setTransition(c,i,q.from.x,c.from),c.to=a.effects.setTransition(c,i,q.to.x,c.to)),c.css(c.from),c.animate(c.to,b.duration,b.options.easing,function(){k&&a.effects.restore(c,f)})});c.animate(c.to,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){c.to.opacity===0&&c.css("opacity",c.from.opacity),j=="hide"&&c.hide(),a.effects.restore(c,k?d:e),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.shake.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.shake=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"effect"),f=b.options.direction||"left",g=b.options.distance||20,h=b.options.times||3,i=b.duration||b.options.duration||140;a.effects.save(c,d),c.show(),a.effects.createWrapper(c);var j=f=="up"||f=="down"?"top":"left",k=f=="up"||f=="left"?"pos":"neg",l={},m={},n={};l[j]=(k=="pos"?"-=":"+=")+g,m[j]=(k=="pos"?"+=":"-=")+g*2,n[j]=(k=="pos"?"-=":"+=")+g*2,c.animate(l,i,b.options.easing);for(var p=1;p<h;p++)c.animate(m,i,b.options.easing).animate(n,i,b.options.easing);c.animate(m,i,b.options.easing).animate(l,i/2,b.options.easing,function(){a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments)}),c.queue("fx",function(){c.dequeue()}),c.dequeue()})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.slide.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.slide=function(b){return this.queue(function(){var c=a(this),d=["position","top","bottom","left","right"],e=a.effects.setMode(c,b.options.mode||"show"),f=b.options.direction||"left";a.effects.save(c,d),c.show(),a.effects.createWrapper(c).css({overflow:"hidden"});var g=f=="up"||f=="down"?"top":"left",h=f=="up"||f=="left"?"pos":"neg",i=b.options.distance||(g=="top"?c.outerHeight(!0):c.outerWidth(!0));e=="show"&&c.css(g,h=="pos"?isNaN(i)?"-"+i:-i:i);var j={};j[g]=(e=="show"?h=="pos"?"+=":"-=":h=="pos"?"-=":"+=")+i,c.animate(j,{queue:!1,duration:b.duration,easing:b.options.easing,complete:function(){e=="hide"&&c.hide(),a.effects.restore(c,d),a.effects.removeWrapper(c),b.callback&&b.callback.apply(this,arguments),c.dequeue()}})})}})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15
+* https://github.com/jquery/jquery-ui
+* Includes: jquery.effects.transfer.js
+* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */
+(function(a,b){a.effects.transfer=function(b){return this.queue(function(){var c=a(this),d=a(b.options.to),e=d.offset(),f={top:e.top,left:e.left,height:d.innerHeight(),width:d.innerWidth()},g=c.offset(),h=a('<div class="ui-effects-transfer"></div>').appendTo(document.body).addClass(b.options.className).css({top:g.top,left:g.left,height:c.innerHeight(),width:c.innerWidth(),position:"absolute"}).animate(f,b.duration,b.options.easing,function(){h.remove(),b.callback&&b.callback.apply(c[0],arguments),c.dequeue()})})}})(jQuery);;
\ No newline at end of file
diff --git a/server-webapp/src/main/resources/static/t2cogs.png b/server-webapp/src/main/resources/static/t2cogs.png
new file mode 100644
index 0000000..a93b3bd
--- /dev/null
+++ b/server-webapp/src/main/resources/static/t2cogs.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-bg_diagonals-small_25_c5ddfc_40x40.png b/server-webapp/src/main/resources/static/ui-bg_diagonals-small_25_c5ddfc_40x40.png
new file mode 100644
index 0000000..c664c51
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-bg_diagonals-small_25_c5ddfc_40x40.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_20_e69700_40x40.png b/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_20_e69700_40x40.png
new file mode 100644
index 0000000..6aed97a
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_20_e69700_40x40.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_22_1484e6_40x40.png b/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_22_1484e6_40x40.png
new file mode 100644
index 0000000..43ba34e
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_22_1484e6_40x40.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_26_2293f7_40x40.png b/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_26_2293f7_40x40.png
new file mode 100644
index 0000000..68306d1
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-bg_diagonals-thick_26_2293f7_40x40.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-bg_flat_0_e69700_40x100.png b/server-webapp/src/main/resources/static/ui-bg_flat_0_e69700_40x100.png
new file mode 100644
index 0000000..f567c28
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-bg_flat_0_e69700_40x100.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-bg_flat_0_e6b900_40x100.png b/server-webapp/src/main/resources/static/ui-bg_flat_0_e6b900_40x100.png
new file mode 100644
index 0000000..5c5494f
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-bg_flat_0_e6b900_40x100.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-bg_highlight-soft_100_f9f9f9_1x100.png b/server-webapp/src/main/resources/static/ui-bg_highlight-soft_100_f9f9f9_1x100.png
new file mode 100644
index 0000000..9a46d19
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-bg_highlight-soft_100_f9f9f9_1x100.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-bg_inset-hard_100_eeeeee_1x100.png b/server-webapp/src/main/resources/static/ui-bg_inset-hard_100_eeeeee_1x100.png
new file mode 100644
index 0000000..f811f30
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-bg_inset-hard_100_eeeeee_1x100.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-icons_0a82eb_256x240.png b/server-webapp/src/main/resources/static/ui-icons_0a82eb_256x240.png
new file mode 100644
index 0000000..755fe99
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-icons_0a82eb_256x240.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-icons_0b54d5_256x240.png b/server-webapp/src/main/resources/static/ui-icons_0b54d5_256x240.png
new file mode 100644
index 0000000..98705f9
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-icons_0b54d5_256x240.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-icons_5fa5e3_256x240.png b/server-webapp/src/main/resources/static/ui-icons_5fa5e3_256x240.png
new file mode 100644
index 0000000..3f67eca
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-icons_5fa5e3_256x240.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-icons_fcdd4a_256x240.png b/server-webapp/src/main/resources/static/ui-icons_fcdd4a_256x240.png
new file mode 100644
index 0000000..de76ce2
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-icons_fcdd4a_256x240.png
Binary files differ
diff --git a/server-webapp/src/main/resources/static/ui-icons_ffffff_256x240.png b/server-webapp/src/main/resources/static/ui-icons_ffffff_256x240.png
new file mode 100644
index 0000000..42f8f99
--- /dev/null
+++ b/server-webapp/src/main/resources/static/ui-icons_ffffff_256x240.png
Binary files differ
diff --git a/server-webapp/src/main/resources/version.properties b/server-webapp/src/main/resources/version.properties
new file mode 100644
index 0000000..7ddb7d3
--- /dev/null
+++ b/server-webapp/src/main/resources/version.properties
@@ -0,0 +1,5 @@
+# What is the version of the server? Bind this in automatically.
+tavernaserver.version=${project.version}
+tavernaserver.revision.describe=${git.commit.id.describe}
+tavernaserver.branch=${git.branch}
+tavernaserver.timestamp=${git.build.time}
diff --git a/server-webapp/src/main/resources/welcome.html b/server-webapp/src/main/resources/welcome.html
new file mode 100644
index 0000000..f80da4a
--- /dev/null
+++ b/server-webapp/src/main/resources/welcome.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=US-ASCII">
+<title>Taverna Server %{VERSION}</title>
+</head>
+<body>
+<h1>Taverna Server %{VERSION}</h1>
+<div style="text-align;center">
+	<p>
+		<i>Note that this is a pre-release version. Significant known
+			issues remain open and it is not guaranteed that the service API
+			will be stable.</i>
+	</p>
+</div>
+<p>For a full list of operations, see the <a
+    href="%{BASEURL}/services">service listing</a> generated by Apache
+CXF, which indicates where to access the WSDL and WADL descriptions of
+the T2Server interface.</p>
+<p>What follows below is a simple guide to using the server. It does
+<i>not</i> cover all the features; a much more extensive guide is available on
+<a href="http://www.mygrid.org.uk/dev/wiki/display/taverna/Taverna+Server+2.4"
+>the myGrid website</a>.</p>
+<h2>5 Minute Guide to Using the REST API</h2>
+<p>Taverna Server %{VERSION} supports both REST and SOAP APIs; you may use either API
+to access the service and any of the workflow runs hosted by the service. This
+simple guide just discusses the REST API.</p>
+<ol>
+  <li>
+  <p>The client starts by creating a workflow run. This is done by POSTing a
+  T2flow document to the service at the address <tt>%{BASEURL}/rest/runs</tt>
+  with the content type <tt>application/vnd.taverna.t2flow+xml</tt>.</p>
+  <p>The result of the POST is an <tt>HTTP 201 Created</tt> that gives the
+  location of the created run (in a <tt>Location</tt> header),
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b></tt> (where <tt><b>UUID</b></tt> is a
+  unique string that identifies the particular run; this is also the name of
+  the run that you would use in the SOAP interface). Note that the run is not
+  yet actually doing anything.</p>
+  </li>
+  <li>
+  <p>Next, you need to set up the inputs to the workflow ports. To set the
+  input port, <tt><b>FOO</b></tt>, to have the value <tt><b>BAR</b></tt>, you
+  would PUT a message like this to the URI
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b>/input/input/<b>FOO</b></tt>
+  </p>
+  <blockquote><pre>&lt;t2sr:runInput xmlns:t2sr=&quot;http://ns.taverna.org.uk/2010/xml/server/rest/&quot;&gt;
+    &lt;t2sr:value&gt;<b>BAR</b>&lt;/t2sr:value&gt;
+&lt;/t2sr:runInput&gt;</pre></blockquote>
+  </li>
+  <li>
+  <p>Now you can start the file running. This is done by using a PUT to set
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b>/status</tt> to the plain text value
+  <tt>Operating</tt>.</p>
+  </li>
+  <li>
+  <p>Now you need to poll, waiting for the workflow to finish. To discover the
+  state of a run, you can (at any time) do a GET on
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b>/status</tt>; when the workflow has
+  finished executing, this will return <tt>Finished</tt> instead of
+  <tt>Operating</tt> (or <tt>Initialized</tt>, the starting state).</p>
+  </li>
+  <li>
+  <p>Every workflow run has an expiry time, after which it will be destroyed
+  and all resources (i.e., local files) associated with it cleaned up. By
+  default in this release, this is 20 minutes after initial creation. To see
+  when a particular run is scheduled to be disposed of, do a GET on
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b>/expiry</tt>; you may set the time when
+  the run is disposed of by PUTting a new time to that same URI. Note that
+  this includes not just the time when the workflow is executing, but also
+  when the input files are being created beforehand and when the results are
+  being downloaded afterwards; you are advised to make your clients regularly
+  advance the expiry time while the run is in use.</p>
+  </li>
+  <li>
+  <p>The outputs from the workflow are files created in the <tt>out</tt>
+  subdirectory of the run's working directory. The contents of the
+  subdirectory can be read by doing a GET on
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b>/wd/out</tt> which will return an XML
+  document describing the contents of the directory, with links to each of the
+  files within it. Doing a GET on those links will retrieve the actual created
+  files (as uninterpreted binary data).</p>
+  <p>Thus, if a single output <tt><b>FOO.OUT</b></tt> was produced from the
+  workflow, it would be written to the file that can be retrieved from
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b>/wd/out/<b>FOO.OUT</b></tt> and the
+  result of the GET on <tt>%{BASEURL}/rest/runs/<b>UUID</b>/wd/out</tt> would
+  look something like this:</p>
+  <blockquote><pre>&lt;t2sr:directoryContents xmlns:xlink="http://www.w3.org/1999/xlink"
+        xmlns:t2sr="http://ns.taverna.org.uk/2010/xml/server/rest"
+        xmlns:t2s="http://ns.taverna.org.uk/2010/xml/server/"&gt;
+    &lt;t2s:file xlink:href="%{BASEURL}/rest/runs/<b>UUID</b>/wd/out/<b>FOO.OUT</b>"
+            t2sr:name="<b>FOO.OUT</b>"&gt;out/<b>FOO.OUT</b>&lt;/t2s:file&gt;
+&lt;/t2sr:directoryContents&gt;</pre></blockquote>
+  </li>
+  <li>
+  <p>The standard output and standard error from the T2 Command Line Executor
+  subprocess can be read via properties of the special I/O listener. To do
+  that, do a GET on
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b>/listeners/io/properties/<b>stdout</b></tt>
+  (or <tt>.../<b>stderr</b></tt>). Once the subprocess has finished executing,
+  the I/O listener will provide a third property containing the exit code of
+  the subprocess, called <tt>exitcode</tt>.</p> <p>Note that the supported set
+  of listeners and properties will be subject to change in future versions of
+  the server, and should not be relied upon.</p>
+  </li>
+  <li>
+  <p>Once you have finished, destroy the run by doing a DELETE on
+  <tt>%{BASEURL}/rest/runs/<b>UUID</b></tt>. Once you have done that, none of
+  the resources associated with the run (including both input and output
+  files) will exist any more. If the run is still executing, this will also
+  cause it to be stopped.</p>
+  </li>
+</ol>
+<p>All operations described above have equivalents in the
+<a href="%{BASEURL}/soap?wsdl">SOAP service interface</a>.</p>
+
+<div>
+<hr>
+<p><small>Copyright &copy; 2010&ndash;2014. The University of Manchester.</small></p>
+<p><small>Software Release ID: ${project.version} (commit: ${git.branch})</small></p>
+</div>
+</body>
+</html>
diff --git a/server-webapp/src/main/webapp/META-INF/MANIFEST.MF b/server-webapp/src/main/webapp/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..5e94951
--- /dev/null
+++ b/server-webapp/src/main/webapp/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0

+Class-Path: 

+

diff --git a/server-webapp/src/main/webapp/META-INF/cxf/org.apache.cxf.Logger b/server-webapp/src/main/webapp/META-INF/cxf/org.apache.cxf.Logger
new file mode 100644
index 0000000..4fd9372
--- /dev/null
+++ b/server-webapp/src/main/webapp/META-INF/cxf/org.apache.cxf.Logger
@@ -0,0 +1 @@
+org.apache.cxf.common.logging.Log4jLogger
diff --git a/server-webapp/src/main/webapp/META-INF/persistence.xml b/server-webapp/src/main/webapp/META-INF/persistence.xml
new file mode 100644
index 0000000..afd640a
--- /dev/null
+++ b/server-webapp/src/main/webapp/META-INF/persistence.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<persistence version="1.0"
+	xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd ">
+	<persistence-unit name="TavernaServer">
+		<class>org.taverna.server.master.WebappState</class>
+		<class>org.taverna.server.master.identity.User</class>
+		<class>org.taverna.server.master.localworker.PersistedState</class>
+		<class>org.taverna.server.master.notification.atom.Event</class>
+		<class>org.taverna.server.master.usage.UsageRecord</class>
+		<class>org.taverna.server.master.worker.RunConnection</class>
+		<exclude-unlisted-classes>true</exclude-unlisted-classes>
+	</persistence-unit>
+</persistence>
diff --git a/server-webapp/src/main/webapp/WEB-INF/beans.xml b/server-webapp/src/main/webapp/WEB-INF/beans.xml
new file mode 100644
index 0000000..73990a7
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/beans.xml
@@ -0,0 +1,518 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2010-2011 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
+	xmlns:task="http://www.springframework.org/schema/task" xmlns:util="http://www.springframework.org/schema/util"
+	default-lazy-init="false"
+	xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd
+		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
+		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
+		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd">
+
+	<bean id="webapp" class="org.taverna.server.master.TavernaServer">
+		<property name="policy" ref="worker.policy" />
+		<property name="runStore" ref="worker.rundb" />
+		<property name="fileUtils" ref="fileUtils" />
+		<property name="contentsDescriptorBuilder" ref="contentsDescriptorBuilder" />
+		<property name="notificationEngine" ref="notificationFabric" />
+		<property name="support" ref="webapp.support" />
+		<property name="eventSource" ref="dispatch.atom" />
+		<property name="interactionFeed" value="${taverna.interaction.feed_path}" />
+		<lookup-method name="makeRunInterface" bean="run.coreView.prototype" />
+		<lookup-method name="getPolicyDescription" bean="webapp.policyView" />
+	</bean>
+
+	<bean id="webapp.support" class="org.taverna.server.master.TavernaServerSupport">
+		<property name="policy" ref="worker.policy" />
+		<property name="listenerFactory" ref="localworker.factory" />
+		<property name="runFactory" ref="localworker.factory" />
+		<property name="fileUtils" ref="fileUtils" />
+		<property name="runStore" ref="worker.rundb" />
+		<property name="stateModel" ref="webapp.state" />
+		<property name="idMapper" ref="IdentityMapper" />
+		<property name="invocationCounter" ref="webapp.invocationCounter" />
+		<property name="webapp" ref="webapp" />
+		<property name="contentTypeMap">
+			<map key-type="java.lang.String" value-type="java.lang.String">
+				<description>Maps suffixes (post '.') to content types.</description>
+				<entry key="baclava" value="application/vnd.taverna.baclava+xml" />
+				<entry key="robundle.zip" value="application/vnd.wf4ever.robundle+zip" />
+			</map>
+		</property>
+		<property name="capabilitySource" ref="capabilities" />
+	</bean>
+
+	<bean id="capabilities" class="org.taverna.server.master.utils.CapabilityLister" />
+
+	<bean id="webapp.policyView" class="org.taverna.server.master.PolicyREST">
+		<property name="support" ref="webapp.support" />
+		<property name="policy" ref="worker.policy" />
+		<property name="listenerFactory" ref="localworker.factory" />
+		<property name="notificationEngine" ref="notificationFabric" />
+	</bean>
+
+	<bean id="contentsDescriptorBuilder" class="org.taverna.server.master.ContentsDescriptorBuilder">
+		<property name="uriBuilderFactory" ref="webapp" />
+		<property name="fileUtils" ref="fileUtils" />
+	</bean>
+
+	<bean id="webapp.state" class="org.taverna.server.master.ManagementState">
+		<description>The initial state of the webapp.</description>
+		<property name="logIncomingWorkflows" value="${default.logworkflows}" />
+		<property name="allowNewWorkflowRuns" value="${default.permitsubmit}" />
+		<property name="logOutgoingExceptions" value="${default.logexceptions}" />
+		<property name="persistenceManagerBuilder" ref="pmb" />
+		<property name="self" ref="webapp.state" />
+	</bean>
+
+	<bean id="webapp.invocationCounter" class="org.taverna.server.master.utils.InvocationCounter" />
+	<bean id="webapp.perfLogger" class="org.taverna.server.master.utils.CallTimeLogger">
+		<property name="threshold" value="${calltime.logthreshold:4000000}" />
+	</bean>
+
+	<bean id="run.coreView.prototype" scope="prototype"
+		class="org.taverna.server.master.RunREST">
+		<property name="support" ref="webapp.support" />
+		<property name="cdBuilder" ref="contentsDescriptorBuilder" />
+		<lookup-method name="makeSecurityInterface" bean="run.securityView.prototype" />
+		<lookup-method name="makeInputInterface" bean="run.inputView.prototype" />
+		<lookup-method name="makeListenersInterface" bean="run.listenersView.prototype" />
+		<lookup-method name="makeDirectoryInterface" bean="run.directoryView.prototype" />
+		<lookup-method name="makeInteractionFeed" bean="run.interactionFeed.prototype" />
+	</bean>
+
+	<bean id="run.directoryView.prototype" scope="prototype"
+		class="org.taverna.server.master.DirectoryREST">
+		<property name="support" ref="webapp.support" />
+		<property name="fileUtils" ref="fileUtils" />
+	</bean>
+
+	<bean id="run.listenersView.prototype" scope="prototype"
+		class="org.taverna.server.master.ListenersREST">
+		<property name="support" ref="webapp.support" />
+		<lookup-method name="makeListenerInterface" bean="run.singleListenerView.prototype" />
+	</bean>
+
+	<bean id="run.singleListenerView.prototype" scope="prototype"
+		class="org.taverna.server.master.SingleListenerREST">
+		<lookup-method name="makePropertyInterface" bean="run.propertyView.prototype" />
+	</bean>
+
+	<bean id="run.propertyView.prototype" scope="prototype"
+		class="org.taverna.server.master.ListenerPropertyREST">
+		<property name="support" ref="webapp.support" />
+	</bean>
+
+	<bean id="run.inputView.prototype" scope="prototype"
+		class="org.taverna.server.master.InputREST">
+		<property name="support" ref="webapp.support" />
+		<property name="cdBuilder" ref="contentsDescriptorBuilder" />
+		<property name="fileUtils" ref="fileUtils" />
+	</bean>
+
+	<bean id="run.securityView.prototype" scope="prototype"
+		class="org.taverna.server.master.RunSecurityREST">
+		<property name="support" ref="webapp.support" />
+	</bean>
+
+	<bean id="run.interactionFeed.prototype" scope="prototype"
+		class="org.taverna.server.master.InteractionFeed">
+		<property name="interactionFeedSupport" ref="interactionFeed" />
+	</bean>
+
+	<bean id="feed" class="org.taverna.server.master.notification.atom.AtomFeed">
+		<property name="eventSource" ref="dispatch.atom" />
+		<property name="support" ref="webapp.support" />
+		<property name="feedLanguage" value="${atom.language}" />
+		<property name="abdera" ref="abdera" />
+	</bean>
+
+	<bean id="admin" class="org.taverna.server.master.admin.AdminBean">
+		<property name="adminHtmlFile" value="/admin.html" />
+		<property name="counter" ref="webapp.invocationCounter" />
+		<property name="factory" ref="localworker.factory" />
+		<property name="localWorkerModel" ref="localworker.state" />
+		<property name="runDB" ref="worker.rundb" />
+		<property name="state" ref="webapp.state" />
+		<property name="usageRecords" ref="usageRecordSink" />
+		<property name="userStore" ref="userStore" />
+	</bean>
+
+	<bean id="IdentityMapper" class="org.taverna.server.master.identity.CompositeIDMapper">
+		<property name="identityMappers">
+			<list>
+				<bean id="AuthorityBased"
+					class="org.taverna.server.master.identity.AuthorityDerivedIDMapper">
+					<description>Derives the local user identity to use for execution
+						from the LOCALUSER_* Spring Security authority. Thus, if the user
+						has &quot;LOCALUSER_foo&quot;, they will be executing as the local
+						user id &quot;foo&quot;.</description>
+				</bean>
+				<bean id="SelfAccess"
+					class="org.taverna.server.master.identity.WorkflowInternalAuthProvider.WorkflowSelfIDMapper">
+					<description>Handles the case where a workflow is accessing itself for
+						the purpose of publishing interactions.</description>
+					<property name="runStore" ref="worker.rundb" />
+				</bean>
+				<bean id="Extracting" class="org.taverna.server.master.identity.NameIDMapper">
+					<description>An alternate mechanism for mapping users. This tries
+						to use an RE to extract the user name from the principal name.
+					</description>
+					<property name="regexp" value="${localusernameregexp}">
+						<description>An optional regexp to extract the local user name
+							from the principal's string description. The first capturing
+							group will be the result of the mapping operation.
+						</description>
+					</property>
+				</bean>
+				<bean id="Constant" class="org.taverna.server.master.identity.ConstantIDMapper">
+					<description>How to map web principals to local users. This one
+						maps everyone to the same user, "taverna".
+					</description>
+					<property name="constantId" value="${default.localusername}" />
+				</bean>
+			</list>
+		</property>
+	</bean>
+
+	<bean id="passwordEncoder"
+		class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
+	<bean class="org.taverna.server.master.identity.UserStore" id="userStore">
+		<property name="persistenceManagerBuilder" ref="pmb" />
+		<property name="baselineUserProperties">
+			<util:properties location="/WEB-INF/security/users.properties" />
+		</property>
+		<property name="defaultLocalUser" value="${default.localusername}" />
+		<property name="encoder" ref="passwordEncoder" />
+	</bean>
+
+	<!-- <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"> 
+		<property name="dataSource" ref="dataSource" /> <property name="hibernateProperties"> 
+		<props> <prop key="hibernate.dialect">org.hibernate.dialect.DerbyDialect</prop> 
+		<prop key="hibernate.hbm2ddl.auto">create</prop> </props> </property> <property 
+		name="annotatedClasses"> <list> </list> </property> </bean> -->
+	<bean id="pmb" class="org.taverna.server.master.utils.JDOSupport.PersistenceManagerBuilder">
+		<property name="persistenceManagerFactory">
+			<bean id="pmf" class="org.datanucleus.api.jdo.JDOPersistenceManagerFactory"
+				destroy-method="close">
+				<property name="connectionFactory" ref="dataSource" />
+				<property name="nontransactionalRead" value="true" />
+				<property name="persistenceProperties">
+					<props>
+						<prop key="datanucleus.storeManagerType">rdbms</prop>
+						<prop key="datanucleus.autoCreateTables">true</prop>
+						<prop key="datanucleus.autoCreateTables">true</prop>
+						<prop key="datanucleus.validateTables">true</prop>
+						<prop key="datanucleus.autoCreateColumns">true</prop>
+						<prop key="datanucleus.autoCreateConstraints">true</prop>
+						<prop key="datanucleus.validateConstraints">true</prop>
+						<prop key="datanucleus.autoCreateSchema">true</prop>
+						<prop key="datanucleus.PersistenceUnitName">TavernaServer</prop>
+						<prop key="datanucleus.rdbms.datastoreAdapterClassName"
+							>org.taverna.server.master.utils.LoggingDerbyAdapter</prop>
+					</props>
+				</property>
+			</bean>
+		</property>
+	</bean>
+	<bean id="transactionAspect"
+		class="org.taverna.server.master.utils.JDOSupport.TransactionAspect" />
+
+	<bean id="systemPrereqs" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
+		<description>http://stackoverflow.com/questions/3339736/set-system-property-with-spring-configuration-file</description>
+		<property name="targetObject" value="#{@systemProperties}" />
+		<property name="targetMethod" value="putAll" />
+		<property name="arguments">
+			<util:properties>
+				<prop key="derby.stream.error.field">org.taverna.server.master.utils.DerbyUtils.TO_LOG</prop>
+				<prop key="derby.stream.error.logSeverityLevel">30000</prop>
+			</util:properties>
+		</property>
+	</bean>
+	<!-- TODO: control the location of the database properly, or move to JNDI 
+		TODO: review whether what we are doing now is correct! -->
+	<bean id="dataSource" class="org.taverna.server.master.utils.WebappAwareDataSource">
+		<property name="driverClassName" value="org.apache.derby.jdbc.EmbeddedDriver" />
+		<property name="url"
+			value="jdbc:derby:directory:%{WEBAPPROOT}tavernaserver;create=true" />
+		<!-- Needed for Derby... -->
+		<property name="shutdownUrl"
+			value="jdbc:derby:directory:%{WEBAPPROOT}tavernaserver;shutdown=true" />
+		<property name="username" value="taverna" />
+		<property name="password" value="" />
+		<property name="contextualizer" ref="contextualizer" />
+	</bean>
+
+	<bean id="contextualizer" class="org.taverna.server.master.utils.Contextualizer" />
+
+	<bean id="usageRecordSink" class="org.taverna.server.master.usage.UsageRecordRecorder">
+		<property name="state" ref="webapp.state" />
+		<property name="contextualizer" ref="contextualizer" />
+		<property name="persistenceManagerBuilder" ref="pmb" />
+		<property name="self" ref="usageRecordSink" />
+		<property name="disableDB" value="${usage.disableDB}" />
+		<property name="logFile" value="${usage.logFile}" />
+	</bean>
+
+	<context:property-placeholder
+		ignore-unresolvable="true" order="2" properties-ref="default-factory-properties" />
+	<util:properties id="default-factory-properties">
+		<prop key="backEndFactory">org.taverna.server.master.localworker.IdAwareForkRunFactory</prop>
+	</util:properties>
+
+	<!-- This bean configuration replaced with org.taverna.server.master.localworker.LocalWorkerFactory -->
+	<!-- <bean id="localworker.factory" scope="singleton" lazy-init="false" 
+		class="${backEndFactory}"> <description> The simple policy manager and factory 
+		for the baseline localworker case. </description> <property name="state" 
+		ref="localworker.state" /> <property name="runDB" ref="worker.rundb" 
+		/> -->
+	<!-- Handled by autowiring to make type-resilient. -->
+	<!-- <property name="idMapper" ref="IdentityMapper" /> -->
+	<!-- <property name="securityContextFactory" ref="worker.securityContext" 
+		/> <property name="usageRecordSink" ref="usageRecordSink" /> <property name="URProcessorPool" 
+		ref="URThreads" /> </bean> -->
+
+	<!-- <task:executor id="URThreads" pool-size="${pool.size}" /> -->
+
+	<bean id="worker.securityContext"
+		class="org.taverna.server.master.worker.SecurityContextFactory">
+		<property name="runDatabase" ref="worker.rundb" />
+		<property name="filenameConverter" ref="fileUtils" />
+		<property name="x500Utils" ref="x500Utils" />
+		<property name="httpRealm" value="${http.realmName}" />
+		<property name="uriSource" ref="webapp" />
+		<property name="certificateFetcher" ref="certificateChainFetcher" />
+		<property name="passwordIssuer" ref="passwordIssuer" />
+	</bean>
+
+	<bean id="passwordIssuer" class="org.taverna.server.master.worker.PasswordIssuer">
+		<!-- <property name="length" value="8" /> -->
+	</bean>
+
+	<bean id="certificateChainFetcher" class="org.taverna.server.master.utils.CertificateChainFetcher">
+		<property name="secure" value="${fetchCertificateChain}" />
+	</bean>
+
+	<bean id="localworker.state" class="org.taverna.server.master.localworker.LocalWorkerState"
+		scope="singleton" lazy-init="false">
+		<description>
+			The state of the simple factory for the identity-aware
+			local worker.
+		</description>
+		<property name="defaultLifetime" value="${default.lifetime}">
+			<description>How long the run lasts for by default, in seconds.
+			</description>
+		</property>
+		<property name="maxRuns" value="${default.runlimit}">
+			<description>The maximum simultaneous number of runs.</description>
+		</property>
+		<property name="operatingLimit" value="${default.operatinglimit}">
+			<description>
+				The maximum number of runs that are in the Operating state,
+				i.e., actually running with a process consuming resources.
+			</description>
+		</property>
+		<property name="extraArgs">
+			<description>Any extra arguments (memory control, etc.) to pass to
+				the spawned subprocesses.
+			</description>
+			<list>
+			</list>
+		</property>
+		<property name="waitSeconds" value="40">
+			<description>An upper bound (in seconds) on the time to wait for a
+				subprocess to start before failing it.
+			</description>
+		</property>
+		<property name="sleepMS" value="1000">
+			<description>The time to wait (in milliseconds) between polling for
+				the subprocess to complete its registration.
+			</description>
+		</property>
+		<property name="persistenceManagerBuilder" ref="pmb" />
+		<!-- <property name="javaBinary"> <description>The name of the java executable 
+			used to run the server worker. Defaults to the executable used to run the 
+			hosting environment.</description> </property> -->
+		<!-- <property name="serverWorkerJar"> <description>The full path to the 
+			executable JAR file containing the implementation of the server worker.</description> 
+			</property> -->
+		<!-- <property name="executeWorkflowScript"> <description>The full path 
+			to the executeworkflow.sh in either the Taverna 2 Workbench distribution 
+			or the Taverna 2 Command Line distribution.</description> </property> -->
+		<property name="self" ref="localworker.state" />
+		<property name="defaultExecuteWorkflowScript" value="${executeWorkflowScript:NONE}" />
+	</bean>
+
+	<bean id="worker.policy" class="org.taverna.server.master.worker.PolicyImpl">
+		<description>
+			The implementation of the access control policy supported
+			by the localworker run engine.
+		</description>
+		<property name="limits" ref="localworker.state" />
+		<property name="runDB" ref="worker.rundb" />
+	</bean>
+	<bean id="worker.rundb" class="org.taverna.server.master.worker.RunDatabase">
+		<property name="notifier" ref="worker.notifier" />
+		<property name="typeNotifiers">
+			<list>
+				<ref bean="cn.email" />
+				<ref bean="cn.sms" />
+				<ref bean="cn.twitter" />
+				<!-- <ref bean="cn.xmpp"/> -->
+			</list>
+		</property>
+		<property name="notificationEngine" ref="notificationFabric" />
+		<property name="dao" ref="worker.runDAO" />
+	</bean>
+	<bean id="cn.email"
+		class="org.taverna.server.master.worker.VelocityCompletionNotifier">
+		<property name="name" value="email" />
+		<property name="subject" value="Workflow run finished executing" />
+		<property name="velocityEngine" ref="velocity" />
+		<property name="uriBuilderFactory" ref="webapp" />
+	</bean>
+	<bean id="cn.sms"
+		class="org.taverna.server.master.worker.VelocityCompletionNotifier">
+		<property name="name" value="sms" />
+		<property name="subject" value="Run finished" />
+		<property name="velocityEngine" ref="velocity" />
+		<property name="uriBuilderFactory" ref="webapp" />
+	</bean>
+	<bean id="cn.twitter"
+		class="org.taverna.server.master.worker.VelocityCompletionNotifier">
+		<property name="name" value="twitter" />
+		<property name="subject" value="Run finished" />
+		<property name="velocityEngine" ref="velocity" />
+		<property name="uriBuilderFactory" ref="webapp" />
+	</bean>
+	
+	<bean id="worker.runDAO" class="org.taverna.server.master.worker.RunDatabaseDAO">
+		<description>
+			The implementation of the catalog of workflow runs
+			supported by the localworker run engine.
+		</description>
+		<property name="persistenceManagerBuilder" ref="pmb" />
+		<property name="facade" ref="worker.rundb" />
+	</bean>
+	<task:scheduled-tasks scheduler="taskScheduler">
+		<task:scheduled ref="worker.rundb" method="cleanNow"
+			fixed-delay="${purge.interval}" />
+		<task:scheduled ref="worker.rundb" method="checkForFinishNow"
+			fixed-delay="${finish.interval}" />
+		<task:scheduled ref="dispatch.atom" method="deleteExpiredEvents"
+			fixed-delay="${atom.cleaninterval}" />
+	</task:scheduled-tasks>
+
+	<bean id="notificationFabric"
+		class="org.taverna.server.master.notification.NotificationEngine">
+		<property name="dispatchers">
+			<list>
+				<ref bean="dispatch.email" />
+				<ref bean="dispatch.twitter" />
+				<ref bean="dispatch.xmpp" />
+				<ref bean="dispatch.sms" />
+			</list>
+		</property>
+		<property name="universalDispatchers">
+			<list>
+				<ref bean="dispatch.atom" />
+			</list>
+		</property>
+	</bean>
+
+	<bean id="dispatch.email" class="org.taverna.server.master.notification.EmailDispatcher">
+		<property name="from" value="${email.from}" />
+		<property name="messageContentType" value="${email.type}" />
+		<property name="cooldownSeconds" value="${message.cooldown}" />
+		<property name="smtpHost" value="${email.host}" />
+		<property name="sender">
+			<bean class="org.springframework.mail.javamail.JavaMailSenderImpl"
+				id="javamail">
+				<property name="host" value="${email.host}" />
+			</bean>
+		</property>
+	</bean>
+	<bean id="dispatch.twitter"
+		class="org.taverna.server.master.notification.TwitterDispatcher">
+		<property name="cooldownSeconds" value="${message.cooldown}" />
+		<property name="accessToken" value="${twitter.oauth.accessToken}" />
+		<property name="accessSecret" value="${twitter.oauth.accessTokenSecret}" />
+	</bean>
+	<bean id="dispatch.xmpp" class="org.taverna.server.master.notification.JabberDispatcher">
+		<property name="resource" value="${xmpp.resource}" />
+		<property name="host" value="${xmpp.service}" />
+		<property name="username" value="${xmpp.user}" />
+		<property name="password" value="${xmpp.password}" />
+	</bean>
+	<bean id="dispatch.sms" class="org.taverna.server.master.notification.SMSDispatcher">
+		<property name="usernameField" value="${sms.userfield}" />
+		<property name="passwordField" value="${sms.passfield}" />
+		<property name="destinationField" value="${sms.destfield}" />
+		<property name="messageField" value="${sms.msgfield}" />
+		<property name="cooldownSeconds" value="${message.cooldown}" />
+	</bean>
+
+	<bean id="dispatch.atom" class="org.taverna.server.master.notification.atom.EventDAO">
+		<property name="expiryAgeDays" value="${atom.lifespan}" />
+		<property name="persistenceManagerBuilder" ref="pmb" />
+		<property name="uriBuilderFactory" ref="feed" />
+		<property name="self" ref="dispatch.atom" />
+	</bean>
+
+	<bean id="worker.notifier"
+		class="org.taverna.server.master.worker.SimpleFormattedCompletionNotifier">
+		<property name="subject" value="${message.termination.subject}" />
+		<property name="messageFormat" value="${message.termination.body}" />
+		<property name="name" value="fallback"/>
+	</bean>
+
+	<bean id="fileUtils" class="org.taverna.server.master.utils.FilenameUtils" />
+	<bean id="x500Utils" class="org.taverna.server.master.utils.X500Utils" />
+	<task:scheduler id="taskScheduler" pool-size="${pool.size}" />
+
+	<bean class="org.taverna.server.master.utils.JCECheck" id="JCECheck" />
+
+	<bean class="org.taverna.server.master.interaction.InteractionFeedSupport"
+		id="interactionFeed" scope="singleton">
+		<property name="abdera" ref="abdera" />
+		<property name="support" ref="webapp.support" />
+		<property name="uriBuilder" ref="webapp" />
+		<property name="utils" ref="fileUtils" />
+	</bean>
+	<bean class="org.taverna.server.master.rest.handler.FeedHandler" id="atomFeedHandler">
+		<property name="abdera" ref="abdera" />
+	</bean>
+	<bean class="org.taverna.server.master.rest.handler.EntryHandler" id="atomEntryHandler">
+		<property name="abdera" ref="abdera" />
+	</bean>
+
+	<bean id="authProvider" class="org.taverna.server.master.identity.StrippedDownAuthProvider">
+		<property name="passwordEncoder" ref="passwordEncoder" />
+		<property name="userDetailsService">
+			<bean class="org.taverna.server.master.identity.UserStore.CachedUserStore">
+				<property name="realStore" ref="userStore" />
+			</bean>
+		</property>
+	</bean>
+	<bean id="workflowInternalAuthProvder"
+		class="org.taverna.server.master.identity.WorkflowInternalAuthProvider">
+		<property name="dao" ref="worker.runDAO" />
+		<property name="cacheBound" value="${default.runlimit}" />
+	</bean>
+	<bean id="velocity" class="org.apache.velocity.app.VelocityEngine"
+		init-method="init" lazy-init="false">
+		<constructor-arg>
+			<props>
+				<prop key="input.encoding">UTF-8</prop>
+				<prop key="output.encoding">UTF-8</prop>
+				<prop key="runtime.log.logsystem.class">org.apache.velocity.runtime.log.Log4JLogChute</prop>
+				<prop key="runtime.log.logsystem.log4j.logger">org.taverna.server.master.worker.VelocityCompletionNotifier</prop>
+				<prop key="resource.loader">class</prop>
+				<prop key="class.resource.loader.description">Velocity Classpath Resource Loader</prop>
+				<prop key="class.resource.loader.class">org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader</prop>
+			</props>
+		</constructor-arg>
+	</bean>
+	<bean id="abdera" class="org.apache.abdera.Abdera"/>
+</beans>
diff --git a/server-webapp/src/main/webapp/WEB-INF/insecure.xml b/server-webapp/src/main/webapp/WEB-INF/insecure.xml
new file mode 100644
index 0000000..92c0b03
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/insecure.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2010-2012 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:security="http://www.springframework.org/schema/security"
+	xmlns:context="http://www.springframework.org/schema/context"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xsi:schemaLocation="http://cxf.apache.org/configuration/security http://cxf.apache.org/schemas/configuration/security.xsd
+		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
+		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
+		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
+		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
+
+	<description>
+		The Taverna Server bean, plus supporting configuration.
+		THIS IS AN INSECURE CONFIGURATION!
+	</description>
+
+	<import resource="webappBeans.xml" />
+
+	<context:property-placeholder order="0" ignore-unresolvable="true"
+		 properties-ref="security.override.properties" />
+	<util:properties id="security.override.properties">
+		<prop key="backEndFactory">org.taverna.server.master.localworker.ForkRunFactory</prop>
+		<prop key="fetchCertificateChain">false</prop>
+		<prop key="suppressRewriteEngine">true</prop>
+		<prop key="requiredChannel">any</prop>
+	</util:properties>
+
+	<!-- No JMX support; assume too unsafe. -->
+</beans>
diff --git a/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_email.vtmpl b/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_email.vtmpl
new file mode 100644
index 0000000..d706fed
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_email.vtmpl
@@ -0,0 +1,15 @@
+Your workflow, $name, has #if( $prop_exitcode == 0)finished#{else}failed#{end}.
+
+It started execution at ${startTime}.
+It finished execution at ${finishTime}.
+
+For further information, go to $uriBuilder.build() before the run is
+automatically deleted (currently scheduled to happen at ${expiryTime}).
+
+#if( $prop_exitcode == 0)
+Exit code: $prop_exitcode
+Standard error:
+$prop_stderr
+#end
+
+(This message was automatically generated by Taverna Server ${serverVersion}.)
\ No newline at end of file
diff --git a/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_sms.vtmpl b/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_sms.vtmpl
new file mode 100644
index 0000000..b595931
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_sms.vtmpl
@@ -0,0 +1 @@
+Taverna Server ${serverVersion}: Workflow "$name" from ${startTime} has #if( $prop_exitcode == 0)finished#{else}failed#{end}.
\ No newline at end of file
diff --git a/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_twitter.vtmpl b/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_twitter.vtmpl
new file mode 100644
index 0000000..cd920a7
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/lib/org.taverna.server.master.worker.VelocityCompletionNotifier_twitter.vtmpl
@@ -0,0 +1 @@
+Your workflow, $name, has #if( $prop_exitcode == 0)finished#{else}failed#{end}. $uriBuilder.build() #TavernaServer ${serverVersion}
\ No newline at end of file
diff --git a/server-webapp/src/main/webapp/WEB-INF/partsecure.xml b/server-webapp/src/main/webapp/WEB-INF/partsecure.xml
new file mode 100644
index 0000000..2e27b81
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/partsecure.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2011-2012 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:context="http://www.springframework.org/schema/context"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xmlns:security="http://www.springframework.org/schema/security"
+	xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
+		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
+		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
+		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
+
+	<description>
+		The Taverna Server bean, plus supporting configuration.
+		THIS IS THE CONFIGURATION FOR SEMI-SECURE OPERATION ON WINDOWS.
+	</description>
+
+	<import resource="webappBeans.xml" />
+
+	<!-- This is all the same as the secure config *EXCEPT* for this override. -->
+	<context:property-placeholder order="0" ignore-unresolvable="true"
+		 properties-ref="security.override.properties" />
+	<util:properties id="security.override.properties">
+		<prop key="backEndFactory">org.taverna.server.master.localworker.ForkRunFactory</prop>
+		<prop key="fetchCertificateChain">true</prop>
+		<prop key="suppressRewriteEngine">false</prop>
+		<prop key="requiredChannel">https</prop>
+	</util:properties>
+
+	<bean id="MBeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
+		<property name="locateExistingServerIfPossible" value="true" />
+	</bean>
+	<context:mbean-export server="MBeanServer"
+		default-domain="${my.domain}" registration="ignoreExisting" />
+</beans>
diff --git a/server-webapp/src/main/webapp/WEB-INF/providers.xml b/server-webapp/src/main/webapp/WEB-INF/providers.xml
new file mode 100644
index 0000000..43531ab
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/providers.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2010-2011 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
+		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">
+
+	<import resource="beans.xml" />
+
+	<bean id="ExceptionProvider.BadInputPortName" class="org.taverna.server.master.rest.handler.BadInputPortNameHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.BadPropertyValue" class="org.taverna.server.master.rest.handler.BadPropertyValueHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.BadStateChange" class="org.taverna.server.master.rest.handler.BadStateChangeHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.FilesystemAccess" class="org.taverna.server.master.rest.handler.FilesystemAccessHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.GeneralFailure" class="org.taverna.server.master.rest.handler.GeneralFailureHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.IllegalArgument" class="org.taverna.server.master.rest.handler.IllegalArgumentHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.ImplementationProblem"
+		class="org.taverna.server.master.rest.handler.ImplementationProblemHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.InvalidCredential" class="org.taverna.server.master.rest.handler.InvalidCredentialHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.NoCreate" class="org.taverna.server.master.rest.handler.NoCreateHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.Overloaded" class="org.taverna.server.master.rest.handler.OverloadedHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.NoCredential" class="org.taverna.server.master.rest.handler.NoCredentialHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.NoDestroy" class="org.taverna.server.master.rest.handler.NoDestroyHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.NoDirectoryEntry" class="org.taverna.server.master.rest.handler.NoDirectoryEntryHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.NoListener" class="org.taverna.server.master.rest.handler.NoListenerHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.NoUpdate" class="org.taverna.server.master.rest.handler.NoUpdateHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.NotOwner" class="org.taverna.server.master.rest.handler.NotOwnerHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.UnknownRun" class="org.taverna.server.master.rest.handler.UnknownRunHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.JAXBException" class="org.taverna.server.master.rest.handler.JAXBExceptionHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+	<bean id="ExceptionProvider.AccessDenied" class="org.taverna.server.master.rest.handler.AccessDeniedHandler">
+		<property name="managementModel" ref="webapp.state" />
+	</bean>
+    <bean id="ExceptionProvider.NegotiationFailed" class="org.taverna.server.master.rest.handler.NegotiationFailedHandler">
+    </bean>
+	<bean id="MessagingProvider.File" class="org.taverna.server.master.rest.handler.FileMessageHandler">
+		<property name="maxChunkSize" value="${default.messageSize}" />
+	</bean>
+	<bean id="MessagingProvider.FileConcatenation" class="org.taverna.server.master.rest.handler.FileConcatenationHandler">
+		<property name="maxChunkSize" value="${default.messageSize}" />
+	</bean>
+	<bean id="MessagingProvider.FileSegment" class="org.taverna.server.master.rest.handler.FileSegmentHandler">
+		<property name="maxChunkSize" value="${default.messageSize}" />
+	</bean>
+	<bean id="MessagingProvider.InputStream"
+		class="org.taverna.server.master.rest.handler.InputStreamMessageHandler">
+	</bean>
+	<bean id="MessagingProvider.T2flow" class="org.taverna.server.master.rest.handler.T2FlowDocumentHandler">
+	</bean>
+	<bean id="MessagingProvider.Permission" class="org.taverna.server.master.rest.handler.PermissionHandler">
+	</bean>
+
+	<bean id="Provider.RuntimeExceptionRemapping" class="org.taverna.server.master.utils.RuntimeExceptionWrapper" />
+	<bean id="MessagingProvider.ZipStream" class="org.taverna.server.master.rest.handler.ZipStreamHandler" />
+	<bean id="MessagingProvider.URIList" class="org.taverna.server.master.rest.handler.URIListHandler" />
+	<bean id="Interceptor.FlushThreadLocalCache"
+		class="org.taverna.server.master.utils.FlushThreadLocalCacheInterceptor"
+		lazy-init="false">
+	</bean>
+</beans>
diff --git a/server-webapp/src/main/webapp/WEB-INF/secure.xml b/server-webapp/src/main/webapp/WEB-INF/secure.xml
new file mode 100644
index 0000000..cf08083
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/secure.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2010-2012 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:context="http://www.springframework.org/schema/context"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:security="http://www.springframework.org/schema/security"
+	xsi:schemaLocation="
+	http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
+	http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
+	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
+	http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
+
+	<description>
+		The Taverna Server bean, plus supporting configuration.
+		THIS IS THE CONFIGURATION FOR SECURE OPERATION.
+	</description>
+
+	<import resource="webappBeans.xml" />
+
+	<context:property-placeholder order="0" ignore-unresolvable="true"
+		 properties-ref="security.override.properties" />
+	<util:properties id="security.override.properties">
+		<prop key="fetchCertificateChain">true</prop>
+		<prop key="suppressRewriteEngine">false</prop>
+		<prop key="requiredChannel">https</prop>
+	</util:properties>
+
+	<bean id="MBeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean">
+		<property name="locateExistingServerIfPossible" value="true" />
+	</bean>
+	<context:mbean-export server="MBeanServer"
+		default-domain="${my.domain}" registration="ignoreExisting" />
+</beans>
diff --git a/server-webapp/src/main/webapp/WEB-INF/security/users.properties b/server-webapp/src/main/webapp/WEB-INF/security/users.properties
new file mode 100644
index 0000000..03f8436
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/security/users.properties
@@ -0,0 +1,48 @@
+# This is a Java properties file that defines the default users supported by
+# Taverna Server. The keys are the user names, and the values are comma-
+# -separated lists of values: the first is the password (which must not have
+# any spaces or commas in) and the last is "enabled" or "disabled" to indicate
+# whether this user is actually allowed to log in or not. The values between
+# are the authorities associated with the user: these are either roles (which
+# describe a particular set of permissions for the user) or user mappings.
+#
+# The supported set of authorities are:
+#
+#   ROLE_tavernauser      - may use the standard Taverna Server interfaces
+#
+#   ROLE_tavernasuperuser - may use the admin interfaces and see *all*
+#                           workflow runs irrespective of permissions
+#
+#   LOCALUSER_*           - map the server user to the given local system
+#                           account (i.e., the account name is in place of the
+#                           "*" in the authority); don't grant two of these
+#                           authorities at once, or confusion will reign
+#
+# Note that it is usually better to define accounts via the administrative
+# interface or through the JMX interface because those can then be modified
+# without restarting the server; anything in this file is fixed until the
+# next server restart. This file exists mainly to allow permissions for the
+# admin interface to be boot-strapped.
+
+# ---------------------------------------------------------------------
+
+# A normal user. Must have given role (ROLE_tavernauser) to use Taverna Server.
+# The user has password "taverna" and is enabled. The default mapping to user
+# accounts - using the user name - will be used.
+
+taverna: taverna,ROLE_tavernauser,enabled
+
+# Another normal user (username: "taverna_alt"). Will be mapped to the system
+# account called "taverna", but the user is currently disabled.
+
+taverna_alt: qwerty,ROLE_tavernauser,LOCALUSER_taverna,disabled
+
+# The default admin user, who has password "admin". You should change this!
+# Admin users also have ROLE_tavernasuperuser, which grants access to the
+# server's /admin pages and allows all workflow runs to be seen (if the
+# ROLE_tavernauser is also assigned, as below; one does not imply the other).
+# If you don't have any enabled admin users, you'll need to use JMX to do
+# management operations (JMX is only accessible from the local host and by
+# the overall container user or the system administrator).
+
+admin: admin,ROLE_tavernauser,ROLE_tavernasuperuser,LOCALUSER_taverna,enabled
diff --git a/server-webapp/src/main/webapp/WEB-INF/tavernaserver.properties b/server-webapp/src/main/webapp/WEB-INF/tavernaserver.properties
new file mode 100644
index 0000000..9616a45
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/tavernaserver.properties
@@ -0,0 +1,77 @@
+# Override the hostname, port and webapp; leave at 'NONE' if no override
+# desired. If set, set it to something like:
+#      foo.example.com:8000/tav-serv
+default.webapp:			NONE
+# User name to use if nothing else specified
+default.localusername:	taverna
+# How to pick a user name out of a global identity
+localusernameregexp:	^TAVERNAUSER=(.*)$
+# General defaults
+default.logworkflows:	false
+default.logexceptions:	false
+default.permitsubmit:	true
+default.lifetime:		1440
+default.runlimit:		100
+default.operatinglimit:	10
+default.messageSize:	65536
+#taverna.preferredUserUri:	https://some.host:8443/tavernaserver/rest/
+http.realmName:         tavernaserver
+
+rmi.localhostOnly:		true
+helio.cis.enableTokenPassing:	false
+log.security.details:	false
+
+#executeWorkflowScript:	/usr/taverna/executeworkflow.sh
+#secureForkPasswordFile:	/usr/local/tomcat6.0/conf/sudopass.txt
+
+# Usage Record handling
+usage.logFile:		none
+usage.disableDB:	no
+
+# Delays used in the task executor 
+purge.interval:		30000
+finish.interval:	10000
+
+# Static configuration of messaging
+
+### Email
+email.from:		taverna.server@localhost
+email.type:		text/plain
+#email.host:	localhost
+
+### Jabber
+#xmpp.server:	xmpp://some.host:5222
+xmpp.resource:	TavernaServer
+#xmpp.user:		taverna
+#xmpp.password:	*******
+
+### Atom/RSS; lifespan in days, cleaninterval in milliseconds
+atom.language:	en
+atom.lifespan:	7
+atom.cleaninterval:	3600000
+
+### SMS
+#sms.service:	https://www.intellisoftware.co.uk/smsgateway/sendmsg.aspx
+sms.userfield:	username
+sms.passfield:	password
+sms.destfield:	to
+sms.msgfield:	text
+#sms.user:		taverna
+#sms.pass:		*******
+
+### Twitter
+#twitter.oauth.accessToken:			...
+#twitter.oauth.accessTokenSecret:	...
+
+### General; cooldown in seconds
+message.cooldown:				300
+message.termination.subject:	Taverna workflow run finished
+message.termination.body:		Your job with ID={0} has finished with exit code {1,number,integer}.
+
+# Thread pool sizing
+pool.size:	2
+
+taverna.interaction.host:			none
+taverna.interaction.port:			none
+taverna.interaction.webdav_path:	none
+taverna.interaction.feed_path:		none
diff --git a/server-webapp/src/main/webapp/WEB-INF/web-nosec.xml b/server-webapp/src/main/webapp/WEB-INF/web-nosec.xml
new file mode 100644
index 0000000..d6c6ee7
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/web-nosec.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE web-app
+    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+    "http://java.sun.com/dtd/web-app_2_3.dtd">
+
+<!-- Copyright (C) 2010-2011 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<web-app id="TAVSERV-2.5.4">
+	<display-name>Taverna 2.5.4 Server</display-name>
+	<description>This is the front-end engine for Taverna 2.5.4 Server.</description>
+	<context-param>
+		<param-name>contextConfigLocation</param-name>
+		<param-value>WEB-INF/insecure.xml</param-value>
+		<description>Where Spring is to load its bean definitions from. DO NOT
+			CHANGE WITHOUT CONSULTING DOCUMENTATION.</description>
+	</context-param>
+	<context-param>
+		<param-name>log4jExposeWebAppRoot</param-name>
+		<param-value>false</param-value>
+		<description>
+			THIS IS STUPID! We have to do this so that Tomcat webapps do not
+			leak their configurations into each other via the log4j support
+			trying to be "smart".
+			http://javacolors.blogspot.co.uk/2010/08/tomcat-and-webxmls-webapprootkey.html
+			If you change this, good luck hunting down the weird crashes.
+		</description>
+	</context-param>
+
+	<filter>
+		<filter-name>springSecurityFilterChain</filter-name>
+		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+	</filter>
+	<filter-mapping>
+		<filter-name>springSecurityFilterChain</filter-name>
+		<url-pattern>/*</url-pattern>
+	</filter-mapping>
+	<listener>
+		<listener-class>
+			org.springframework.web.context.request.RequestContextListener
+		</listener-class>
+	</listener>
+	<listener>
+		<listener-class>
+			org.springframework.web.util.Log4jConfigListener
+		</listener-class>
+	</listener>
+	<listener>
+		<listener-class>
+			org.springframework.web.context.ContextLoaderListener
+		</listener-class>
+	</listener>
+
+	<servlet>
+		<servlet-name>CXFServlet</servlet-name>
+		<display-name>CXF Servlet</display-name>
+		<servlet-class>
+			org.apache.cxf.transport.servlet.CXFServlet
+		</servlet-class>
+		<load-on-startup>1</load-on-startup>
+	</servlet>
+
+	<servlet-mapping>
+		<servlet-name>CXFServlet</servlet-name>
+		<url-pattern>/*</url-pattern>
+	</servlet-mapping>
+
+	<resource-ref>
+		<description>
+			Resource reference to a factory for javax.mail.Session
+			instances that may be used for sending electronic mail
+			messages, preconfigured to connect to the appropriate
+			SMTP server.
+		</description>
+		<res-ref-name>mail/Session</res-ref-name>
+		<res-type>javax.mail.Session</res-type>
+		<res-auth>Container</res-auth>
+		<res-sharing-scope>Shareable</res-sharing-scope>
+	</resource-ref>
+</web-app>
diff --git a/server-webapp/src/main/webapp/WEB-INF/web-partsec.xml b/server-webapp/src/main/webapp/WEB-INF/web-partsec.xml
new file mode 100644
index 0000000..a2b545e
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/web-partsec.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE web-app
+    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+    "http://java.sun.com/dtd/web-app_2_3.dtd">
+
+<!-- Copyright (C) 2010-2011 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<web-app id="TAVSERV-2.5.4">
+	<display-name>Taverna 2.5.4 Server</display-name>
+	<description>This is the front-end engine for Taverna 2.5.4 Server.</description>
+	<context-param>
+		<param-name>contextConfigLocation</param-name>
+		<param-value>WEB-INF/partsecure.xml</param-value>
+		<description>Where Spring is to load its bean definitions from. DO NOT
+			CHANGE WITHOUT CONSULTING DOCUMENTATION.</description>
+	</context-param>
+	<context-param>
+		<param-name>log4jExposeWebAppRoot</param-name>
+		<param-value>false</param-value>
+		<description>
+			THIS IS STUPID! We have to do this so that Tomcat webapps do not
+			leak their configurations into each other via the log4j support
+			trying to be "smart".
+			http://javacolors.blogspot.co.uk/2010/08/tomcat-and-webxmls-webapprootkey.html
+			If you change this, good luck hunting down the weird crashes.
+		</description>
+	</context-param>
+
+	<filter>
+		<filter-name>springSecurityFilterChain</filter-name>
+		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+	</filter>
+	<filter-mapping>
+		<filter-name>springSecurityFilterChain</filter-name>
+		<url-pattern>/*</url-pattern>
+	</filter-mapping>
+	<listener>
+		<listener-class>
+			org.springframework.web.context.request.RequestContextListener
+		</listener-class>
+	</listener>
+	<listener>
+		<listener-class>
+			org.springframework.web.util.Log4jConfigListener
+		</listener-class>
+	</listener>
+	<listener>
+		<listener-class>
+			org.springframework.web.context.ContextLoaderListener
+		</listener-class>
+	</listener>
+
+	<servlet>
+		<servlet-name>CXFServlet</servlet-name>
+		<display-name>CXF Servlet</display-name>
+		<servlet-class>
+			org.apache.cxf.transport.servlet.CXFServlet
+		</servlet-class>
+		<load-on-startup>1</load-on-startup>
+	</servlet>
+
+	<servlet-mapping>
+		<servlet-name>CXFServlet</servlet-name>
+		<url-pattern>/*</url-pattern>
+	</servlet-mapping>
+
+	<resource-ref>
+		<description>
+			Resource reference to a factory for javax.mail.Session
+			instances that may be used for sending electronic mail
+			messages, preconfigured to connect to the appropriate
+			SMTP server.
+		</description>
+		<res-ref-name>mail/Session</res-ref-name>
+		<res-type>javax.mail.Session</res-type>
+		<res-auth>Container</res-auth>
+		<res-sharing-scope>Shareable</res-sharing-scope>
+	</resource-ref>
+</web-app>
diff --git a/server-webapp/src/main/webapp/WEB-INF/web-sec.xml b/server-webapp/src/main/webapp/WEB-INF/web-sec.xml
new file mode 100644
index 0000000..9a5395a
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/web-sec.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE web-app
+    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
+    "http://java.sun.com/dtd/web-app_2_3.dtd">
+
+<!-- Copyright (C) 2010-2011 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<web-app id="TAVSERV-2.5.4">
+	<display-name>Taverna 2.5.4 Server</display-name>
+	<description>This is the front-end engine for Taverna 2.5.4 Server.</description>
+	<context-param>
+		<param-name>contextConfigLocation</param-name>
+		<param-value>WEB-INF/secure.xml</param-value>
+		<description>Where Spring is to load its bean definitions from. DO NOT
+			CHANGE WITHOUT CONSULTING DOCUMENTATION.</description>
+	</context-param>
+	<context-param>
+		<param-name>log4jExposeWebAppRoot</param-name>
+		<param-value>false</param-value>
+		<description>
+			THIS IS STUPID! We have to do this so that Tomcat webapps do not
+			leak their configurations into each other via the log4j support
+			trying to be "smart".
+			http://javacolors.blogspot.co.uk/2010/08/tomcat-and-webxmls-webapprootkey.html
+			If you change this, good luck hunting down the weird crashes.
+		</description>
+	</context-param>
+
+	<filter>
+		<filter-name>springSecurityFilterChain</filter-name>
+		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+	</filter>
+	<filter-mapping>
+		<filter-name>springSecurityFilterChain</filter-name>
+		<url-pattern>/*</url-pattern>
+	</filter-mapping>
+	<listener>
+		<listener-class>
+			org.springframework.web.context.request.RequestContextListener
+		</listener-class>
+	</listener>
+	<listener>
+		<listener-class>
+			org.springframework.web.util.Log4jConfigListener
+		</listener-class>
+	</listener>
+	<listener>
+		<listener-class>
+			org.springframework.web.context.ContextLoaderListener
+		</listener-class>
+	</listener>
+
+	<servlet>
+		<servlet-name>CXFServlet</servlet-name>
+		<display-name>CXF Servlet</display-name>
+		<servlet-class>
+			org.apache.cxf.transport.servlet.CXFServlet
+		</servlet-class>
+		<load-on-startup>1</load-on-startup>
+	</servlet>
+
+	<servlet-mapping>
+		<servlet-name>CXFServlet</servlet-name>
+		<url-pattern>/*</url-pattern>
+	</servlet-mapping>
+
+	<resource-ref>
+		<description>
+			Resource reference to a factory for javax.mail.Session
+			instances that may be used for sending electronic mail
+			messages, preconfigured to connect to the appropriate
+			SMTP server.
+		</description>
+		<res-ref-name>mail/Session</res-ref-name>
+		<res-type>javax.mail.Session</res-type>
+		<res-auth>Container</res-auth>
+		<res-sharing-scope>Shareable</res-sharing-scope>
+	</resource-ref>
+</web-app>
diff --git a/server-webapp/src/main/webapp/WEB-INF/webappBeans.xml b/server-webapp/src/main/webapp/WEB-INF/webappBeans.xml
new file mode 100644
index 0000000..22e74b3
--- /dev/null
+++ b/server-webapp/src/main/webapp/WEB-INF/webappBeans.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2010-2012 The University of Manchester See the file "LICENSE" 
+	for license terms. -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:context="http://www.springframework.org/schema/context"
+	xmlns:aop="http://www.springframework.org/schema/aop"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:jaxrs="http://cxf.apache.org/jaxrs"
+	xmlns:jaxws="http://cxf.apache.org/jaxws"
+	xmlns:cxf="http://cxf.apache.org/core"
+	xmlns:security="http://www.springframework.org/schema/security"
+	xmlns:util="http://www.springframework.org/schema/util"
+	xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
+		http://cxf.apache.org/configuration/security http://cxf.apache.org/schemas/configuration/security.xsd
+		http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
+		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd
+		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
+		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
+		http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
+		http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd
+		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
+
+	<description>
+		The Taverna Server bean, plus supporting configuration.
+	</description>
+
+	<import resource="classpath:META-INF/cxf/cxf.xml" />
+	<import resource="providers.xml" />
+
+	<context:annotation-config />
+	<context:component-scan base-package="org.taverna.server" />
+	<bean id="servletContextPropertyConfigurer"
+		class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
+		<property name="ignoreUnresolvablePlaceholders" value="true" />
+		<property name="localOverride" value="true" />
+		<property name="order" value="0" />
+		<property name="location" value="/WEB-INF/tavernaserver.properties" />
+	</bean>
+
+	<jaxws:server id="master_SOAP"
+		serviceClass="org.taverna.server.master.TavernaServer" address="/soap">
+		<jaxws:serviceBean>
+			<ref bean="webapp" />
+		</jaxws:serviceBean>
+		<jaxws:properties>
+			<entry key="mtom-enabled" value="true" />
+		</jaxws:properties>
+		<!-- <jaxws:dataBinding><ref bean="addStylesheet"/></jaxws:dataBinding> -->
+	</jaxws:server>
+
+	<jaxrs:server id="root_REST" address="/">
+		<jaxrs:serviceBeans>
+			<ref bean="root_facade" />
+		</jaxrs:serviceBeans>
+		<jaxrs:properties>
+			<entry key="org.apache.cxf.endpoint.private" value="true" />
+		</jaxrs:properties>
+	</jaxrs:server>
+
+	<jaxrs:server id="master_REST" address="/rest"
+		staticSubresourceResolution="true">
+		<jaxrs:serviceBeans>
+			<ref bean="webapp" />
+		</jaxrs:serviceBeans>
+		<jaxrs:features>
+			<!-- <cxf:logging /> --><!-- NOISY! -->
+		</jaxrs:features>
+		<jaxrs:providers>
+			<ref bean="ExceptionProvider.AccessDenied" />
+			<ref bean="ExceptionProvider.BadInputPortName" />
+			<ref bean="ExceptionProvider.BadPropertyValue" />
+			<ref bean="ExceptionProvider.BadStateChange" />
+			<ref bean="ExceptionProvider.FilesystemAccess" />
+			<ref bean="ExceptionProvider.GeneralFailure" />
+			<ref bean="ExceptionProvider.IllegalArgument" />
+			<ref bean="ExceptionProvider.ImplementationProblem" />
+			<ref bean="ExceptionProvider.InvalidCredential" />
+			<ref bean="ExceptionProvider.JAXBException" />
+			<ref bean="ExceptionProvider.NegotiationFailed" />
+			<ref bean="ExceptionProvider.NoCreate" />
+			<ref bean="ExceptionProvider.NoCredential" />
+			<ref bean="ExceptionProvider.NoDestroy" />
+			<ref bean="ExceptionProvider.NoDirectoryEntry" />
+			<ref bean="ExceptionProvider.NoListener" />
+			<ref bean="ExceptionProvider.NoUpdate" />
+			<ref bean="ExceptionProvider.NotOwner" />
+			<ref bean="ExceptionProvider.Overloaded" />
+			<ref bean="ExceptionProvider.UnknownRun" />
+			<ref bean="MessagingProvider.File" />
+			<ref bean="MessagingProvider.FileConcatenation" />
+			<ref bean="MessagingProvider.FileSegment" />
+			<ref bean="MessagingProvider.InputStream" />
+			<ref bean="MessagingProvider.T2flow" />
+			<ref bean="MessagingProvider.Permission" />
+			<ref bean="MessagingProvider.URIList" />
+			<ref bean="MessagingProvider.ZipStream" />
+			<ref bean="jsonProvider" />
+			<ref bean="atomEntryHandler" />
+			<ref bean="atomFeedHandler" />
+			<bean class="org.apache.cxf.jaxrs.model.wadl.WadlGenerator">
+				<property name="addResourceAndMethodIds" value="true"/>
+			</bean>
+		</jaxrs:providers>
+		<jaxrs:outInterceptors>
+			<ref bean="Interceptor.FlushThreadLocalCache" />
+		</jaxrs:outInterceptors>
+ 	</jaxrs:server>
+
+	<jaxrs:server id="AtomFeed" address="/feed">
+		<jaxrs:serviceBeans>
+			<ref bean="feed" />
+		</jaxrs:serviceBeans>
+		<jaxrs:properties>
+			<entry key="org.apache.cxf.endpoint.private" value="true" />
+		</jaxrs:properties>
+		<jaxrs:providers>
+			<ref bean="atomEntryHandler" />
+			<ref bean="atomFeedHandler" />
+		</jaxrs:providers>
+	</jaxrs:server>
+
+	<jaxrs:server id="AdministrationInterface" address="/admin"
+		staticSubresourceResolution="true">
+		<jaxrs:serviceBeans>
+			<ref bean="admin" />
+		</jaxrs:serviceBeans>
+		<jaxrs:providers>
+			<ref bean="jsonProvider" />
+		</jaxrs:providers>
+	</jaxrs:server>
+
+	<bean id="jsonProvider" class="org.apache.cxf.jaxrs.provider.json.JSONProvider">
+		<property name="ignoreNamespaces" value="true" />
+		<property name="namespaceMap" ref="jsonNamespaceMap" />
+	</bean>
+	<util:map id="jsonNamespaceMap" key-type="java.lang.String"
+		value-type="java.lang.String">
+		<entry key="http://www.w3.org/1999/xlink" value="" />
+		<entry key="http://www.w3.org/2000/09/xmldsig#" value="" />
+		<entry key="http://schema.ogf.org/urf/2003/09/urf" value="" />
+		<entry key="http://ns.taverna.org.uk/2010/xml/server/" value="" />
+		<entry key="http://ns.taverna.org.uk/2010/xml/server/rest/" value="" />
+		<entry key="http://ns.taverna.org.uk/2010/xml/server/soap/" value="" />
+		<entry key="http://ns.taverna.org.uk/2010/xml/server/feed/" value="" />
+		<entry key="http://ns.taverna.org.uk/2010/xml/server/admin/" value="" />
+		<entry key="http://ns.taverna.org.uk/2010/port/" value="" />
+		<entry key="http://ns.taverna.org.uk/2010/run/" value="" />
+	</util:map>
+
+	<bean id="root_facade" class="org.taverna.server.master.facade.Facade">
+		<property name="file" value="/welcome.html" />
+		<property name="contextualizer" ref="contextualizer" />
+	</bean>
+
+	<bean class="org.taverna.server.master.common.Uri.Rewriter"
+		autowire="byType" id="URI-Rewriter-Thunk">
+		<property name="suppressSecurity" value="${suppressRewriteEngine}" />
+		<property name="rewriteTarget" value="${default.webapp}" />
+	</bean>
+	<security:authentication-manager>
+		<security:authentication-provider ref="workflowInternalAuthProvder" />
+		<security:authentication-provider ref="authProvider" />
+	</security:authentication-manager>
+
+	<bean id="WSDLHeadOptionsInterceptor"
+		class="org.taverna.server.master.utils.WSDLHeadOptionsInterceptor" />
+	<cxf:bus>
+		<cxf:inInterceptors>
+			<ref bean="WSDLHeadOptionsInterceptor" />
+		</cxf:inInterceptors>
+	</cxf:bus>
+
+	<aop:aspectj-autoproxy proxy-target-class="true" />
+	<security:global-method-security
+		jsr250-annotations="enabled" />
+
+	<security:http realm="${http.realmName}" create-session="never"
+		use-expressions="true">
+		<security:http-basic />
+		<security:intercept-url pattern="/"
+			requires-channel="any" access="permitAll" />
+		<security:intercept-url pattern="/rest"
+			requires-channel="${requiredChannel}" access="permitAll" />
+		<security:intercept-url pattern="/rest/"
+			requires-channel="${requiredChannel}" access="permitAll" />
+		<security:intercept-url pattern="/rest/policy"
+			requires-channel="${requiredChannel}" access="permitAll" />
+		<security:intercept-url pattern="/rest/policy/"
+			requires-channel="${requiredChannel}" access="permitAll" />
+		<security:intercept-url pattern="/services/**"
+			requires-channel="${requiredChannel}" access="permitAll" />
+		<security:intercept-url pattern="/soap/**" method="GET"
+			requires-channel="${requiredChannel}" access="permitAll" />
+		<security:intercept-url pattern="/soap/**" method="POST"
+			requires-channel="${requiredChannel}"
+			access="hasRole('ROLE_tavernauser')" />
+		<security:intercept-url pattern="/admin"
+			requires-channel="${requiredChannel}"
+			access="hasRole('ROLE_tavernasuperuser')" />
+		<security:intercept-url pattern="/admin/**"
+			requires-channel="${requiredChannel}"
+			access="hasRole('ROLE_tavernasuperuser')" />
+		<security:intercept-url pattern="/rest/**"
+			requires-channel="${requiredChannel}"
+			access="hasAnyRole('ROLE_tavernauser','ROLE_tavernaworkflow')" />
+		<security:intercept-url pattern="/feed"
+			requires-channel="${requiredChannel}"
+			access="hasRole('ROLE_tavernauser')" />
+		<security:intercept-url pattern="/feed/**"
+			requires-channel="${requiredChannel}"
+			access="hasRole('ROLE_tavernauser')" />
+	</security:http>
+</beans>
diff --git a/server-webapp/src/misc/xsd/persistence_1_0.xsd b/server-webapp/src/misc/xsd/persistence_1_0.xsd
new file mode 100644
index 0000000..a485e30
--- /dev/null
+++ b/server-webapp/src/misc/xsd/persistence_1_0.xsd
@@ -0,0 +1,305 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- persistence.xml schema -->
+<xsd:schema targetNamespace="http://java.sun.com/xml/ns/persistence" 
+  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+  xmlns:persistence="http://java.sun.com/xml/ns/persistence"
+  elementFormDefault="qualified" 
+  attributeFormDefault="unqualified" 
+  version="1.0">
+
+  <xsd:annotation>
+    <xsd:documentation>
+      @(#)persistence_1_0.xsd  1.0  Feb 9 2006
+    </xsd:documentation>
+  </xsd:annotation>
+
+  <xsd:annotation>
+    <xsd:documentation>
+
+      DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+      Copyright 2005-2007 Sun Microsystems, Inc. All rights reserved.
+
+      The contents of this file are subject to the terms of either the
+      GNU General Public License Version 2 only ("GPL") or the Common
+      Development and Distribution License("CDDL") (collectively, the
+      "License").  You may not use this file except in compliance with
+      the License. You can obtain a copy of the License at
+      https://glassfish.dev.java.net/public/CDDL+GPL.html or
+      glassfish/bootstrap/legal/LICENSE.txt.  See the License for the
+      specific language governing permissions and limitations under the
+      License.
+
+      When distributing the software, include this License Header
+      Notice in each file and include the License file at
+      glassfish/bootstrap/legal/LICENSE.txt.  Sun designates this
+      particular file as subject to the "Classpath" exception as
+      provided by Sun in the GPL Version 2 section of the License file
+      that accompanied this code.  If applicable, add the following
+      below the License Header, with the fields enclosed by brackets []
+      replaced by your own identifying information:
+      "Portions Copyrighted [year] [name of copyright owner]"
+
+      Contributor(s):
+
+      If you wish your version of this file to be governed by only the
+      CDDL or only the GPL Version 2, indicate your decision by adding
+      "[Contributor] elects to include this software in this
+      distribution under the [CDDL or GPL Version 2] license."  If you
+      don't indicate a single choice of license, a recipient has the
+      option to distribute your version of this file under either the
+      CDDL, the GPL Version 2 or to extend the choice of license to its
+      licensees as provided above.  However, if you add GPL Version 2
+      code and therefore, elected the GPL Version 2 license, then the
+      option applies only if the new code is made subject to such
+      option by the copyright holder.
+
+    </xsd:documentation>
+  </xsd:annotation>
+
+   <xsd:annotation>
+     <xsd:documentation><![CDATA[
+
+     This is the XML Schema for the persistence configuration file.
+     The file must be named "META-INF/persistence.xml" in the 
+     persistence archive.
+     Persistence configuration files must indicate
+     the persistence schema by using the persistence namespace:
+
+     http://java.sun.com/xml/ns/persistence
+
+     and indicate the version of the schema by
+     using the version element as shown below:
+
+      <persistence xmlns="http://java.sun.com/xml/ns/persistence"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
+          http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
+        version="1.0">
+          ...
+      </persistence>
+
+    ]]></xsd:documentation>
+  </xsd:annotation>
+
+  <xsd:simpleType name="versionType">
+    <xsd:restriction base="xsd:token">
+      <xsd:pattern value="[0-9]+(\.[0-9]+)*"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+  <!-- **************************************************** -->
+
+  <xsd:element name="persistence">
+    <xsd:complexType>
+      <xsd:sequence>
+
+        <!-- **************************************************** -->
+
+        <xsd:element name="persistence-unit" 
+                     minOccurs="0" maxOccurs="unbounded">
+          <xsd:complexType>
+            <xsd:annotation>
+              <xsd:documentation>
+
+                Configuration of a persistence unit.
+
+              </xsd:documentation>
+            </xsd:annotation>
+            <xsd:sequence>
+
+            <!-- **************************************************** -->
+
+              <xsd:element name="description" type="xsd:string" 
+                           minOccurs="0">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    Textual description of this persistence unit.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:element>
+
+              <!-- **************************************************** -->
+
+              <xsd:element name="provider" type="xsd:string" 
+                           minOccurs="0">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    Provider class that supplies EntityManagers for this 
+                    persistence unit.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:element>
+
+              <!-- **************************************************** -->
+
+              <xsd:element name="jta-data-source" type="xsd:string" 
+                           minOccurs="0">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    The container-specific name of the JTA datasource to use.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:element>
+
+              <!-- **************************************************** -->
+
+              <xsd:element name="non-jta-data-source" type="xsd:string" 
+                           minOccurs="0">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    The container-specific name of a non-JTA datasource to use.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:element>
+
+              <!-- **************************************************** -->
+
+              <xsd:element name="mapping-file" type="xsd:string" 
+                           minOccurs="0" maxOccurs="unbounded">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    File containing mapping information. Loaded as a resource 
+                    by the persistence provider.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:element>
+
+              <!-- **************************************************** -->
+
+              <xsd:element name="jar-file" type="xsd:string" 
+                           minOccurs="0" maxOccurs="unbounded">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    Jar file that should be scanned for entities. 
+                    Not applicable to Java SE persistence units.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:element>
+
+              <!-- **************************************************** -->
+
+              <xsd:element name="class" type="xsd:string" 
+                           minOccurs="0" maxOccurs="unbounded">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    Class to scan for annotations.  It should be annotated 
+                    with either @Entity, @Embeddable or @MappedSuperclass.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:element>
+
+              <!-- **************************************************** -->
+
+              <xsd:element name="exclude-unlisted-classes" type="xsd:boolean" 
+                           default="false" minOccurs="0">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    When set to true then only listed classes and jars will 
+                    be scanned for persistent classes, otherwise the enclosing 
+                    jar or directory will also be scanned. Not applicable to 
+                    Java SE persistence units.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+              </xsd:element>
+
+              <!-- **************************************************** -->
+
+              <xsd:element name="properties" minOccurs="0">
+                <xsd:annotation>
+                  <xsd:documentation>
+
+                    A list of vendor-specific properties.
+
+                  </xsd:documentation>
+                </xsd:annotation>
+                <xsd:complexType>
+                  <xsd:sequence>
+                    <xsd:element name="property" 
+                                 minOccurs="0" maxOccurs="unbounded">
+                      <xsd:annotation>
+                        <xsd:documentation>
+                          A name-value pair.
+                        </xsd:documentation>
+                      </xsd:annotation>
+                      <xsd:complexType>
+                        <xsd:attribute name="name" type="xsd:string" 
+                                       use="required"/>
+                        <xsd:attribute name="value" type="xsd:string" 
+                                       use="required"/>
+                      </xsd:complexType>
+                    </xsd:element>
+                  </xsd:sequence>
+                </xsd:complexType>
+              </xsd:element>
+
+            </xsd:sequence>
+
+            <!-- **************************************************** -->
+
+            <xsd:attribute name="name" type="xsd:string" use="required">
+              <xsd:annotation>
+                <xsd:documentation>
+
+                  Name used in code to reference this persistence unit.
+
+                </xsd:documentation>
+              </xsd:annotation>
+            </xsd:attribute>
+
+            <!-- **************************************************** -->
+
+            <xsd:attribute name="transaction-type" 
+                           type="persistence:persistence-unit-transaction-type">
+              <xsd:annotation>
+                <xsd:documentation>
+
+                  Type of transactions used by EntityManagers from this 
+                  persistence unit.
+
+                </xsd:documentation>
+              </xsd:annotation>
+            </xsd:attribute>
+
+          </xsd:complexType>
+        </xsd:element>
+      </xsd:sequence>
+      <xsd:attribute name="version" type="persistence:versionType" 
+                     fixed="1.0" use="required"/>
+    </xsd:complexType>
+  </xsd:element>
+
+  <!-- **************************************************** -->
+
+  <xsd:simpleType name="persistence-unit-transaction-type">
+    <xsd:annotation>
+      <xsd:documentation>
+
+        public enum TransactionType { JTA, RESOURCE_LOCAL };
+
+      </xsd:documentation>
+    </xsd:annotation>
+    <xsd:restriction base="xsd:token">
+      <xsd:enumeration value="JTA"/>
+      <xsd:enumeration value="RESOURCE_LOCAL"/>
+    </xsd:restriction>
+  </xsd:simpleType>
+
+</xsd:schema>
+
diff --git a/server-webapp/src/test/java/org/taverna/server/master/JaxbSanityTest.java b/server-webapp/src/test/java/org/taverna/server/master/JaxbSanityTest.java
new file mode 100644
index 0000000..79c026b
--- /dev/null
+++ b/server-webapp/src/test/java/org/taverna/server/master/JaxbSanityTest.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Arrays;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.SchemaOutputResolver;
+import javax.xml.transform.Result;
+import javax.xml.transform.stream.StreamResult;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.taverna.server.master.admin.Admin;
+import org.taverna.server.master.common.Credential.KeyPair;
+import org.taverna.server.master.common.Credential.Password;
+import org.taverna.server.master.common.Capability;
+import org.taverna.server.master.common.DirEntryReference;
+import org.taverna.server.master.common.InputDescription;
+import org.taverna.server.master.common.Permission;
+import org.taverna.server.master.common.ProfileList;
+import org.taverna.server.master.common.RunReference;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.common.Uri;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.rest.DirectoryContents;
+import org.taverna.server.master.rest.ListenerDefinition;
+import org.taverna.server.master.rest.MakeOrUpdateDirEntry;
+import org.taverna.server.master.rest.TavernaServerInputREST.InDesc;
+import org.taverna.server.master.rest.TavernaServerInputREST.InputsDescriptor;
+import org.taverna.server.master.rest.TavernaServerListenersREST.ListenerDescription;
+import org.taverna.server.master.rest.TavernaServerListenersREST.Listeners;
+import org.taverna.server.master.rest.TavernaServerListenersREST.Properties;
+import org.taverna.server.master.rest.TavernaServerListenersREST.PropertyDescription;
+import org.taverna.server.master.rest.TavernaServerREST.EnabledNotificationFabrics;
+import org.taverna.server.master.rest.TavernaServerREST.PermittedListeners;
+import org.taverna.server.master.rest.TavernaServerREST.PermittedWorkflows;
+import org.taverna.server.master.rest.TavernaServerREST.PolicyView.CapabilityList;
+import org.taverna.server.master.rest.TavernaServerREST.PolicyView.PolicyDescription;
+import org.taverna.server.master.rest.TavernaServerREST.RunList;
+import org.taverna.server.master.rest.TavernaServerREST.ServerDescription;
+import org.taverna.server.master.rest.TavernaServerRunREST.RunDescription;
+import org.taverna.server.master.rest.TavernaServerSecurityREST;
+import org.taverna.server.master.rest.TavernaServerSecurityREST.CredentialHolder;
+import org.taverna.server.master.soap.DirEntry;
+import org.taverna.server.master.soap.FileContents;
+import org.taverna.server.master.soap.PermissionList;
+
+/**
+ * This test file ensures that the JAXB bindings will work once deployed instead
+ * of mysteriously failing in service.
+ * 
+ * @author Donal Fellows
+ */
+public class JaxbSanityTest {
+	SchemaOutputResolver sink;
+	StringWriter schema;
+
+	String schema() {
+		return schema.toString();
+	}
+
+	@Before
+	public void init() {
+		schema = new StringWriter();
+		sink = new SchemaOutputResolver() {
+			@Override
+			public Result createOutput(String namespaceUri,
+					String suggestedFileName) throws IOException {
+				StreamResult sr = new StreamResult(schema);
+				sr.setSystemId("/dev/null");
+				return sr;
+			}
+		};
+		assertEquals("", schema());
+	}
+
+	private boolean printSchema = false;
+
+	private void testJAXB(Class<?>... classes) throws Exception {
+		JAXBContext.newInstance(classes).generateSchema(sink);
+		if (printSchema)
+			System.out.println(schema());
+		assertTrue(schema().length() > 0);
+	}
+
+	@Test
+	public void testJAXBForDirEntryReference() throws Exception {
+		JAXBContext.newInstance(DirEntryReference.class).generateSchema(sink);
+		assertTrue(schema().length() > 0);
+	}
+
+	@Test
+	public void testJAXBForInputDescription() throws Exception {
+		testJAXB(InputDescription.class);
+	}
+
+	@Test
+	public void testJAXBForRunReference() throws Exception {
+		testJAXB(RunReference.class);
+	}
+
+	@Test
+	public void testJAXBForWorkflow() throws Exception {
+		testJAXB(Workflow.class);
+	}
+
+	@Test
+	public void testJAXBForStatus() throws Exception {
+		testJAXB(Status.class);
+	}
+
+	@Test
+	public void testJAXBForUri() throws Exception {
+		testJAXB(Uri.class);
+	}
+
+	@Test
+	public void testJAXBForDirectoryContents() throws Exception {
+		testJAXB(DirectoryContents.class);
+	}
+
+	@Test
+	public void testJAXBForListenerDefinition() throws Exception {
+		testJAXB(ListenerDefinition.class);
+	}
+
+	@Test
+	public void testJAXBForMakeOrUpdateDirEntry() throws Exception {
+		testJAXB(MakeOrUpdateDirEntry.class);
+	}
+
+	@Test
+	public void testJAXBForInDesc() throws Exception {
+		testJAXB(InDesc.class);
+	}
+
+	@Test
+	public void testJAXBForInputsDescriptor() throws Exception {
+		testJAXB(InputsDescriptor.class);
+	}
+
+	@Test
+	public void testJAXBForListenerDescription() throws Exception {
+		testJAXB(ListenerDescription.class);
+	}
+
+	@Test
+	public void testJAXBForListeners() throws Exception {
+		testJAXB(Listeners.class);
+	}
+
+	@Test
+	public void testJAXBForProperties() throws Exception {
+		testJAXB(Properties.class);
+	}
+
+	@Test
+	public void testJAXBForPropertyDescription() throws Exception {
+		testJAXB(PropertyDescription.class);
+	}
+
+	@Test
+	public void testJAXBForPermittedListeners() throws Exception {
+		testJAXB(PermittedListeners.class);
+	}
+
+	@Test
+	public void testJAXBForPermittedWorkflows() throws Exception {
+		testJAXB(PermittedWorkflows.class);
+	}
+
+	@Test
+	public void testJAXBForEnabledNotifiers() throws Exception {
+		testJAXB(EnabledNotificationFabrics.class);
+	}
+
+	@Test
+	public void testJAXBForServerDescription() throws Exception {
+		testJAXB(ServerDescription.class);
+	}
+
+	@Test
+	public void testJAXBForRunDescription() throws Exception {
+		testJAXB(RunDescription.class);
+	}
+
+	@Test
+	public void testJAXBForRunList() throws Exception {
+		testJAXB(RunList.class);
+	}
+
+	@Test
+	public void testJAXBForPolicyDescription() throws Exception {
+		testJAXB(PolicyDescription.class);
+	}
+
+	@Test
+	public void testJAXBForSecurityCredential() throws Exception {
+		testJAXB(CredentialHolder.class);
+	}
+
+	@Test
+	public void testJAXBForSecurityCredentialList() throws Exception {
+		testJAXB(TavernaServerSecurityREST.CredentialList.class);
+	}
+
+	@Test
+	public void testJAXBForSecurityTrust() throws Exception {
+		testJAXB(Trust.class);
+	}
+
+	@Test
+	public void testJAXBForSecurityTrustList() throws Exception {
+		testJAXB(TavernaServerSecurityREST.TrustList.class);
+	}
+
+	@Test
+	public void testJAXBForPermission() throws Exception {
+		testJAXB(Permission.class);
+	}
+
+	@Test
+	public void testJAXBForSecurityPermissionDescription() throws Exception {
+		testJAXB(TavernaServerSecurityREST.PermissionDescription.class);
+	}
+
+	@Test
+	public void testJAXBForSecurityPermissionsDescription() throws Exception {
+		testJAXB(TavernaServerSecurityREST.PermissionsDescription.class);
+	}
+
+	@Test
+	public void testJAXBForSecurityDescriptor() throws Exception {
+		testJAXB(TavernaServerSecurityREST.Descriptor.class);
+	}
+
+	@Test
+	public void testJAXBForProfileList() throws Exception {
+		testJAXB(ProfileList.class);
+	}
+
+	@Test
+	public void testJAXBForDirEntry() throws Exception {
+		testJAXB(DirEntry.class);
+	}
+
+	@Test
+	public void testJAXBForCapability() throws Exception {
+		testJAXB(Capability.class);
+	}
+
+	@Test
+	public void testJAXBForCapabilityList() throws Exception {
+		testJAXB(CapabilityList.class);
+	}
+
+	@Test
+	public void testJAXBForEverythingREST() throws Exception {
+		testJAXB(DirEntryReference.class, InputDescription.class,
+				RunReference.class, Workflow.class, Status.class,
+				DirectoryContents.class, InDesc.class,
+				ListenerDefinition.class, MakeOrUpdateDirEntry.class,
+				InputsDescriptor.class, ListenerDescription.class,
+				Listeners.class, Properties.class, PropertyDescription.class,
+				PermittedListeners.class, PermittedWorkflows.class,
+				EnabledNotificationFabrics.class, ServerDescription.class,
+				RunDescription.class, Uri.class, RunList.class,
+				PolicyDescription.class, CredentialHolder.class, Trust.class,
+				TavernaServerSecurityREST.CredentialList.class,
+				TavernaServerSecurityREST.TrustList.class, Permission.class,
+				TavernaServerSecurityREST.Descriptor.class,
+				TavernaServerSecurityREST.PermissionDescription.class,
+				TavernaServerSecurityREST.PermissionsDescription.class,
+				ProfileList.class, Capability.class, CapabilityList.class);
+	}
+
+	@Test
+	public void testJAXBForEverythingSOAP() throws Exception {
+		testJAXB(DirEntry.class, FileContents.class, InputDescription.class,
+				Permission.class, PermissionList.class,
+				PermissionList.SinglePermissionMapping.class,
+				RunReference.class, Status.class, Trust.class, Uri.class,
+				ProfileList.class, Workflow.class, Capability.class);
+	}
+
+	@Test
+	public void testUserPassSerializeDeserialize() throws Exception {
+		JAXBContext c = JAXBContext.newInstance(CredentialHolder.class);
+
+		Password password = new Password();
+		password.username = "foo";
+		password.password = "bar";
+
+		// Serialize
+		StringWriter sw = new StringWriter();
+		CredentialHolder credIn = new CredentialHolder(password);
+		c.createMarshaller().marshal(credIn, sw);
+
+		// Deserialize
+		StringReader sr = new StringReader(sw.toString());
+		Object credOutObj = c.createUnmarshaller().unmarshal(sr);
+
+		// Test value-equivalence
+		assertEquals(credIn.getClass(), credOutObj.getClass());
+		CredentialHolder credOut = (CredentialHolder) credOutObj;
+		assertEquals(credIn.credential.getClass(),
+				credOut.credential.getClass());
+		assertEquals(credIn.getUserpass().username,
+				credOut.getUserpass().username);
+		assertEquals(credIn.getUserpass().password,
+				credOut.getUserpass().password);
+	}
+
+	@Test
+	public void testKeypairSerializeDeserialize() throws Exception {
+		JAXBContext c = JAXBContext.newInstance(CredentialHolder.class);
+
+		KeyPair keypair = new KeyPair();
+		keypair.credentialName = "foo";
+		keypair.credentialBytes = new byte[] { 1, 2, 3 };
+
+		// Serialize
+		StringWriter sw = new StringWriter();
+		CredentialHolder credIn = new CredentialHolder(keypair);
+		c.createMarshaller().marshal(credIn, sw);
+
+		// Deserialize
+		StringReader sr = new StringReader(sw.toString());
+		Object credOutObj = c.createUnmarshaller().unmarshal(sr);
+
+		// Test value-equivalence
+		assertEquals(credIn.getClass(), credOutObj.getClass());
+		CredentialHolder credOut = (CredentialHolder) credOutObj;
+		assertEquals(credIn.credential.getClass(),
+				credOut.credential.getClass());
+		assertEquals(credIn.getKeypair().credentialName,
+				credOut.getKeypair().credentialName);
+		assertTrue(Arrays.equals(credIn.getKeypair().credentialBytes,
+				credOut.getKeypair().credentialBytes));
+	}
+
+	@Test
+	public void testJAXBforAdmininstration() throws Exception {
+		testJAXB(Admin.AdminDescription.class);
+	}
+}
diff --git a/server-webapp/src/test/java/org/taverna/server/master/TavernaServerImplTest.java b/server-webapp/src/test/java/org/taverna/server/master/TavernaServerImplTest.java
new file mode 100644
index 0000000..9665cf0
--- /dev/null
+++ b/server-webapp/src/test/java/org/taverna/server/master/TavernaServerImplTest.java
@@ -0,0 +1,246 @@
+package org.taverna.server.master;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonMap;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.taverna.server.master.api.ManagementModel;
+import org.taverna.server.master.common.RunReference;
+import org.taverna.server.master.exceptions.BadPropertyValueException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.mocks.ExampleRun;
+import org.taverna.server.master.mocks.MockPolicy;
+import org.taverna.server.master.mocks.SimpleListenerFactory;
+import org.taverna.server.master.mocks.SimpleNonpersistentRunStore;
+
+public class TavernaServerImplTest {
+	private TavernaServer server;
+	private MockPolicy policy;
+	private SimpleNonpersistentRunStore store;
+	@java.lang.SuppressWarnings("unused")
+	private ExampleRun.Builder runFactory;
+	private SimpleListenerFactory lFactory;
+	private TavernaServerSupport support;
+
+	private String lrunname;
+	private String lrunconf;
+
+	Listener makeListener(TavernaRun run, final String config) {
+		lrunname = run.toString();
+		lrunconf = config;
+		return new Listener() {
+			@Override
+			public String getConfiguration() {
+				return config;
+			}
+
+			@Override
+			public String getName() {
+				return "bar";
+			}
+
+			@Override
+			public String getProperty(String propName)
+					throws NoListenerException {
+				throw new NoListenerException();
+			}
+
+			@Override
+			public String getType() {
+				return "foo";
+			}
+
+			@Override
+			public String[] listProperties() {
+				return new String[0];
+			}
+
+			@Override
+			public void setProperty(String propName, String value)
+					throws NoListenerException, BadPropertyValueException {
+				throw new NoListenerException();
+			}
+		};
+	}
+
+	@Before
+	public void wireup() throws Exception {
+		// Wire everything up; ought to be done with Spring, but this works...
+		server = new TavernaServer() {
+			@Override
+			protected RunREST makeRunInterface() {
+				return new RunREST() {
+					@Override
+					protected ListenersREST makeListenersInterface() {
+						return new ListenersREST() {
+							@Override
+							protected SingleListenerREST makeListenerInterface() {
+								return new SingleListenerREST() {
+									@Override
+									protected ListenerPropertyREST makePropertyInterface() {
+										return new ListenerPropertyREST() {
+										};
+									}
+								};
+							}
+						};
+					}
+
+					@Override
+					protected RunSecurityREST makeSecurityInterface() {
+						return new RunSecurityREST() {
+						};
+					}
+
+					@Override
+					protected DirectoryREST makeDirectoryInterface() {
+						return new DirectoryREST() {
+						};
+					}
+
+					@Override
+					protected InputREST makeInputInterface() {
+						return new InputREST() {
+						};
+					}
+
+					@Override
+					protected InteractionFeed makeInteractionFeed() {
+						return null; // TODO...
+					}
+				};
+			}
+
+			@Override
+			public PolicyView getPolicyDescription() {
+				return new PolicyREST();
+			}
+		};
+		support = new TavernaServerSupport();
+		server.setSupport(support);
+		support.setWebapp(server);
+		support.setLogGetPrincipalFailures(false);
+		support.setStateModel(new ManagementModel() {
+			@Override
+			public boolean getAllowNewWorkflowRuns() {
+				return true;
+			}
+
+			@Override
+			public boolean getLogIncomingWorkflows() {
+				return false;
+			}
+
+			@Override
+			public boolean getLogOutgoingExceptions() {
+				return false;
+			}
+
+			@Override
+			public void setAllowNewWorkflowRuns(boolean allowNewWorkflowRuns) {
+			}
+
+			@Override
+			public void setLogIncomingWorkflows(boolean logIncomingWorkflows) {
+			}
+
+			@Override
+			public void setLogOutgoingExceptions(boolean logOutgoingExceptions) {
+			}
+
+			@Override
+			public String getUsageRecordLogFile() {
+				return null;
+			}
+
+			@Override
+			public void setUsageRecordLogFile(String usageRecordLogFile) {
+			}
+		});
+		server.setPolicy(policy = new MockPolicy());
+		support.setPolicy(policy);
+		server.setRunStore(store = new SimpleNonpersistentRunStore());
+		support.setRunStore(store);
+		store.setPolicy(policy);
+		support.setRunFactory(runFactory = new ExampleRun.Builder(1));
+		support.setListenerFactory(lFactory = new SimpleListenerFactory());
+		lFactory.setBuilders(singletonMap(
+				"foo",
+				(SimpleListenerFactory.Builder) new SimpleListenerFactory.Builder() {
+					@Override
+					public Listener build(TavernaRun run, String configuration)
+							throws NoListenerException {
+						return makeListener(run, configuration);
+					}
+				}));
+	}
+
+	@Test
+	public void defaults1() {
+		assertNotNull(server);
+	}
+
+	@Test
+	public void defaults2() {
+		assertEquals(10, server.getServerMaxRuns());
+	}
+
+	@Test
+	public void defaults3() {
+		assertEquals(1, server.getServerListeners().length);
+	}
+
+	@Test
+	public void defaults4() {
+		assertNotNull(support.getPrincipal());
+	}
+
+	@Test
+	public void serverAsksPolicyForMaxRuns() {
+		int oldmax = policy.maxruns;
+		try {
+			policy.maxruns = 1;
+			assertEquals(1, server.getServerMaxRuns());
+		} finally {
+			policy.maxruns = oldmax;
+		}
+	}
+
+	@Test
+	public void makeAndKillARun() throws NoUpdateException, UnknownRunException {
+		RunReference rr = server.submitWorkflow(null);
+		assertNotNull(rr);
+		assertNotNull(rr.name);
+		server.destroyRun(rr.name);
+	}
+
+	@Test
+	public void makeListenKillRun() throws Exception {
+		RunReference run = server.submitWorkflow(null);
+		try {
+			lrunname = lrunconf = null;
+			assertEquals(asList("foo"), asList(server.getServerListeners()));
+			String l = server.addRunListener(run.name, "foo", "foobar");
+			assertEquals("bar", l);
+			assertEquals("foobar", lrunconf);
+			assertEquals(lrunname, support.getRun(run.name).toString());
+			assertEquals(asList("default", "bar"),
+					asList(server.getRunListeners(run.name)));
+			assertEquals(0,
+					server.getRunListenerProperties(run.name, "bar").length);
+		} finally {
+			try {
+				server.destroyRun(run.name);
+			} catch (Exception e) {
+				// Ignore
+			}
+		}
+	}
+}
diff --git a/server-webapp/src/test/java/org/taverna/server/master/WorkflowSerializationTest.java b/server-webapp/src/test/java/org/taverna/server/master/WorkflowSerializationTest.java
new file mode 100644
index 0000000..0450317
--- /dev/null
+++ b/server-webapp/src/test/java/org/taverna/server/master/WorkflowSerializationTest.java
@@ -0,0 +1,68 @@
+package org.taverna.server.master;
+
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW_NS;
+import static org.taverna.server.master.rest.handler.T2FlowDocumentHandler.T2FLOW_ROOTNAME;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.taverna.server.master.common.Workflow;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+public class WorkflowSerializationTest {
+	@Test
+	public void testWorkflowSerialization()
+			throws ParserConfigurationException, IOException,
+			ClassNotFoundException {
+		DocumentBuilder db = DocumentBuilderFactory.newInstance()
+				.newDocumentBuilder();
+		Document doc = db.getDOMImplementation().createDocument(null, null,
+				null);
+		Element workflow = doc.createElementNS(T2FLOW_NS, T2FLOW_ROOTNAME);
+		Element foo = doc.createElementNS("urn:foo:bar", "pqr:foo");
+		foo.setTextContent("bar");
+		foo.setAttribute("xyz", "abc");
+		workflow.appendChild(foo);
+		Workflow w = new Workflow(workflow);
+
+		ByteArrayOutputStream baos = new ByteArrayOutputStream();
+		try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+			oos.writeObject(w);
+		}
+
+		Object o;
+		try (ObjectInputStream ois = new ObjectInputStream(
+				new ByteArrayInputStream(baos.toByteArray()))) {
+			o = ois.readObject();
+		}
+
+		Assert.assertNotNull(o);
+		Assert.assertEquals(w.getClass(), o.getClass());
+		Workflow w2 = (Workflow) o;
+		Assert.assertNotNull(w2.getT2flowWorkflow());
+		Element e = w2.getT2flowWorkflow();
+		Assert.assertEquals(T2FLOW_ROOTNAME, e.getLocalName());
+		Assert.assertEquals(T2FLOW_NS, e.getNamespaceURI());
+		e = (Element) e.getFirstChild();
+		Assert.assertEquals("foo", e.getLocalName());
+		Assert.assertEquals("pqr", e.getPrefix());
+		Assert.assertEquals("urn:foo:bar", e.getNamespaceURI());
+		Assert.assertEquals("bar", e.getTextContent());
+		Assert.assertEquals(1, e.getChildNodes().getLength());
+		// WARNING: These are dependent on how namespaces are encoded!
+		Assert.assertEquals(2, e.getAttributes().getLength());
+		Assert.assertEquals("xyz", ((Attr) e.getAttributes().item(1)).getLocalName());
+		Assert.assertEquals("abc", e.getAttribute("xyz"));
+	}
+}
diff --git a/server-webapp/src/test/java/org/taverna/server/master/mocks/ExampleRun.java b/server-webapp/src/test/java/org/taverna/server/master/mocks/ExampleRun.java
new file mode 100644
index 0000000..a2a0791
--- /dev/null
+++ b/server-webapp/src/test/java/org/taverna/server/master/mocks/ExampleRun.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.master.mocks;
+
+import static java.util.Calendar.MINUTE;
+import static java.util.Collections.unmodifiableList;
+import static java.util.UUID.randomUUID;
+import static org.taverna.server.master.common.Status.Initialized;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.xml.ws.handler.MessageContext;
+
+import org.springframework.security.core.context.SecurityContext;
+import org.taverna.server.master.common.Credential;
+import org.taverna.server.master.common.Status;
+import org.taverna.server.master.common.Trust;
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.BadStateChangeException;
+import org.taverna.server.master.exceptions.FilesystemAccessException;
+import org.taverna.server.master.exceptions.InvalidCredentialException;
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.factories.RunFactory;
+import org.taverna.server.master.interfaces.Directory;
+import org.taverna.server.master.interfaces.Input;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.SecurityContextFactory;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.interfaces.TavernaSecurityContext;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+@SuppressWarnings("serial")
+public class ExampleRun implements TavernaRun, TavernaSecurityContext {
+	String id;
+	List<Listener> listeners;
+	Workflow workflow;
+	Status status;
+	Date expiry;
+	UsernamePrincipal owner;
+	String inputBaclava;
+	String outputBaclava;
+	java.io.File realRoot;
+	List<Input> inputs;
+	String name;
+
+	public ExampleRun(UsernamePrincipal creator, Workflow workflow, Date expiry) {
+		this.id = randomUUID().toString();
+		this.listeners = new ArrayList<>();
+		this.status = Initialized;
+		this.owner = creator;
+		this.workflow = workflow;
+		this.expiry = expiry;
+		this.inputs = new ArrayList<>();
+		listeners.add(new DefaultListener());
+	}
+
+	@Override
+	public void addListener(Listener l) {
+		listeners.add(l);
+	}
+
+	@Override
+	public void destroy() {
+		// This does nothing...
+	}
+
+	@Override
+	public Date getExpiry() {
+		return expiry;
+	}
+
+	@Override
+	public List<Listener> getListeners() {
+		return listeners;
+	}
+
+	@Override
+	public TavernaSecurityContext getSecurityContext() {
+		return this;
+	}
+
+	@Override
+	public Status getStatus() {
+		return status;
+	}
+
+	@Override
+	public Workflow getWorkflow() {
+		return workflow;
+	}
+
+	@Override
+	public Directory getWorkingDirectory() {
+		// LATER: Implement this!
+		throw new UnsupportedOperationException("not yet implemented");
+	}
+
+	@Override
+	public void setExpiry(Date d) {
+		if (d.after(new Date()))
+			this.expiry = d;
+	}
+
+	@Override
+	public String setStatus(Status s) {
+		this.status = s;
+		return null;
+	}
+
+	@Override
+	public UsernamePrincipal getOwner() {
+		return owner;
+	}
+
+	public static class Builder implements RunFactory {
+		private int lifetime;
+
+		public Builder(int initialLifetimeMinutes) {
+			this.lifetime = initialLifetimeMinutes;
+		}
+
+		@Override
+		public TavernaRun create(UsernamePrincipal creator, Workflow workflow) {
+			Calendar c = GregorianCalendar.getInstance();
+			c.add(MINUTE, lifetime);
+			return new ExampleRun(creator, workflow, c.getTime());
+		}
+
+		@Override
+		public boolean isAllowingRunsToStart() {
+			return true;
+		}
+	}
+
+	static final String[] emptyArray = new String[0];
+
+	class DefaultListener implements Listener {
+		@Override
+		public String getConfiguration() {
+			return "";
+		}
+
+		@Override
+		public String getName() {
+			return "default";
+		}
+
+		@Override
+		public String getType() {
+			return "default";
+		}
+
+		@Override
+		public String[] listProperties() {
+			return emptyArray;
+		}
+
+		@Override
+		public String getProperty(String propName) throws NoListenerException {
+			throw new NoListenerException("no such property");
+		}
+
+		@Override
+		public void setProperty(String propName, String value)
+				throws NoListenerException {
+			throw new NoListenerException("no such property");
+		}
+	}
+
+	@Override
+	public String getInputBaclavaFile() {
+		return inputBaclava;
+	}
+
+	@Override
+	public List<Input> getInputs() {
+		return unmodifiableList(inputs);
+	}
+
+	@Override
+	public String getOutputBaclavaFile() {
+		return outputBaclava;
+	}
+
+	class ExampleInput implements Input {
+		public String name;
+		public String file;
+		public String value;
+		public String delim;
+
+		public ExampleInput(String name) {
+			this.name = name;
+		}
+
+		@Override
+		public String getFile() {
+			return file;
+		}
+
+		@Override
+		public String getName() {
+			return name;
+		}
+
+		@Override
+		public String getValue() {
+			return value;
+		}
+
+		@Override
+		public void setFile(String file) throws FilesystemAccessException,
+				BadStateChangeException {
+			if (status != Status.Initialized)
+				throw new BadStateChangeException();
+			checkBadFilename(file);
+			this.file = file;
+			this.value = null;
+			inputBaclava = null;
+		}
+
+		@Override
+		public void setValue(String value) throws BadStateChangeException {
+			if (status != Status.Initialized)
+				throw new BadStateChangeException();
+			this.value = value;
+			this.file = null;
+			inputBaclava = null;
+		}
+
+		void reset() {
+			this.file = null;
+			this.value = null;
+		}
+
+		@Override
+		public String getDelimiter() {
+			return delim;
+		}
+
+		@Override
+		public void setDelimiter(String delimiter)
+				throws BadStateChangeException {
+			if (status != Status.Initialized)
+				throw new BadStateChangeException();
+			if (delimiter == null)
+				delim = null;
+			else
+				delim = delimiter.substring(0, 1);
+		}
+	}
+
+	@Override
+	public Input makeInput(String name) throws BadStateChangeException {
+		if (status != Status.Initialized)
+			throw new BadStateChangeException();
+		Input i = new ExampleInput(name);
+		inputs.add(i);
+		return i;
+	}
+
+	static void checkBadFilename(String filename)
+			throws FilesystemAccessException {
+		if (filename.startsWith("/"))
+			throw new FilesystemAccessException("filename may not be absolute");
+		if (Arrays.asList(filename.split("/")).contains(".."))
+			throw new FilesystemAccessException(
+					"filename may not refer to parent");
+	}
+
+	@Override
+	public void setInputBaclavaFile(String filename)
+			throws FilesystemAccessException, BadStateChangeException {
+		if (status != Status.Initialized)
+			throw new BadStateChangeException();
+		checkBadFilename(filename);
+		inputBaclava = filename;
+		for (Input i : inputs)
+			((ExampleInput) i).reset();
+	}
+
+	@Override
+	public void setOutputBaclavaFile(String filename)
+			throws FilesystemAccessException, BadStateChangeException {
+		if (status != Status.Initialized)
+			throw new BadStateChangeException();
+		if (filename != null)
+			checkBadFilename(filename);
+		outputBaclava = filename;
+	}
+
+	private Date created = new Date();
+	@Override
+	public Date getCreationTimestamp() {
+		return created;
+	}
+
+	@Override
+	public Date getFinishTimestamp() {
+		return null;
+	}
+
+	@Override
+	public Date getStartTimestamp() {
+		return null;
+	}
+
+	@Override
+	public Credential[] getCredentials() {
+		return new Credential[0];
+	}
+
+	@Override
+	public void addCredential(Credential toAdd) {
+	}
+
+	@Override
+	public void deleteCredential(Credential toDelete) {
+	}
+
+	@Override
+	public Trust[] getTrusted() {
+		return new Trust[0];
+	}
+
+	@Override
+	public void addTrusted(Trust toAdd) {
+	}
+
+	@Override
+	public void deleteTrusted(Trust toDelete) {
+	}
+
+	@Override
+	public void validateCredential(Credential c)
+			throws InvalidCredentialException {
+	}
+
+	@Override
+	public void validateTrusted(Trust t) throws InvalidCredentialException {
+	}
+
+	@Override
+	public void initializeSecurityFromSOAPContext(MessageContext context) {
+		// Do nothing
+	}
+
+	@Override
+	public void initializeSecurityFromRESTContext(HttpHeaders headers) {
+		// Do nothing
+	}
+
+	@Override
+	public void conveySecurity() throws GeneralSecurityException, IOException {
+		// Do nothing
+	}
+
+	@Override
+	public SecurityContextFactory getFactory() {
+		return null;
+	}
+
+	private Set<String> destroyers = new HashSet<String>();
+	private Set<String> updaters = new HashSet<String>();
+	private Set<String> readers = new HashSet<String>();
+	@Override
+	public Set<String> getPermittedDestroyers() {
+		return destroyers;
+	}
+
+	@Override
+	public void setPermittedDestroyers(Set<String> destroyers) {
+		this.destroyers = destroyers;
+		updaters.addAll(destroyers);
+		readers.addAll(destroyers);
+	}
+
+	@Override
+	public Set<String> getPermittedUpdaters() {
+		return updaters;
+	}
+
+	@Override
+	public void setPermittedUpdaters(Set<String> updaters) {
+		this.updaters = updaters;
+		this.updaters.addAll(destroyers);
+		readers.addAll(updaters);
+	}
+
+	@Override
+	public Set<String> getPermittedReaders() {
+		return readers;
+	}
+
+	@Override
+	public void setPermittedReaders(Set<String> readers) {
+		this.readers = readers;
+		this.readers.addAll(destroyers);
+		this.readers.addAll(updaters);
+	}
+
+	@Override
+	public String getId() {
+		return id;
+	}
+
+	@Override
+	public void initializeSecurityFromContext(SecurityContext securityContext)
+			throws Exception {
+		// Do nothing
+	}
+
+	@Override
+	public String getName() {
+		return name;
+	}
+
+	@Override
+	public void setName(String name) {
+		this.name = (name.length() > 5 ? name.substring(0, 5) : name);
+	}
+
+	@Override
+	public void ping() throws UnknownRunException {
+		// Do nothing
+	}
+
+	@Override
+	public boolean getGenerateProvenance() {
+		// TODO Auto-generated method stub
+		return false;
+	}
+
+	@Override
+	public void setGenerateProvenance(boolean generateProvenance) {
+		// TODO Auto-generated method stub
+		
+	}
+}
diff --git a/server-webapp/src/test/java/org/taverna/server/master/mocks/MockPolicy.java b/server-webapp/src/test/java/org/taverna/server/master/mocks/MockPolicy.java
new file mode 100644
index 0000000..81dd08c
--- /dev/null
+++ b/server-webapp/src/test/java/org/taverna/server/master/mocks/MockPolicy.java
@@ -0,0 +1,59 @@
+package org.taverna.server.master.mocks;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+public class MockPolicy extends SimpleServerPolicy {
+	public MockPolicy() {
+		super();
+		super.setCleanerInterval(30);
+	}
+
+	public int maxruns = 10;
+	Integer usermaxruns;
+	Set<TavernaRun> denyaccess = new HashSet<>();
+	boolean exnOnUpdate, exnOnCreate, exnOnDelete;
+
+	@Override
+	public int getMaxRuns() {
+		return maxruns;
+	}
+
+	@Override
+	public Integer getMaxRuns(UsernamePrincipal user) {
+		return usermaxruns;
+	}
+
+	@Override
+	public boolean permitAccess(UsernamePrincipal user, TavernaRun run) {
+		return !denyaccess.contains(run);
+	}
+
+	@Override
+	public void permitCreate(UsernamePrincipal user, Workflow workflow)
+			throws NoCreateException {
+		if (this.exnOnCreate)
+			throw new NoCreateException();
+	}
+
+	@Override
+	public void permitDestroy(UsernamePrincipal user, TavernaRun run)
+			throws NoDestroyException {
+		if (this.exnOnDelete)
+			throw new NoDestroyException();
+	}
+
+	@Override
+	public void permitUpdate(UsernamePrincipal user, TavernaRun run)
+			throws NoUpdateException {
+		if (this.exnOnUpdate)
+			throw new NoUpdateException();
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleListenerFactory.java b/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleListenerFactory.java
new file mode 100644
index 0000000..d864214
--- /dev/null
+++ b/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleListenerFactory.java
@@ -0,0 +1,64 @@
+package org.taverna.server.master.mocks;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.taverna.server.master.exceptions.NoListenerException;
+import org.taverna.server.master.factories.ListenerFactory;
+import org.taverna.server.master.interfaces.Listener;
+import org.taverna.server.master.interfaces.TavernaRun;
+
+/**
+ * A factory for event listener. The factory is configured using Spring.
+ * 
+ * @author Donal Fellows
+ */
+public class SimpleListenerFactory implements ListenerFactory {
+	private Map<String, Builder> builders = new HashMap<>();
+
+	public void setBuilders(Map<String, Builder> builders) {
+		this.builders = builders;
+	}
+
+	@Override
+	public List<String> getSupportedListenerTypes() {
+		return new ArrayList<>(builders.keySet());
+	}
+
+	@Override
+	public Listener makeListener(TavernaRun run, String listenerType,
+			String configuration) throws NoListenerException {
+		Builder b = builders.get(listenerType);
+		if (b == null)
+			throw new NoListenerException("no such listener type");
+		Listener l = b.build(run, configuration);
+		run.addListener(l);
+		return l;
+	}
+
+	/**
+	 * How to actually construct a listener.
+	 * 
+	 * @author Donal Fellows
+	 */
+	public interface Builder {
+		/**
+		 * Make an event listener attached to a run.
+		 * 
+		 * @param run
+		 *            The run to attach to.
+		 * @param configuration
+		 *            A user-specified configuration document. The constructed
+		 *            listener <i>should</i> process this configuration document
+		 *            and be able to return it to the user when requested.
+		 * @return The listener object.
+		 * @throws NoListenerException
+		 *             If the listener construction failed or the
+		 *             <b>configuration</b> document was bad in some way.
+		 */
+		public Listener build(TavernaRun run, String configuration)
+				throws NoListenerException;
+	}
+}
diff --git a/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleNonpersistentRunStore.java b/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleNonpersistentRunStore.java
new file mode 100644
index 0000000..a3751e4
--- /dev/null
+++ b/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleNonpersistentRunStore.java
@@ -0,0 +1,151 @@
+package org.taverna.server.master.mocks;
+
+import java.lang.ref.WeakReference;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.UnknownRunException;
+import org.taverna.server.master.interfaces.Policy;
+import org.taverna.server.master.interfaces.RunStore;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * Example of a store for Taverna Workflow Runs.
+ * 
+ * @author Donal Fellows
+ */
+public class SimpleNonpersistentRunStore implements RunStore {
+	private Map<String, TavernaRun> store = new HashMap<>();
+	private Object lock = new Object();
+
+	Timer timer;
+	private CleanerTask cleaner;
+
+	/**
+	 * The connection to the main policy store. Suitable for wiring up with
+	 * Spring.
+	 * 
+	 * @param p
+	 *            The policy to connect to.
+	 */
+	public void setPolicy(SimpleServerPolicy p) {
+		p.store = this;
+		cleanerIntervalUpdated(p.getCleanerInterval());
+	}
+
+	public SimpleNonpersistentRunStore() {
+		timer = new Timer("SimpleNonpersistentRunStore.CleanerTimer", true);
+		cleanerIntervalUpdated(300);
+	}
+
+	@Override
+	protected void finalize() {
+		timer.cancel();
+	}
+
+	/**
+	 * Remove and destroy all runs that are expired at the moment that this
+	 * method starts.
+	 */
+	void clean() {
+		Date now = new Date();
+		synchronized (lock) {
+			// Use an iterator so we have access to its remove() method...
+			Iterator<TavernaRun> i = store.values().iterator();
+			while (i.hasNext()) {
+				TavernaRun w = i.next();
+				if (w.getExpiry().before(now)) {
+					i.remove();
+					try {
+						w.destroy();
+					} catch (NoDestroyException e) {
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Reconfigure the cleaner task's call interval. This is called internally
+	 * and from the Policy when the interval is set there.
+	 * 
+	 * @param intervalInSeconds
+	 *            How long between runs of the cleaner task, in seconds.
+	 */
+	void cleanerIntervalUpdated(int intervalInSeconds) {
+		if (cleaner != null)
+			cleaner.cancel();
+		cleaner = new CleanerTask(this, intervalInSeconds);
+	}
+
+	@Override
+	public TavernaRun getRun(UsernamePrincipal user, Policy p, String uuid)
+			throws UnknownRunException {
+		synchronized (lock) {
+			TavernaRun w = store.get(uuid);
+			if (w == null || !p.permitAccess(user, w))
+				throw new UnknownRunException();
+			return w;
+		}
+	}
+
+	@Override
+	public TavernaRun getRun(String uuid) throws UnknownRunException {
+		synchronized (lock) {
+			TavernaRun w = store.get(uuid);
+			if (w == null)
+				throw new UnknownRunException();
+			return w;
+		}
+	}
+
+	@Override
+	public Map<String, TavernaRun> listRuns(UsernamePrincipal user, Policy p) {
+		Map<String, TavernaRun> filtered = new HashMap<>();
+		synchronized (lock) {
+			for (Map.Entry<String, TavernaRun> entry : store.entrySet())
+				if (p.permitAccess(user, entry.getValue()))
+					filtered.put(entry.getKey(), entry.getValue());
+		}
+		return filtered;
+	}
+
+	@Override
+	public String registerRun(TavernaRun run) {
+		synchronized (lock) {
+			store.put(run.getId(), run);
+			return run.getId();
+		}
+	}
+
+	@Override
+	public void unregisterRun(String uuid) {
+		synchronized (lock) {
+			store.remove(uuid);
+		}
+	}
+}
+
+class CleanerTask extends TimerTask {
+	WeakReference<SimpleNonpersistentRunStore> store;
+
+	CleanerTask(SimpleNonpersistentRunStore store, int interval) {
+		this.store = new WeakReference<>(store);
+		int tms = interval * 1000;
+		store.timer.scheduleAtFixedRate(this, tms, tms);
+	}
+
+	@Override
+	public void run() {
+		SimpleNonpersistentRunStore s = store.get();
+		if (s != null) {
+			s.clean();
+		}
+	}
+}
\ No newline at end of file
diff --git a/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleServerPolicy.java b/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleServerPolicy.java
new file mode 100644
index 0000000..1a68d2c
--- /dev/null
+++ b/server-webapp/src/test/java/org/taverna/server/master/mocks/SimpleServerPolicy.java
@@ -0,0 +1,110 @@
+package org.taverna.server.master.mocks;
+
+import java.net.URI;
+import java.util.List;
+
+import org.taverna.server.master.common.Workflow;
+import org.taverna.server.master.exceptions.NoCreateException;
+import org.taverna.server.master.exceptions.NoDestroyException;
+import org.taverna.server.master.exceptions.NoUpdateException;
+import org.taverna.server.master.interfaces.Policy;
+import org.taverna.server.master.interfaces.TavernaRun;
+import org.taverna.server.master.utils.UsernamePrincipal;
+
+/**
+ * A very simple (and unsafe) security model. The number of runs is configurable
+ * through Spring (or 10 if unconfigured) with no per-user limits supported, all
+ * workflows are permitted, and all identified users may create a workflow run.
+ * Any user may read off information about any run, but only its owner may
+ * modify or destroy it.
+ * <p>
+ * Note that this is a <i>Policy Enforcement Point</i> for access control to
+ * individual workflows.
+ * 
+ * @author Donal Fellows
+ */
+public class SimpleServerPolicy implements Policy {
+	private int maxRuns = 10;
+	private int cleanerInterval;
+	SimpleNonpersistentRunStore store;
+
+	public void setMaxRuns(int maxRuns) {
+		this.maxRuns = maxRuns;
+	}
+
+	@Override
+	public int getMaxRuns() {
+		return maxRuns;
+	}
+
+	@Override
+	public Integer getMaxRuns(UsernamePrincipal p) {
+		return null; // No per-user limits
+	}
+
+	public int getCleanerInterval() {
+		return cleanerInterval;
+	}
+
+	/**
+	 * Sets how often the store of workflow runs will try to clean out expired
+	 * runs.
+	 * 
+	 * @param intervalInSeconds
+	 */
+	public void setCleanerInterval(int intervalInSeconds) {
+		cleanerInterval = intervalInSeconds;
+		if (store != null)
+			store.cleanerIntervalUpdated(intervalInSeconds);
+	}
+
+	@Override
+	public boolean permitAccess(UsernamePrincipal p, TavernaRun run) {
+		// No secrets here!
+		return true;
+	}
+
+	@Override
+	public void permitCreate(UsernamePrincipal p, Workflow workflow)
+			throws NoCreateException {
+		// Only identified users may create
+		if (p == null)
+			throw new NoCreateException();
+		// Global run count limit enforcement
+		if (store.listRuns(p, this).size() >= maxRuns)
+			throw new NoCreateException();
+		// Per-user run count enforcement would come here
+	}
+
+	@Override
+	public void permitDestroy(UsernamePrincipal p, TavernaRun run)
+			throws NoDestroyException {
+		// Only the creator may destroy
+		if (p == null || !p.equals(run.getSecurityContext().getOwner()))
+			throw new NoDestroyException();
+	}
+
+	@Override
+	public void permitUpdate(UsernamePrincipal p, TavernaRun run)
+			throws NoUpdateException {
+		// Only the creator may change
+		if (p == null || !p.equals(run.getSecurityContext().getOwner()))
+			throw new NoUpdateException();
+	}
+
+	@Override
+	public int getOperatingLimit() {
+		return 1;
+	}
+
+	@Override
+	public List<URI> listPermittedWorkflowURIs(UsernamePrincipal user) {
+		return null;
+	}
+
+	@Override
+	public void setPermittedWorkflowURIs(UsernamePrincipal user,
+			List<URI> permitted) {
+		// Ignore
+	}
+}
diff --git a/server-webapp/src/test/resources/example.xml b/server-webapp/src/test/resources/example.xml
new file mode 100644
index 0000000..14cc242
--- /dev/null
+++ b/server-webapp/src/test/resources/example.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
+
+	<bean id="policy" class="org.taverna.server.master.mocks.SimpleServerPolicy"
+		lazy-init="false" scope="singleton">
+		<property name="maxRuns" value="1">
+			<description>
+				Limit on total number of simultaneous runs.
+			</description>
+		</property>
+		<property name="cleanerInterval" value="300">
+			<description>
+				Time between trying to delete expired runs, in seconds.
+			</description>
+		</property>
+	</bean>
+
+	<bean id="runFactory" class="org.taverna.server.master.mocks.ExampleRun$Builder">
+		<constructor-arg type="int" value="10" /> <!-- "initialLifetimeMinutes" -->
+	</bean>
+
+	<bean id="runCatalog" scope="singleton"
+		class="org.taverna.server.master.mocks.SimpleNonpersistentRunStore">
+		<property name="policy" ref="policy" />
+	</bean>
+
+	<bean id="listenerFactory" class="org.taverna.server.master.mocks.SimpleListenerFactory">
+		<property name="builders">
+			<description>
+				This map describes how to build each type of supported
+				event listener that is not installed by default. Any site policy for
+				a listeners should be installed using its properties, as shown. The
+				"key" is the type, the "class" is the builder for actual instances
+				(which must be an instance of
+				org.taverna.server.master.factories.SimpleListenerFactory.Builder)
+				and any policies and installation-specific configurations are
+				characterised by properties such as "sitePolicy" below.
+			</description>
+			<map>
+				<!--						<entry key="exampleListener">-->
+				<!--
+					<bean
+					class="org.taverna.server.master.example.ExampleListener$Builder">
+				-->
+				<!--								<property name="sitePolicy">-->
+				<!--									<value>Just an example!</value>-->
+				<!--								</property>-->
+				<!--							</bean>-->
+				<!--						</entry>-->
+			</map>
+		</property>
+	</bean>
+</beans>
diff --git a/server-webapp/src/test/resources/log4j.properties b/server-webapp/src/test/resources/log4j.properties
new file mode 100644
index 0000000..6707f55
--- /dev/null
+++ b/server-webapp/src/test/resources/log4j.properties
@@ -0,0 +1,4 @@
+log4j.rootLogger=info, R 
+log4j.appender.R=org.apache.log4j.ConsoleAppender
+log4j.appender.R.layout=org.apache.log4j.PatternLayout
+log4j.appender.R.layout.ConversionPattern=%d{yyyyMMdd'T'HHmmss.SSS} %-5p %c{1} %C{1} - %m%n
\ No newline at end of file
diff --git a/server-worker/pom.xml b/server-worker/pom.xml
new file mode 100644
index 0000000..70239a2
--- /dev/null
+++ b/server-worker/pom.xml
@@ -0,0 +1,126 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<artifactId>server-worker</artifactId>
+	<name>Workflow Executor/File System Access Process Implementation</name>
+	<description>This is the implementation of the factory process that is started up by the web application to manage executing Taverna Workflow executeworkflow.sh calls. Also provides per-user access to filestore.</description>
+	<parent>
+		<groupId>uk.org.taverna.server</groupId>
+		<artifactId>server</artifactId>
+		<version>3.0-SNAPSHOT</version>
+		<relativePath>..</relativePath>
+	</parent>
+	<scm>
+		<url>${scmBrowseRoot}/server-worker</url>
+	</scm>
+
+	<properties>
+		<workerMainClass>org.taverna.server.localworker.impl.TavernaRunManager</workerMainClass>
+		<scufl2.version>0.9.2</scufl2.version>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-runinterface</artifactId>
+			<version>${project.parent.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>commons-collections</groupId>
+			<artifactId>commons-collections</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.server</groupId>
+			<artifactId>server-usagerecord</artifactId>
+			<version>${project.parent.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.scufl2</groupId>
+			<artifactId>scufl2-api</artifactId>
+			<version>${scufl2.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.scufl2</groupId>
+			<artifactId>scufl2-t2flow</artifactId>
+			<version>${scufl2.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>uk.org.taverna.scufl2</groupId>
+			<artifactId>scufl2-rdfxml</artifactId>
+			<version>${scufl2.version}</version>
+			<scope>runtime</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<encoding>US-ASCII</encoding>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<artifactId>maven-assembly-plugin</artifactId>
+				<configuration>
+					<descriptorRefs>
+						<descriptorRef>jar-with-dependencies</descriptorRef>
+					</descriptorRefs>
+					<archive>
+						<manifest>
+							<mainClass>${workerMainClass}</mainClass>
+						</manifest>
+					</archive>
+				</configuration>
+				<executions>
+					<execution>
+						<id>make-assembly</id>
+						<phase>package</phase>
+						<goals>
+							<goal>single</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+		<pluginManagement>
+			<plugins>
+				<plugin>
+					<groupId>org.eclipse.m2e</groupId>
+					<artifactId>lifecycle-mapping</artifactId>
+					<version>1.0.0</version>
+					<configuration>
+						<lifecycleMappingMetadata>
+							<pluginExecutions>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>org.apache.maven.plugins</groupId>
+										<artifactId>maven-assembly-plugin</artifactId>
+                                    	<versionRange>[2.0,)</versionRange>
+                                    	<goals>
+                                    		<goal>single</goal>
+                                    	</goals>
+									</pluginExecutionFilter>
+									<action>
+										<execute />
+									</action>
+								</pluginExecution>
+							</pluginExecutions>
+						</lifecycleMappingMetadata>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
+	</build>
+</project>
diff --git a/server-worker/src/main/java/META-INF/MANIFEST.MF b/server-worker/src/main/java/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..5e94951
--- /dev/null
+++ b/server-worker/src/main/java/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0

+Class-Path: 

+

diff --git a/server-worker/src/main/java/org/taverna/server/localworker/api/Constants.java b/server-worker/src/main/java/org/taverna/server/localworker/api/Constants.java
new file mode 100644
index 0000000..600913a
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/api/Constants.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.api;
+
+import static java.nio.charset.Charset.defaultCharset;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * The defaults associated with this worker, together with various other
+ * constants.
+ * 
+ * @author Donal Fellows
+ */
+public abstract class Constants {
+	/**
+	 * Subdirectories of the working directory to create by default.
+	 */
+	public static final String[] SUBDIR_LIST = { "conf", "externaltool", "feed",
+			"interactions", "lib", "logs", "plugins", "repository", "var" };
+
+	/** The name of the default encoding for characters on this machine. */
+	public static final String SYSTEM_ENCODING = defaultCharset().name();
+
+	/**
+	 * Password to use to encrypt security information. This default is <7 chars
+	 * to work even without Unlimited Strength JCE.
+	 */
+	public static final char[] KEYSTORE_PASSWORD = { 'c', 'h', 'a', 'n', 'g', 'e' };
+
+	/**
+	 * The name of the directory (in the home directory) where security settings
+	 * will be written.
+	 */
+	public static final String SECURITY_DIR_NAME = ".taverna-server-security";
+
+	/** The name of the file that will be the created keystore. */
+	public static final String KEYSTORE_FILE = "t2keystore.ubr";
+
+	/** The name of the file that will be the created truststore. */
+	public static final String TRUSTSTORE_FILE = "t2truststore.ubr";
+
+	/**
+	 * The name of the file that contains the password to unlock the keystore
+	 * and truststore.
+	 */
+	public static final String PASSWORD_FILE = "password.txt";
+
+	// --------- UNUSED ---------
+	// /**
+	// * The name of the file that contains the mapping from URIs to keystore
+	// * aliases.
+	// */
+	// public static final String URI_ALIAS_MAP = "urlmap.txt";
+
+	/**
+	 * Used to instruct the Taverna credential manager to use a non-default
+	 * location for user credentials.
+	 */
+	public static final String CREDENTIAL_MANAGER_DIRECTORY = "-cmdir";
+
+	/**
+	 * Used to instruct the Taverna credential manager to take its master
+	 * password from standard input.
+	 */
+	public static final String CREDENTIAL_MANAGER_PASSWORD = "-cmpassword";
+
+	/**
+	 * Name of environment variable used to pass HELIO security tokens to
+	 * workflows.
+	 */
+	// This technique is known to be insecure; bite me.
+	public static final String HELIO_TOKEN_NAME = "HELIO_CIS_TOKEN";
+
+	/**
+	 * The name of the standard listener, which is installed by default.
+	 */
+	public static final String DEFAULT_LISTENER_NAME = "io";
+
+	/**
+	 * Time to wait for the subprocess to wait, in milliseconds.
+	 */
+	public static final int START_WAIT_TIME = 1500;
+
+	/**
+	 * Time to wait for success or failure of a death-causing activity (i.e.,
+	 * sending a signal).
+	 */
+	public static final int DEATH_TIME = 333;
+
+	/**
+	 * The name of the file (in this code's resources) that provides the default
+	 * security policy that we use.
+	 */
+	public static final String SECURITY_POLICY_FILE = "security.policy";
+
+	/**
+	 * The Java property holding security policy info.
+	 */
+	public static final String SEC_POLICY_PROP = "java.security.policy";
+	/**
+	 * The Java property to set to make this code not try to enforce security
+	 * policy.
+	 */
+	public static final String UNSECURE_PROP = "taverna.suppressrestrictions.rmi";
+	/**
+	 * The Java property that holds the name of the host name to enforce.
+	 */
+	public static final String RMI_HOST_PROP = "java.rmi.server.hostname";
+	/**
+	 * The default hostname to require in secure mode. This is the
+	 * <i>resolved</i> version of "localhost".
+	 */
+	public static final String LOCALHOST;
+	static {
+		String h = "127.0.0.1"; // fallback
+		try {
+			h = InetAddress.getByName("localhost").getHostAddress();
+		} catch (UnknownHostException e) {
+			e.printStackTrace();
+		} finally {
+			LOCALHOST = h;
+		}
+	}
+
+	/**
+	 * Time to wait during closing down this process. In milliseconds.
+	 */
+	public static final int DEATH_DELAY = 500;
+	/**
+	 * The name of the property describing where shared directories should be
+	 * located.
+	 */
+	public static final String SHARED_DIR_PROP = "taverna.sharedDirectory";
+
+	public static final String TIME = "/usr/bin/time";
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/api/RunAccounting.java b/server-worker/src/main/java/org/taverna/server/localworker/api/RunAccounting.java
new file mode 100644
index 0000000..3896c06
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/api/RunAccounting.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2013 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.api;
+
+/**
+ * 
+ * @author Donal Fellows
+ */
+public interface RunAccounting {
+	/**
+	 * Logs that a run has started executing.
+	 */
+	void runStarted();
+
+	/**
+	 * Logs that a run has finished executing.
+	 */
+	void runCeased();
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/api/Worker.java b/server-worker/src/main/java/org/taverna/server/localworker/api/Worker.java
new file mode 100644
index 0000000..c513ed8
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/api/Worker.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.api;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+import org.taverna.server.localworker.impl.LocalWorker;
+import org.taverna.server.localworker.remote.ImplementationException;
+import org.taverna.server.localworker.remote.RemoteListener;
+import org.taverna.server.localworker.remote.RemoteStatus;
+import org.taverna.server.localworker.server.UsageRecordReceiver;
+
+/**
+ * The interface between the connectivity layer and the thunk to the
+ * subprocesses.
+ * 
+ * @author Donal Fellows
+ */
+public interface Worker {
+	/**
+	 * Fire up the workflow. This causes a transition into the operating state.
+	 * 
+	 * @param local
+	 *            The reference to the factory class for this worker.
+	 * @param executeWorkflowCommand
+	 *            The command to run to execute the workflow.
+	 * @param workflow
+	 *            The workflow document to execute.
+	 * @param workingDir
+	 *            What directory to use as the working directory.
+	 * @param inputBaclavaFile
+	 *            The baclava file to use for inputs, or <tt>null</tt> to use
+	 *            the other <b>input*</b> arguments' values.
+	 * @param inputRealFiles
+	 *            A mapping of input names to files that supply them. Note that
+	 *            we assume that nothing mapped here will be mapped in
+	 *            <b>inputValues</b>.
+	 * @param inputValues
+	 *            A mapping of input names to values to supply to them. Note
+	 *            that we assume that nothing mapped here will be mapped in
+	 *            <b>inputFiles</b>.
+	 * @param inputDelimiters
+	 *            A mapping of input names to characters used to split them into
+	 *            lists.
+	 * @param outputBaclavaFile
+	 *            What baclava file to write the output from the workflow into,
+	 *            or <tt>null</tt> to have it written into the <tt>out</tt>
+	 *            subdirectory.
+	 * @param contextDirectory
+	 *            The directory containing the keystore and truststore. <i>Must
+	 *            not be <tt>null</tt>.</i>
+	 * @param keystorePassword
+	 *            The password to the keystore and truststore. <i>Must not be
+	 *            <tt>null</tt>.</i>
+	 * @param generateProvenance
+	 *            Whether to generate a run bundle containing provenance data.
+	 * @param environment
+	 *            Any environment variables that need to be added to the
+	 *            invokation.
+	 * @param masterToken
+	 *            The internal name of the workflow run.
+	 * @param runtimeSettings
+	 *            List of configuration details for the forked runtime.
+	 * @return Whether a successful start happened.
+	 * @throws Exception
+	 *             If any of quite a large number of things goes wrong.
+	 */
+	boolean initWorker(LocalWorker local, String executeWorkflowCommand,
+			byte[] workflow, File workingDir, File inputBaclavaFile,
+			Map<String, File> inputRealFiles, Map<String, String> inputValues,
+			Map<String, String> inputDelimiters, File outputBaclavaFile,
+			File contextDirectory, char[] keystorePassword,
+			boolean generateProvenance, Map<String, String> environment,
+			String masterToken, List<String> runtimeSettings) throws Exception;
+
+	/**
+	 * Kills off the subprocess if it exists and is alive.
+	 * 
+	 * @throws Exception
+	 *             if anything goes badly wrong when the worker is being killed
+	 *             off.
+	 */
+	void killWorker() throws Exception;
+
+	/**
+	 * Move the worker out of the stopped state and back to operating.
+	 * 
+	 * @throws Exception
+	 *             if it fails (which it always does; operation currently
+	 *             unsupported).
+	 */
+	void startWorker() throws Exception;
+
+	/**
+	 * Move the worker into the stopped state from the operating state.
+	 * 
+	 * @throws Exception
+	 *             if it fails (which it always does; operation currently
+	 *             unsupported).
+	 */
+	void stopWorker() throws Exception;
+
+	/**
+	 * @return The status of the workflow run. Note that this can be an
+	 *         expensive operation.
+	 */
+	RemoteStatus getWorkerStatus();
+
+	/**
+	 * @return The listener that is registered by default, in addition to all
+	 *         those that are explicitly registered by the user.
+	 */
+	RemoteListener getDefaultListener();
+
+	/**
+	 * @param receiver
+	 *            The destination where any final usage records are to be
+	 *            written in order to log them back to the server.
+	 */
+	void setURReceiver(UsageRecordReceiver receiver);
+
+	/**
+	 * Arrange for the deletion of any resources created during worker process
+	 * construction. Guaranteed to be the last thing done before finalization.
+	 * 
+	 * @throws ImplementationException
+	 *             If anything goes wrong.
+	 */
+	void deleteLocalResources() throws ImplementationException;
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/api/WorkerFactory.java b/server-worker/src/main/java/org/taverna/server/localworker/api/WorkerFactory.java
new file mode 100644
index 0000000..4ce13a7
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/api/WorkerFactory.java
@@ -0,0 +1,18 @@
+package org.taverna.server.localworker.api;
+
+
+/**
+ * Class that manufactures instances of {@link Worker}.
+ * 
+ * @author Donal Fellows
+ */
+public interface WorkerFactory {
+	/**
+	 * Create an instance of the low-level worker class.
+	 * 
+	 * @return The worker object.
+	 * @throws Exception
+	 *             If anything goes wrong.
+	 */
+	Worker makeInstance() throws Exception;
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/impl/DirectoryDelegate.java b/server-worker/src/main/java/org/taverna/server/localworker/impl/DirectoryDelegate.java
new file mode 100644
index 0000000..1692856
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/impl/DirectoryDelegate.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.impl;
+
+import static org.apache.commons.io.FileUtils.forceDelete;
+import static org.apache.commons.io.FileUtils.forceMkdir;
+import static org.apache.commons.io.FileUtils.touch;
+import static org.taverna.server.localworker.impl.utils.FilenameVerifier.getValidatedNewFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.collections.MapIterator;
+import org.apache.commons.collections.map.ReferenceMap;
+import org.taverna.server.localworker.remote.RemoteDirectory;
+import org.taverna.server.localworker.remote.RemoteDirectoryEntry;
+import org.taverna.server.localworker.remote.RemoteFile;
+
+/**
+ * This class acts as a remote-aware delegate for the workflow run's working
+ * directory and its subdirectories.
+ * 
+ * @author Donal Fellows
+ * @see FileDelegate
+ */
+@SuppressWarnings("serial")
+public class DirectoryDelegate extends UnicastRemoteObject implements
+		RemoteDirectory {
+	private File dir;
+	private DirectoryDelegate parent;
+	private ReferenceMap localCache;
+
+	/**
+	 * @param dir
+	 * @param parent
+	 * @throws RemoteException
+	 *             If registration of the directory fails.
+	 */
+	public DirectoryDelegate(@Nonnull File dir,
+			@Nonnull DirectoryDelegate parent) throws RemoteException {
+		super();
+		this.localCache = new ReferenceMap();
+		this.dir = dir;
+		this.parent = parent;
+	}
+
+	@Override
+	public Collection<RemoteDirectoryEntry> getContents()
+			throws RemoteException {
+		List<RemoteDirectoryEntry> result = new ArrayList<>();
+		for (String s : dir.list()) {
+			if (s.equals(".") || s.equals(".."))
+				continue;
+			File f = new File(dir, s);
+			RemoteDirectoryEntry entry;
+			synchronized (localCache) {
+				entry = (RemoteDirectoryEntry) localCache.get(s);
+				if (f.isDirectory()) {
+					if (entry == null || !(entry instanceof DirectoryDelegate)) {
+						entry = new DirectoryDelegate(f, this);
+						localCache.put(s, entry);
+					}
+				} else if (f.isFile()) {
+					if (entry == null || !(entry instanceof FileDelegate)) {
+						entry = new FileDelegate(f, this);
+						localCache.put(s, entry);
+					}
+				} else {
+					// not file or dir; skip...
+					continue;
+				}
+			}
+			result.add(entry);
+		}
+		return result;
+	}
+
+	@Override
+	public RemoteFile makeEmptyFile(String name) throws IOException {
+		File f = getValidatedNewFile(dir, name);
+		touch(f);
+		FileDelegate delegate = new FileDelegate(f, this);
+		synchronized (localCache) {
+			localCache.put(name, delegate);
+		}
+		return delegate;
+	}
+
+	@Override
+	public RemoteDirectory makeSubdirectory(String name) throws IOException {
+		File f = getValidatedNewFile(dir, name);
+		forceMkdir(f);
+		DirectoryDelegate delegate = new DirectoryDelegate(f, this);
+		synchronized (localCache) {
+			localCache.put(name, delegate);
+		}
+		return delegate;
+	}
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public void destroy() throws IOException {
+		if (parent == null)
+			throw new IOException("tried to destroy main job working directory");
+		Collection<RemoteDirectoryEntry> values;
+		synchronized (localCache) {
+			values = new ArrayList<>(localCache.values());
+		}
+		for (RemoteDirectoryEntry obj : values) {
+			if (obj == null)
+				continue;
+			try {
+				obj.destroy();
+			} catch (IOException e) {
+			}
+		}
+		forceDelete(dir);
+		parent.forgetEntry(this);
+	}
+
+	@Override
+	public RemoteDirectory getContainingDirectory() {
+		return parent;
+	}
+
+	void forgetEntry(@Nonnull RemoteDirectoryEntry entry) {
+		synchronized (localCache) {
+			MapIterator i = localCache.mapIterator();
+			while (i.hasNext()) {
+				Object key = i.next();
+				if (entry == i.getValue()) {
+					localCache.remove(key);
+					break;
+				}
+			}
+		}
+	}
+
+	@Override
+	public String getName() {
+		if (parent == null)
+			return "";
+		return dir.getName();
+	}
+
+	@Override
+	public Date getModificationDate() throws RemoteException {
+		return new Date(dir.lastModified());
+	}
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/impl/FileDelegate.java b/server-worker/src/main/java/org/taverna/server/localworker/impl/FileDelegate.java
new file mode 100644
index 0000000..7e47af9
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/impl/FileDelegate.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.impl;
+
+import static java.lang.System.arraycopy;
+import static java.net.InetAddress.getLocalHost;
+import static org.apache.commons.io.FileUtils.copyFile;
+import static org.apache.commons.io.FileUtils.forceDelete;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.Date;
+
+import javax.annotation.Nonnull;
+
+import org.taverna.server.localworker.remote.RemoteDirectory;
+import org.taverna.server.localworker.remote.RemoteFile;
+
+/**
+ * This class acts as a remote-aware delegate for the files in a workflow run's
+ * working directory and its subdirectories.
+ * 
+ * @author Donal Fellows
+ * @see DirectoryDelegate
+ */
+@java.lang.SuppressWarnings("serial")
+public class FileDelegate extends UnicastRemoteObject implements RemoteFile {
+	private File file;
+	private DirectoryDelegate parent;
+
+	/**
+	 * @param file
+	 * @param parent
+	 * @throws RemoteException
+	 *             If registration of the file fails.
+	 */
+	public FileDelegate(@Nonnull File file, @Nonnull DirectoryDelegate parent)
+			throws RemoteException {
+		super();
+		this.file = file;
+		this.parent = parent;
+	}
+
+	@Override
+	public byte[] getContents(int offset, int length) throws IOException {
+		if (length == -1)
+			length = (int) (file.length() - offset);
+		if (length < 0 || length > 1024 * 64)
+			length = 1024 * 64;
+		byte[] buffer = new byte[length];
+		int read;
+		try (FileInputStream fis = new FileInputStream(file)) {
+			if (offset > 0 && fis.skip(offset) != offset)
+				throw new IOException("did not move to correct offset in file");
+			read = fis.read(buffer);
+		}
+		if (read <= 0)
+			return new byte[0];
+		if (read < buffer.length) {
+			byte[] shortened = new byte[read];
+			arraycopy(buffer, 0, shortened, 0, read);
+			return shortened;
+		}
+		return buffer;
+	}
+
+	@Override
+	public long getSize() {
+		return file.length();
+	}
+
+	@Override
+	public void setContents(byte[] data) throws IOException {
+		try (FileOutputStream fos = new FileOutputStream(file)) {
+			fos.write(data);
+		}
+	}
+
+	@Override
+	public void appendContents(byte[] data) throws IOException {
+		try (FileOutputStream fos = new FileOutputStream(file, true)) {
+			fos.write(data);
+		}
+	}
+
+	@Override
+	public void destroy() throws IOException {
+		forceDelete(file);
+		parent.forgetEntry(this);
+		parent = null;
+	}
+
+	@Override
+	public RemoteDirectory getContainingDirectory() {
+		return parent;
+	}
+
+	@Override
+	public String getName() {
+		return file.getName();
+	}
+
+	@Override
+	public void copy(RemoteFile sourceFile) throws RemoteException, IOException {
+		String sourceHost = sourceFile.getNativeHost();
+		if (!getNativeHost().equals(sourceHost)) {
+			throw new IOException(
+					"cross-system copy not implemented; cannot copy from "
+							+ sourceHost + " to " + getNativeHost());
+		}
+		// Must copy; cannot count on other file to stay unmodified
+		copyFile(new File(sourceFile.getNativeName()), file);
+	}
+
+	@Override
+	public String getNativeName() {
+		return file.getAbsolutePath();
+	}
+
+	@Override
+	public String getNativeHost() {
+		try {
+			return getLocalHost().getHostAddress();
+		} catch (UnknownHostException e) {
+			throw new RuntimeException(
+					"unexpected failure to resolve local host address", e);
+		}
+	}
+
+	@Override
+	public Date getModificationDate() throws RemoteException {
+		return new Date(file.lastModified());
+	}
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/impl/LocalWorker.java b/server-worker/src/main/java/org/taverna/server/localworker/impl/LocalWorker.java
new file mode 100644
index 0000000..adf3ea7
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/impl/LocalWorker.java
@@ -0,0 +1,769 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.impl;
+
+import static java.lang.Runtime.getRuntime;
+import static java.lang.System.getProperty;
+import static java.lang.System.out;
+import static java.lang.management.ManagementFactory.getRuntimeMXBean;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.UUID.randomUUID;
+import static org.apache.commons.io.FileUtils.forceDelete;
+import static org.apache.commons.io.FileUtils.forceMkdir;
+import static org.apache.commons.io.FileUtils.writeByteArrayToFile;
+import static org.apache.commons.io.FileUtils.writeLines;
+import static org.taverna.server.localworker.api.Constants.HELIO_TOKEN_NAME;
+import static org.taverna.server.localworker.api.Constants.KEYSTORE_FILE;
+import static org.taverna.server.localworker.api.Constants.KEYSTORE_PASSWORD;
+import static org.taverna.server.localworker.api.Constants.SECURITY_DIR_NAME;
+import static org.taverna.server.localworker.api.Constants.SHARED_DIR_PROP;
+import static org.taverna.server.localworker.api.Constants.SUBDIR_LIST;
+import static org.taverna.server.localworker.api.Constants.SYSTEM_ENCODING;
+import static org.taverna.server.localworker.api.Constants.TRUSTSTORE_FILE;
+import static org.taverna.server.localworker.impl.utils.FilenameVerifier.getValidatedFile;
+import static org.taverna.server.localworker.remote.RemoteStatus.Finished;
+import static org.taverna.server.localworker.remote.RemoteStatus.Initialized;
+import static org.taverna.server.localworker.remote.RemoteStatus.Operating;
+import static org.taverna.server.localworker.remote.RemoteStatus.Stopped;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+
+import org.taverna.server.localworker.api.Worker;
+import org.taverna.server.localworker.api.WorkerFactory;
+import org.taverna.server.localworker.remote.IllegalStateTransitionException;
+import org.taverna.server.localworker.remote.ImplementationException;
+import org.taverna.server.localworker.remote.RemoteDirectory;
+import org.taverna.server.localworker.remote.RemoteInput;
+import org.taverna.server.localworker.remote.RemoteListener;
+import org.taverna.server.localworker.remote.RemoteSecurityContext;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.localworker.remote.RemoteStatus;
+import org.taverna.server.localworker.remote.StillWorkingOnItException;
+import org.taverna.server.localworker.server.UsageRecordReceiver;
+
+/**
+ * This class implements one side of the connection between the Taverna Server
+ * master server and this process. It delegates to a {@link Worker} instance the
+ * handling of actually running a workflow.
+ * 
+ * @author Donal Fellows
+ * @see DirectoryDelegate
+ * @see FileDelegate
+ * @see WorkerCore
+ */
+@SuppressWarnings("serial")
+public class LocalWorker extends UnicastRemoteObject implements RemoteSingleRun {
+	// ----------------------- CONSTANTS -----------------------
+
+	/** Handle to the directory containing the security info. */
+	static final File SECURITY_DIR;
+	static final String SLASHTEMP;
+	static {
+		SLASHTEMP = getProperty("java.io.tmpdir");
+		File home = new File(getProperty("user.home"));
+		// If we can't write to $HOME (i.e., we're in an odd deployment) use
+		// the official version of /tmp/$PID as a fallback.
+		if (!home.canWrite())
+			home = new File(SLASHTEMP, getRuntimeMXBean().getName());
+		SECURITY_DIR = new File(home, SECURITY_DIR_NAME);
+	}
+
+	// ----------------------- VARIABLES -----------------------
+
+	/**
+	 * Magic flag used to turn off problematic code when testing inside CI
+	 * environment.
+	 */
+	static boolean DO_MKDIR = true;
+
+	/** What to use to run a workflow engine. */
+	private final String executeWorkflowCommand;
+	/** What workflow to run. */
+	private final byte[] workflow;
+	/** The remote access object for the working directory. */
+	private final DirectoryDelegate baseDir;
+	/** What inputs to pass as files. */
+	final Map<String, String> inputFiles;
+	/** What inputs to pass as files (as file refs). */
+	final Map<String, File> inputRealFiles;
+	/** What inputs to pass as direct values. */
+	final Map<String, String> inputValues;
+	/** What delimiters to use. */
+	final Map<String, String> inputDelimiters;
+	/** The interface to the workflow engine subprocess. */
+	private final Worker core;
+	/** Our descriptor token (UUID). */
+	private final String masterToken;
+	/**
+	 * The root working directory for a workflow run, or <tt>null</tt> if it has
+	 * been deleted.
+	 */
+	private File base;
+	/**
+	 * When did this workflow start running, or <tt>null</tt> for
+	 * "never/not yet".
+	 */
+	private Date start;
+	/**
+	 * When did this workflow finish running, or <tt>null</tt> for
+	 * "never/not yet".
+	 */
+	private Date finish;
+	/** The cached status of the workflow run. */
+	RemoteStatus status;
+	/**
+	 * The name of the input Baclava document, or <tt>null</tt> to not do it
+	 * that way.
+	 */
+	String inputBaclava;
+	/**
+	 * The name of the output Baclava document, or <tt>null</tt> to not do it
+	 * that way.
+	 */
+	String outputBaclava;
+	/**
+	 * The file containing the input Baclava document, or <tt>null</tt> to not
+	 * do it that way.
+	 */
+	private File inputBaclavaFile;
+	/**
+	 * The file containing the output Baclava document, or <tt>null</tt> to not
+	 * do it that way.
+	 */
+	private File outputBaclavaFile;
+	/**
+	 * Registered shutdown hook so that we clean up when this process is killed
+	 * off, or <tt>null</tt> if that is no longer necessary.
+	 */
+	Thread shutdownHook;
+	/** Location for security information to be written to. */
+	File securityDirectory;
+	/**
+	 * Password to use to encrypt security information.
+	 */
+	char[] keystorePassword = KEYSTORE_PASSWORD;
+	/** Additional server-specified environment settings. */
+	Map<String, String> environment = new HashMap<>();
+	/** Additional server-specified java runtime settings. */
+	List<String> runtimeSettings = new ArrayList<>();
+	URL interactionFeedURL;
+	URL webdavURL;
+	URL publishURL;//FIXME
+	private boolean doProvenance = true;
+
+	// ----------------------- METHODS -----------------------
+
+	/**
+	 * @param executeWorkflowCommand
+	 *            The script used to execute workflows.
+	 * @param workflow
+	 *            The workflow to execute.
+	 * @param workerClass
+	 *            The class to instantiate as our local representative of the
+	 *            run.
+	 * @param urReceiver
+	 *            The remote class to report the generated usage record(s) to.
+	 * @param id
+	 *            The UUID to use, or <tt>null</tt> if we are to invent one.
+	 * @param seedEnvironment
+	 *            The key/value pairs to seed the worker subprocess environment
+	 *            with.
+	 * @param javaParams
+	 *            Parameters to pass to the worker subprocess java runtime
+	 *            itself.
+	 * @param workerFactory
+	 *            How to make instances of the low-level worker objects.
+	 * @throws RemoteException
+	 *             If registration of the worker fails.
+	 * @throws ImplementationException
+	 *             If something goes wrong during local setup.
+	 */
+	protected LocalWorker(String executeWorkflowCommand, byte[] workflow,
+			UsageRecordReceiver urReceiver, UUID id,
+			Map<String, String> seedEnvironment, List<String> javaParams,
+			WorkerFactory workerFactory) throws RemoteException,
+			ImplementationException {
+		super();
+		if (id == null)
+			id = randomUUID();
+		masterToken = id.toString();
+		this.workflow = workflow;
+		this.executeWorkflowCommand = executeWorkflowCommand;
+		String sharedDir = getProperty(SHARED_DIR_PROP, SLASHTEMP);
+		base = new File(sharedDir, masterToken);
+		out.println("about to create " + base);
+		try {
+			forceMkdir(base);
+			for (String subdir : SUBDIR_LIST) {
+				new File(base, subdir).mkdir();
+			}
+		} catch (IOException e) {
+			throw new ImplementationException(
+					"problem creating run working directory", e);
+		}
+		baseDir = new DirectoryDelegate(base, null);
+		inputFiles = new HashMap<>();
+		inputRealFiles = new HashMap<>();
+		inputValues = new HashMap<>();
+		inputDelimiters = new HashMap<>();
+		environment.putAll(seedEnvironment);
+		runtimeSettings.addAll(javaParams);
+		try {
+			core = workerFactory.makeInstance();
+		} catch (Exception e) {
+			out.println("problem when creating core worker implementation");
+			e.printStackTrace(out);
+			throw new ImplementationException(
+					"problem when creating core worker implementation", e);
+		}
+		core.setURReceiver(urReceiver);
+		Thread t = new Thread(new Runnable() {
+			/**
+			 * Kill off the worker launched by the core.
+			 */
+			@Override
+			public void run() {
+				try {
+					shutdownHook = null;
+					destroy();
+				} catch (ImplementationException e) {
+					// Absolutely nothing we can do here
+				}
+			}
+		});
+		getRuntime().addShutdownHook(t);
+		shutdownHook = t;
+		status = Initialized;
+	}
+
+	@Override
+	public void destroy() throws ImplementationException {
+		killWorkflowSubprocess();
+		removeFromShutdownHooks();
+		// Is this it?
+		deleteWorkingDirectory();
+		deleteSecurityManagerDirectory();
+		core.deleteLocalResources();
+	}
+
+	private void killWorkflowSubprocess() {
+		if (status != Finished && status != Initialized)
+			try {
+				core.killWorker();
+				if (finish == null)
+					finish = new Date();
+			} catch (Exception e) {
+				out.println("problem when killing worker");
+				e.printStackTrace(out);
+			}
+	}
+
+	private void removeFromShutdownHooks() throws ImplementationException {
+		try {
+			if (shutdownHook != null)
+				getRuntime().removeShutdownHook(shutdownHook);
+		} catch (RuntimeException e) {
+			throw new ImplementationException("problem removing shutdownHook",
+					e);
+		} finally {
+			shutdownHook = null;
+		}
+	}
+
+	private void deleteWorkingDirectory() throws ImplementationException {
+		try {
+			if (base != null)
+				forceDelete(base);
+		} catch (IOException e) {
+			out.println("problem deleting working directory");
+			e.printStackTrace(out);
+			throw new ImplementationException(
+					"problem deleting working directory", e);
+		} finally {
+			base = null;
+		}
+	}
+
+	private void deleteSecurityManagerDirectory()
+			throws ImplementationException {
+		try {
+			if (securityDirectory != null)
+				forceDelete(securityDirectory);
+		} catch (IOException e) {
+			out.println("problem deleting security directory");
+			e.printStackTrace(out);
+			throw new ImplementationException(
+					"problem deleting security directory", e);
+		} finally {
+			securityDirectory = null;
+		}
+	}
+
+	@Override
+	public void addListener(RemoteListener listener) throws RemoteException,
+			ImplementationException {
+		throw new ImplementationException("not implemented");
+	}
+
+	@Override
+	public String getInputBaclavaFile() {
+		return inputBaclava;
+	}
+
+	@Override
+	public List<RemoteInput> getInputs() throws RemoteException {
+		ArrayList<RemoteInput> result = new ArrayList<>();
+		for (String name : inputFiles.keySet())
+			result.add(new InputDelegate(name));
+		return result;
+	}
+
+	@Override
+	public List<String> getListenerTypes() {
+		return emptyList();
+	}
+
+	@Override
+	public List<RemoteListener> getListeners() {
+		return singletonList(core.getDefaultListener());
+	}
+
+	@Override
+	public String getOutputBaclavaFile() {
+		return outputBaclava;
+	}
+
+	class SecurityDelegate extends UnicastRemoteObject implements
+			RemoteSecurityContext {
+		private void setPrivatePerms(File dir) {
+			if (!dir.setReadable(false, false) || !dir.setReadable(true, true)
+					|| !dir.setExecutable(false, false)
+					|| !dir.setExecutable(true, true)
+					|| !dir.setWritable(false, false)
+					|| !dir.setWritable(true, true)) {
+				out.println("warning: "
+						+ "failed to set permissions on security context directory");
+			}
+		}
+
+		protected SecurityDelegate(String token) throws IOException {
+			super();
+			if (DO_MKDIR) {
+				securityDirectory = new File(SECURITY_DIR, token);
+				forceMkdir(securityDirectory);
+				setPrivatePerms(securityDirectory);
+			}
+		}
+
+		/**
+		 * Write some data to a given file in the context directory.
+		 * 
+		 * @param name
+		 *            The name of the file to write.
+		 * @param data
+		 *            The bytes to put in the file.
+		 * @throws RemoteException
+		 *             If anything goes wrong.
+		 * @throws ImplementationException
+		 */
+		protected void write(String name, byte[] data) throws RemoteException,
+				ImplementationException {
+			try {
+				File f = new File(securityDirectory, name);
+				writeByteArrayToFile(f, data);
+			} catch (IOException e) {
+				throw new ImplementationException("problem writing " + name, e);
+			}
+		}
+
+		/**
+		 * Write some data to a given file in the context directory.
+		 * 
+		 * @param name
+		 *            The name of the file to write.
+		 * @param data
+		 *            The lines to put in the file. The
+		 *            {@linkplain LocalWorker#SYSTEM_ENCODING system encoding}
+		 *            will be used to do the writing.
+		 * @throws RemoteException
+		 *             If anything goes wrong.
+		 * @throws ImplementationException
+		 */
+		protected void write(String name, Collection<String> data)
+				throws RemoteException, ImplementationException {
+			try {
+				File f = new File(securityDirectory, name);
+				writeLines(f, SYSTEM_ENCODING, data);
+			} catch (IOException e) {
+				throw new ImplementationException("problem writing " + name, e);
+			}
+		}
+
+		/**
+		 * Write some data to a given file in the context directory.
+		 * 
+		 * @param name
+		 *            The name of the file to write.
+		 * @param data
+		 *            The line to put in the file. The
+		 *            {@linkplain LocalWorker#SYSTEM_ENCODING system encoding}
+		 *            will be used to do the writing.
+		 * @throws RemoteException
+		 *             If anything goes wrong.
+		 * @throws ImplementationException
+		 */
+		protected void write(String name, char[] data) throws RemoteException,
+				ImplementationException {
+			try {
+				File f = new File(securityDirectory, name);
+				writeLines(f, SYSTEM_ENCODING, asList(new String(data)));
+			} catch (IOException e) {
+				throw new ImplementationException("problem writing " + name, e);
+			}
+		}
+
+		@Override
+		public void setKeystore(byte[] keystore) throws RemoteException,
+				ImplementationException {
+			if (status != Initialized)
+				throw new RemoteException("not initializing");
+			if (keystore == null)
+				throw new IllegalArgumentException("keystore may not be null");
+			write(KEYSTORE_FILE, keystore);
+		}
+
+		@Override
+		public void setPassword(char[] password) throws RemoteException {
+			if (status != Initialized)
+				throw new RemoteException("not initializing");
+			if (password == null)
+				throw new IllegalArgumentException("password may not be null");
+			keystorePassword = password.clone();
+		}
+
+		@Override
+		public void setTruststore(byte[] truststore) throws RemoteException,
+				ImplementationException {
+			if (status != Initialized)
+				throw new RemoteException("not initializing");
+			if (truststore == null)
+				throw new IllegalArgumentException("truststore may not be null");
+			write(TRUSTSTORE_FILE, truststore);
+		}
+
+		@Override
+		public void setUriToAliasMap(Map<URI, String> uriToAliasMap)
+				throws RemoteException {
+			if (status != Initialized)
+				throw new RemoteException("not initializing");
+			if (uriToAliasMap == null)
+				return;
+			ArrayList<String> lines = new ArrayList<>();
+			for (Entry<URI, String> site : uriToAliasMap.entrySet())
+				lines.add(site.getKey().toASCIIString() + " " + site.getValue());
+			// write(URI_ALIAS_MAP, lines);
+		}
+
+		@Override
+		public void setHelioToken(String helioToken) throws RemoteException {
+			if (status != Initialized)
+				throw new RemoteException("not initializing");
+			out.println("registering HELIO CIS token for export");
+			environment.put(HELIO_TOKEN_NAME, helioToken);
+		}
+	}
+
+	@Override
+	public RemoteSecurityContext getSecurityContext() throws RemoteException,
+			ImplementationException {
+		try {
+			return new SecurityDelegate(masterToken);
+		} catch (RemoteException e) {
+			if (e.getCause() != null)
+				throw new ImplementationException(
+						"problem initializing security context", e.getCause());
+			throw e;
+		} catch (IOException e) {
+			throw new ImplementationException(
+					"problem initializing security context", e);
+		}
+	}
+
+	@Override
+	public RemoteStatus getStatus() {
+		// only state that can spontaneously change to another
+		if (status == Operating) {
+			status = core.getWorkerStatus();
+			if (status == Finished && finish == null)
+				finish = new Date();
+		}
+		return status;
+	}
+
+	@Override
+	public RemoteDirectory getWorkingDirectory() {
+		return baseDir;
+	}
+
+	File validateFilename(String filename) throws RemoteException {
+		if (filename == null)
+			throw new IllegalArgumentException("filename must be non-null");
+		try {
+			return getValidatedFile(base, filename.split("/"));
+		} catch (IOException e) {
+			throw new IllegalArgumentException("failed to validate filename", e);
+		}
+	}
+
+	class InputDelegate extends UnicastRemoteObject implements RemoteInput {
+		private String name;
+
+		InputDelegate(String name) throws RemoteException {
+			super();
+			this.name = name;
+			if (!inputFiles.containsKey(name)) {
+				if (status != Initialized)
+					throw new IllegalStateException("not initializing");
+				inputFiles.put(name, null);
+				inputRealFiles.put(name, null);
+				inputValues.put(name, null);
+				inputDelimiters.put(name, null);
+			}
+		}
+
+		@Override
+		public String getFile() {
+			return inputFiles.get(name);
+		}
+
+		@Override
+		public String getName() {
+			return name;
+		}
+
+		@Override
+		public String getValue() {
+			return inputValues.get(name);
+		}
+
+		@Override
+		public String getDelimiter() throws RemoteException {
+			return inputDelimiters.get(name);
+		}
+
+		@Override
+		public void setFile(String file) throws RemoteException {
+			if (status != Initialized)
+				throw new IllegalStateException("not initializing");
+			inputRealFiles.put(name, validateFilename(file));
+			inputValues.put(name, null);
+			inputFiles.put(name, file);
+			inputBaclava = null;
+		}
+
+		@Override
+		public void setValue(String value) throws RemoteException {
+			if (status != Initialized)
+				throw new IllegalStateException("not initializing");
+			inputValues.put(name, value);
+			inputFiles.put(name, null);
+			inputRealFiles.put(name, null);
+			inputBaclava = null;
+		}
+
+		@Override
+		public void setDelimiter(String delimiter) throws RemoteException {
+			if (status != Initialized)
+				throw new IllegalStateException("not initializing");
+			if (inputBaclava != null)
+				throw new IllegalStateException("input baclava file set");
+			if (delimiter != null) {
+				if (delimiter.length() > 1)
+					throw new IllegalStateException(
+							"multi-character delimiter not permitted");
+				if (delimiter.charAt(0) == 0)
+					throw new IllegalStateException(
+							"may not use NUL for splitting");
+				if (delimiter.charAt(0) > 127)
+					throw new IllegalStateException(
+							"only ASCII characters supported for splitting");
+			}
+			inputDelimiters.put(name, delimiter);
+		}
+	}
+
+	@Override
+	public RemoteInput makeInput(String name) throws RemoteException {
+		return new InputDelegate(name);
+	}
+
+	@Override
+	public RemoteListener makeListener(String type, String configuration)
+			throws RemoteException {
+		throw new RemoteException("listener manufacturing unsupported");
+	}
+
+	@Override
+	public void setInputBaclavaFile(String filename) throws RemoteException {
+		if (status != Initialized)
+			throw new IllegalStateException("not initializing");
+		inputBaclavaFile = validateFilename(filename);
+		for (String input : inputFiles.keySet()) {
+			inputFiles.put(input, null);
+			inputRealFiles.put(input, null);
+			inputValues.put(input, null);
+		}
+		inputBaclava = filename;
+	}
+
+	@Override
+	public void setOutputBaclavaFile(String filename) throws RemoteException {
+		if (status != Initialized)
+			throw new IllegalStateException("not initializing");
+		if (filename != null)
+			outputBaclavaFile = validateFilename(filename);
+		else
+			outputBaclavaFile = null;
+		outputBaclava = filename;
+	}
+
+	@Override
+	public void setGenerateProvenance(boolean prov) {
+		doProvenance = prov;
+	}
+
+	@Override
+	public void setStatus(RemoteStatus newStatus)
+			throws IllegalStateTransitionException, RemoteException,
+			ImplementationException, StillWorkingOnItException {
+		if (status == newStatus)
+			return;
+
+		switch (newStatus) {
+		case Initialized:
+			throw new IllegalStateTransitionException(
+					"may not move back to start");
+		case Operating:
+			switch (status) {
+			case Initialized:
+				boolean started;
+				try {
+					started = createWorker();
+				} catch (Exception e) {
+					throw new ImplementationException(
+							"problem creating executing workflow", e);
+				}
+				if (!started)
+					throw new StillWorkingOnItException(
+							"workflow start in process");
+				break;
+			case Stopped:
+				try {
+					core.startWorker();
+				} catch (Exception e) {
+					throw new ImplementationException(
+							"problem continuing workflow run", e);
+				}
+				break;
+			case Finished:
+				throw new IllegalStateTransitionException("already finished");
+			default:
+				break;
+			}
+			status = Operating;
+			break;
+		case Stopped:
+			switch (status) {
+			case Initialized:
+				throw new IllegalStateTransitionException(
+						"may only stop from Operating");
+			case Operating:
+				try {
+					core.stopWorker();
+				} catch (Exception e) {
+					throw new ImplementationException(
+							"problem stopping workflow run", e);
+				}
+				break;
+			case Finished:
+				throw new IllegalStateTransitionException("already finished");
+			default:
+				break;
+			}
+			status = Stopped;
+			break;
+		case Finished:
+			switch (status) {
+			case Operating:
+			case Stopped:
+				try {
+					core.killWorker();
+					if (finish == null)
+						finish = new Date();
+				} catch (Exception e) {
+					throw new ImplementationException(
+							"problem killing workflow run", e);
+				}
+			default:
+				break;
+			}
+			status = Finished;
+			break;
+		}
+	}
+
+	private boolean createWorker() throws Exception {
+		start = new Date();
+		char[] pw = keystorePassword;
+		keystorePassword = null;
+		/*
+		 * Do not clear the keystorePassword array here; its ownership is
+		 * *transferred* to the worker core which doesn't copy it but *does*
+		 * clear it after use.
+		 */
+		return core.initWorker(this, executeWorkflowCommand, workflow, base,
+				inputBaclavaFile, inputRealFiles, inputValues, inputDelimiters,
+				outputBaclavaFile, securityDirectory, pw, doProvenance,
+				environment, masterToken, runtimeSettings);
+	}
+
+	@Override
+	public Date getFinishTimestamp() {
+		return finish == null ? null : new Date(finish.getTime());
+	}
+
+	@Override
+	public Date getStartTimestamp() {
+		return start == null ? null : new Date(start.getTime());
+	}
+
+	@Override
+	public void setInteractionServiceDetails(URL feed, URL webdav, URL publish) {
+		interactionFeedURL = feed;
+		webdavURL = webdav;
+		publishURL = publish;
+	}
+
+	@Override
+	public void ping() {
+		// Do nothing here; this *should* be empty
+	}
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/impl/TavernaRunManager.java b/server-worker/src/main/java/org/taverna/server/localworker/impl/TavernaRunManager.java
new file mode 100644
index 0000000..03ee69d
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/impl/TavernaRunManager.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.impl;
+
+import static java.lang.Runtime.getRuntime;
+import static java.lang.System.exit;
+import static java.lang.System.getProperty;
+import static java.lang.System.out;
+import static java.lang.System.setProperty;
+import static java.lang.System.setSecurityManager;
+import static java.rmi.registry.LocateRegistry.getRegistry;
+import static org.taverna.server.localworker.api.Constants.DEATH_DELAY;
+import static org.taverna.server.localworker.api.Constants.LOCALHOST;
+import static org.taverna.server.localworker.api.Constants.RMI_HOST_PROP;
+import static org.taverna.server.localworker.api.Constants.SECURITY_POLICY_FILE;
+import static org.taverna.server.localworker.api.Constants.SEC_POLICY_PROP;
+import static org.taverna.server.localworker.api.Constants.UNSECURE_PROP;
+
+import java.io.ByteArrayInputStream;
+import java.net.URI;
+import java.rmi.RMISecurityManager;
+import java.rmi.RemoteException;
+import java.rmi.registry.Registry;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.taverna.server.localworker.api.RunAccounting;
+import org.taverna.server.localworker.api.Worker;
+import org.taverna.server.localworker.api.WorkerFactory;
+import org.taverna.server.localworker.remote.RemoteRunFactory;
+import org.taverna.server.localworker.remote.RemoteSingleRun;
+import org.taverna.server.localworker.server.UsageRecordReceiver;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import uk.org.taverna.scufl2.api.io.WorkflowBundleIO;
+
+/**
+ * The registered factory for runs, this class is responsible for constructing
+ * runs that are suitable for particular users. It is also the entry point for
+ * this whole process.
+ * 
+ * @author Donal Fellows
+ * @see LocalWorker
+ */
+@SuppressWarnings("serial")
+public class TavernaRunManager extends UnicastRemoteObject implements
+		RemoteRunFactory, RunAccounting, WorkerFactory {
+	String command;
+	Map<String, String> seedEnvironment = new HashMap<>();
+	List<String> javaInitParams = new ArrayList<>();
+	private WorkflowBundleIO io;
+	private int activeRuns = 0;
+	// Hacks!
+	public static String interactionHost;
+	public static String interactionPort;
+	public static String interactionWebdavPath;
+	public static String interactionFeedPath;
+
+	/**
+	 * How to get the actual workflow document from the XML document that it is
+	 * contained in.
+	 * 
+	 * @param containerDocument
+	 *            The document sent from the web interface.
+	 * @return The element describing the workflow, as expected by the Taverna
+	 *         command line executor.
+	 */
+	protected Element unwrapWorkflow(Document containerDocument) {
+		return (Element) containerDocument.getDocumentElement().getFirstChild();
+	}
+
+	private static final String usage = "java -jar server.worker.jar workflowExecScript ?-Ekey=val...? ?-Jconfig? UUID";
+
+	/**
+	 * An RMI-enabled factory for runs.
+	 * 
+	 * @param command
+	 *            What command to call to actually run a run.
+	 * @throws RemoteException
+	 *             If anything goes wrong during creation of the instance.
+	 */
+	public TavernaRunManager(String command) throws RemoteException {
+		this.command = command;
+		this.io = new WorkflowBundleIO();
+	}
+
+	@Override
+	public RemoteSingleRun make(byte[] workflow, String creator,
+			UsageRecordReceiver urReceiver, UUID id) throws RemoteException {
+		if (creator == null)
+			throw new RemoteException("no creator");
+		try {
+			URI wfid = io.readBundle(new ByteArrayInputStream(workflow), null)
+					.getMainWorkflow().getWorkflowIdentifier();
+			out.println("Creating run from workflow <" + wfid + "> for <"
+					+ creator + ">");
+			return new LocalWorker(command, workflow, urReceiver, id,
+					seedEnvironment, javaInitParams, this);
+		} catch (RemoteException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new RemoteException("bad instance construction", e);
+		}
+	}
+
+	private static boolean shuttingDown;
+	private static String factoryName;
+	private static Registry registry;
+
+	static synchronized void unregisterFactory() {
+		if (!shuttingDown) {
+			shuttingDown = true;
+			try {
+				if (factoryName != null && registry != null)
+					registry.unbind(factoryName);
+			} catch (Exception e) {
+				e.printStackTrace(out);
+			}
+		}
+	}
+
+	@Override
+	public void shutdown() {
+		unregisterFactory();
+		new Thread(new DelayedDeath()).start();
+	}
+
+	static class DelayedDeath implements Runnable {
+		@Override
+		public void run() {
+			try {
+				Thread.sleep(DEATH_DELAY);
+			} catch (InterruptedException e) {
+			} finally {
+				exit(0);
+			}
+		}
+	}
+
+	private void addArgument(String arg) {
+		if (arg.startsWith("-E")) {
+			String trimmed = arg.substring(2);
+			int idx = trimmed.indexOf('=');
+			if (idx > 0) {
+				addEnvironmentDefinition(trimmed.substring(0, idx),
+						trimmed.substring(idx + 1));
+				return;
+			}
+		} else if (arg.startsWith("-D")) {
+			if (arg.indexOf('=') > 0) {
+				addJavaParameter(arg);
+				return;
+			}
+		} else if (arg.startsWith("-J")) {
+			addJavaParameter(arg.substring(2));
+			return;
+		}
+		throw new IllegalArgumentException("argument \"" + arg
+				+ "\" must start with -D, -E or -J; "
+				+ "-D and -E must contain a \"=\"");
+	}
+
+	/**
+	 * @param args
+	 *            The arguments from the command line invocation.
+	 * @throws Exception
+	 *             If we can't connect to the RMI registry, or if we can't read
+	 *             the workflow, or if we can't build the worker instance, or
+	 *             register it. Also if the arguments are wrong.
+	 */
+	public static void main(String[] args) throws Exception {
+		if (args.length < 2)
+			throw new Exception("wrong # args: must be \"" + usage + "\"");
+		if (!getProperty(UNSECURE_PROP, "no").equals("yes")) {
+			setProperty(SEC_POLICY_PROP, LocalWorker.class.getClassLoader()
+					.getResource(SECURITY_POLICY_FILE).toExternalForm());
+			setProperty(RMI_HOST_PROP, LOCALHOST);
+		}
+		setSecurityManager(new RMISecurityManager());
+		factoryName = args[args.length - 1];
+		TavernaRunManager man = new TavernaRunManager(args[0]);
+		for (int i = 1; i < args.length - 1; i++)
+			man.addArgument(args[i]);
+		registry = getRegistry(LOCALHOST);
+
+		registry.bind(factoryName, man);
+		getRuntime().addShutdownHook(new Thread() {
+			@Override
+			public void run() {
+				unregisterFactory();
+			}
+		});
+		out.println("registered RemoteRunFactory with ID " + factoryName);
+	}
+
+	private void addJavaParameter(String string) {
+		this.javaInitParams.add(string);
+	}
+
+	private void addEnvironmentDefinition(String key, String value) {
+		this.seedEnvironment.put(key, value);
+	}
+
+	@Override
+	public void setInteractionServiceDetails(String host, String port,
+			String webdavPath, String feedPath) throws RemoteException {
+		if (host == null || port == null || webdavPath == null
+				|| feedPath == null)
+			throw new IllegalArgumentException("all params must be non-null");
+		interactionHost = host;
+		interactionPort = port;
+		interactionWebdavPath = webdavPath;
+		interactionFeedPath = feedPath;
+	}
+
+	@Override
+	public synchronized int countOperatingRuns() {
+		return (activeRuns < 0 ? 0 : activeRuns);
+	}
+
+	@Override
+	public synchronized void runStarted() {
+		activeRuns++;
+	}
+
+	@Override
+	public synchronized void runCeased() {
+		activeRuns--;
+	}
+
+	@Override
+	public Worker makeInstance() throws Exception {
+		return new WorkerCore(this);
+	}
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/impl/WorkerCore.java b/server-worker/src/main/java/org/taverna/server/localworker/impl/WorkerCore.java
new file mode 100644
index 0000000..1a6cff8
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/impl/WorkerCore.java
@@ -0,0 +1,918 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.impl;
+
+import static java.io.File.createTempFile;
+import static java.io.File.pathSeparator;
+import static java.lang.Boolean.parseBoolean;
+import static java.lang.Double.parseDouble;
+import static java.lang.Integer.parseInt;
+import static java.lang.Long.parseLong;
+import static java.lang.Runtime.getRuntime;
+import static java.lang.System.out;
+import static java.net.InetAddress.getLocalHost;
+import static org.apache.commons.io.FileUtils.forceDelete;
+import static org.apache.commons.io.FileUtils.sizeOfDirectory;
+import static org.apache.commons.io.FileUtils.write;
+import static org.apache.commons.io.IOUtils.copy;
+import static org.taverna.server.localworker.api.Constants.CREDENTIAL_MANAGER_DIRECTORY;
+import static org.taverna.server.localworker.api.Constants.CREDENTIAL_MANAGER_PASSWORD;
+import static org.taverna.server.localworker.api.Constants.DEATH_TIME;
+import static org.taverna.server.localworker.api.Constants.DEFAULT_LISTENER_NAME;
+import static org.taverna.server.localworker.api.Constants.KEYSTORE_PASSWORD;
+import static org.taverna.server.localworker.api.Constants.START_WAIT_TIME;
+import static org.taverna.server.localworker.api.Constants.SYSTEM_ENCODING;
+import static org.taverna.server.localworker.api.Constants.TIME;
+import static org.taverna.server.localworker.impl.Status.Aborted;
+import static org.taverna.server.localworker.impl.Status.Completed;
+import static org.taverna.server.localworker.impl.Status.Failed;
+import static org.taverna.server.localworker.impl.Status.Held;
+import static org.taverna.server.localworker.impl.Status.Started;
+import static org.taverna.server.localworker.impl.TavernaRunManager.interactionFeedPath;
+import static org.taverna.server.localworker.impl.TavernaRunManager.interactionHost;
+import static org.taverna.server.localworker.impl.TavernaRunManager.interactionPort;
+import static org.taverna.server.localworker.impl.TavernaRunManager.interactionWebdavPath;
+import static org.taverna.server.localworker.impl.WorkerCore.pmap;
+import static org.taverna.server.localworker.remote.RemoteStatus.Finished;
+import static org.taverna.server.localworker.remote.RemoteStatus.Initialized;
+import static org.taverna.server.localworker.remote.RemoteStatus.Operating;
+import static org.taverna.server.localworker.remote.RemoteStatus.Stopped;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.rmi.RemoteException;
+import java.rmi.server.UnicastRemoteObject;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.xml.datatype.DatatypeConfigurationException;
+import javax.xml.ws.Holder;
+
+import org.ogf.usage.JobUsageRecord;
+import org.taverna.server.localworker.api.RunAccounting;
+import org.taverna.server.localworker.api.Worker;
+import org.taverna.server.localworker.impl.utils.TimingOutTask;
+import org.taverna.server.localworker.remote.ImplementationException;
+import org.taverna.server.localworker.remote.RemoteListener;
+import org.taverna.server.localworker.remote.RemoteStatus;
+import org.taverna.server.localworker.server.UsageRecordReceiver;
+
+/**
+ * The core class that connects to a Taverna command-line workflow execution
+ * engine. This implementation always registers a single listener, &lquo;
+ * <tt>io</tt> &rquo;, with two properties representing the stdout and stderr of
+ * the run and one representing the exit code. The listener is
+ * remote-accessible. It does not support attaching any other listeners.
+ * 
+ * @author Donal Fellows
+ */
+@SuppressWarnings("serial")
+public class WorkerCore extends UnicastRemoteObject implements Worker,
+		RemoteListener {
+	@Nonnull
+	static final Map<String, Property> pmap = new HashMap<>();
+	/**
+	 * Regular expression to extract the detailed timing information from the
+	 * output of /usr/bin/time
+	 */
+	@Nonnull
+	private static final Pattern TimeRE;
+	static {
+		final String TIMERE = "([0-9.:]+)";
+		final String TERMS = "(real|user|system|sys|elapsed)";
+		TimeRE = Pattern.compile(TIMERE + " *" + TERMS + "[ \t]*" + TIMERE
+				+ " *" + TERMS + "[ \t]*" + TIMERE + " *" + TERMS);
+	}
+
+	/**
+	 * Environment variables to remove before any fork (because they're large or
+	 * potentially leaky).
+	 */
+	// TODO Conduct a proper survey of what to remove
+	@Nonnull
+	private static final String[] ENVIRONMENT_TO_REMOVE = { "SUDO_COMMAND",
+			"SUDO_USER", "SUDO_GID", "SUDO_UID", "DISPLAY", "LS_COLORS",
+			"XFILESEARCHPATH", "SSH_AGENT_PID", "SSH_AUTH_SOCK" };
+
+	@Nullable
+	Process subprocess;
+	@Nonnull
+	final StringWriter stdout;
+	@Nonnull
+	final StringWriter stderr;
+	@Nullable
+	Integer exitCode;
+	boolean readyToSendEmail;
+	@Nullable
+	String emailAddress;
+	@Nullable
+	Date start;
+	@Nonnull
+	final RunAccounting accounting;
+	@Nonnull
+	final Holder<Integer> pid;
+
+	private boolean finished;
+	@Nullable
+	private JobUsageRecord ur;
+	@Nullable
+	private File wd;
+	@Nullable
+	private UsageRecordReceiver urreceiver;
+	@Nullable
+	private File workflowFile;
+	private boolean stopped;
+
+	/**
+	 * @param accounting
+	 *            Object that looks after how many runs are executing.
+	 * @throws RemoteException
+	 */
+	public WorkerCore(@Nonnull RunAccounting accounting) throws RemoteException {
+		super();
+		stdout = new StringWriter();
+		stderr = new StringWriter();
+		pid = new Holder<>();
+		this.accounting = accounting;
+	}
+
+	private int getPID() {
+		synchronized (pid) {
+			if (pid.value == null)
+				return -1;
+			return pid.value;
+		}
+	}
+
+	/**
+	 * Fire up the workflow. This causes a transition into the operating state.
+	 * 
+	 * @param executeWorkflowCommand
+	 *            The command to run to execute the workflow.
+	 * @param workflow
+	 *            The workflow document to execute.
+	 * @param workingDir
+	 *            What directory to use as the working directory.
+	 * @param inputBaclava
+	 *            The baclava file to use for inputs, or <tt>null</tt> to use
+	 *            the other <b>input*</b> arguments' values.
+	 * @param inputFiles
+	 *            A mapping of input names to files that supply them. Note that
+	 *            we assume that nothing mapped here will be mapped in
+	 *            <b>inputValues</b>.
+	 * @param inputValues
+	 *            A mapping of input names to values to supply to them. Note
+	 *            that we assume that nothing mapped here will be mapped in
+	 *            <b>inputFiles</b>.
+	 * @param outputBaclava
+	 *            What baclava file to write the output from the workflow into,
+	 *            or <tt>null</tt> to have it written into the <tt>out</tt>
+	 *            subdirectory.
+	 * @param token
+	 *            The name of the workflow run.
+	 * @return <tt>true</tt> if the worker started, or <tt>false</tt> if a
+	 *         timeout occurred.
+	 * @throws IOException
+	 *             If any of quite a large number of things goes wrong.
+	 */
+	@Override
+	public boolean initWorker(
+			@Nonnull final LocalWorker local,
+			@Nonnull final String executeWorkflowCommand,
+			@Nonnull final byte[] workflow,
+			@Nonnull final File workingDir,
+			@Nullable final File inputBaclava,
+			@Nonnull final Map<String, File> inputFiles,
+			@Nonnull final Map<String, String> inputValues, 
+			@Nonnull final Map<String, String> inputDelimiters,
+			@Nullable final File outputBaclava,
+			@Nonnull final File securityDir,
+			@Nullable final char[] password,
+			final boolean generateProvenance,
+			@Nonnull final Map<String, String> environment,
+			@Nullable final String token,
+			@Nonnull final List<String> runtime) throws IOException {
+		try {
+			new TimingOutTask() {
+				@Override
+				public void doIt() throws IOException {
+					startExecutorSubprocess(
+							createProcessBuilder(local, executeWorkflowCommand,
+									workflow, workingDir, inputBaclava,
+									inputFiles, inputValues, inputDelimiters,
+									outputBaclava, securityDir, password,
+									generateProvenance, environment, token,
+									runtime), password);
+				}
+			}.doOrTimeOut(START_WAIT_TIME);
+		} catch (IOException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new IOException(e);
+		}
+		return subprocess != null;
+	}
+
+	private void startExecutorSubprocess(@Nonnull ProcessBuilder pb,
+			@Nullable char[] password) throws IOException {
+		// Start the subprocess
+		out.println("starting " + pb.command() + " in directory "
+				+ pb.directory() + " with environment " + pb.environment());
+		subprocess = pb.start();
+		if (subprocess == null)
+			throw new IOException("unknown failure creating process");
+		start = new Date();
+		accounting.runStarted();
+
+		// Capture its stdout and stderr
+		new AsyncCopy(subprocess.getInputStream(), stdout, pid);
+		new AsyncCopy(subprocess.getErrorStream(), stderr);
+		if (password != null)
+			new PasswordWriterThread(subprocess, password);
+	}
+
+	/**
+	 * Assemble the process builder. Does not launch the subprocess.
+	 * 
+	 * @param local
+	 *            The local worker container.
+	 * @param executeWorkflowCommand
+	 *            The reference to the workflow engine implementation.
+	 * @param workflow
+	 *            The workflow to execute.
+	 * @param workingDir
+	 *            The working directory to use.
+	 * @param inputBaclava
+	 *            What file to read a baclava document from (or <tt>null</tt>)
+	 * @param inputFiles
+	 *            The mapping from inputs to files.
+	 * @param inputValues
+	 *            The mapping from inputs to literal values.
+	 * @param outputBaclava
+	 *            What file to write a baclava document to (or <tt>null</tt>)
+	 * @param securityDir
+	 *            The credential manager directory.
+	 * @param password
+	 *            The password for the credential manager.
+	 * @param environment
+	 *            The seed environment
+	 * @param token
+	 *            The run identifier that the server wants to use.
+	 * @param runtime
+	 *            Any runtime parameters to Java.
+	 * @return The configured process builder.
+	 * @throws IOException
+	 *             If file handling fails
+	 * @throws UnsupportedEncodingException
+	 *             If we can't encode any text (unlikely)
+	 * @throws FileNotFoundException
+	 *             If we can't write the workflow out (unlikely)
+	 */
+	@Nonnull
+	ProcessBuilder createProcessBuilder(@Nonnull LocalWorker local,
+			@Nonnull String executeWorkflowCommand, @Nonnull byte[] workflow,
+			@Nonnull File workingDir, @Nullable File inputBaclava,
+			@Nonnull Map<String, File> inputFiles,
+			@Nonnull Map<String, String> inputValues,
+			@Nonnull Map<String, String> inputDelimiters,
+			@Nullable File outputBaclava, @Nonnull File securityDir,
+			@Nonnull char[] password, boolean generateProvenance,
+			@Nonnull Map<String, String> environment, @Nonnull String token,
+			@Nonnull List<String> runtime) throws IOException,
+			UnsupportedEncodingException, FileNotFoundException {
+		ProcessBuilder pb = new ProcessBuilder();
+		pb.command().add(TIME);
+		/*
+		 * WARNING! HERE THERE BE DRAGONS! BE CAREFUL HERE!
+		 * 
+		 * Work around _Maven_ bug with permissions in zip files! The executable
+		 * bit is stripped by Maven's handling of file permissions, and there's
+		 * no practical way to work around it without massively increasing the
+		 * pain in other ways. Only want this on Unix - Windows isn't affected
+		 * by this - so we use the file separator as a proxy for whether this is
+		 * a true POSIX system. Ugly! Ugly ugly ugly...
+		 * 
+		 * http://jira.codehaus.org/browse/MASSEMBLY-337 is relevant, but not
+		 * the whole story as we don't want to use a non-standard packaging
+		 * method as there's a real chance of it going wrong in an unexpected
+		 * way then. Other parts of the story are that the executable bit isn't
+		 * preserved when unpacking with the dependency plugin, and there's no
+		 * way to be sure that the servlet container will preserve the bit
+		 * either (as that's probably using a Java-based ZIP engine).
+		 */
+		if (File.separatorChar == '/')
+			pb.command().add("/bin/sh");
+		pb.command().add(executeWorkflowCommand);
+		if (runtime != null)
+			pb.command().addAll(runtime);
+
+		// Enable verbose logging
+		pb.command().add("-logfile");
+		pb.command().add(
+				new File(new File(workingDir, "logs"), "detail.log")
+						.getAbsolutePath());
+
+		if (securityDir != null) {
+			pb.command().add(CREDENTIAL_MANAGER_DIRECTORY);
+			pb.command().add(securityDir.getAbsolutePath());
+			out.println("security dir location: " + securityDir);
+		}
+		if (password != null) {
+			pb.command().add(CREDENTIAL_MANAGER_PASSWORD);
+			out.println("password of length " + password.length
+					+ " will be written to subprocess stdin");
+		}
+
+		// Add arguments denoting inputs
+		if (inputBaclava != null) {
+			pb.command().add("-inputdoc");
+			pb.command().add(inputBaclava.getAbsolutePath());
+			if (!inputBaclava.exists())
+				throw new IOException("input baclava file doesn't exist");
+		} else {
+			for (Entry<String, File> port : inputFiles.entrySet()) {
+				if (port.getValue() == null)
+					continue;
+				pb.command().add("-inputfile");
+				pb.command().add(port.getKey());
+				pb.command().add(port.getValue().getAbsolutePath());
+				if (!port.getValue().exists())
+					throw new IOException("input file for port \"" + port
+							+ "\" doesn't exist");
+			}
+			for (Entry<String, String> port : inputValues.entrySet()) {
+				if (port.getValue() == null)
+					continue;
+				pb.command().add("-inputfile");
+				pb.command().add(port.getKey());
+				File f = createTempFile(".tav_in_", null, workingDir);
+				pb.command().add(f.getAbsolutePath());
+				write(f, port.getValue(), "UTF-8");
+			}
+			for (Entry<String, String> delim : inputDelimiters.entrySet()) {
+				if (delim.getValue() == null)
+					continue;
+				pb.command().add("-inputdelimiter");
+				pb.command().add(delim.getKey());
+				pb.command().add(delim.getValue());
+			}
+		}
+
+		// Add arguments denoting outputs
+		if (outputBaclava != null) {
+			pb.command().add("-outputdoc");
+			pb.command().add(outputBaclava.getAbsolutePath());
+			if (!outputBaclava.getParentFile().exists())
+				throw new IOException(
+						"parent directory of output baclava file does not exist");
+			if (outputBaclava.exists())
+				throw new IOException("output baclava file exists");
+			// Provenance cannot be supported when using baclava output
+		} else {
+			File out = new File(workingDir, "out");
+			if (!out.mkdir())
+				throw new IOException("failed to make output directory \"out\"");
+			// Taverna needs the dir to *not* exist now
+			forceDelete(out);
+			pb.command().add("-outputdir");
+			pb.command().add(out.getAbsolutePath());
+			// Enable provenance generation
+			if (generateProvenance) {
+				pb.command().add("-embedded");
+				pb.command().add("-provenance");
+				pb.command().add("-provbundle");
+				pb.command().add("out.bundle.zip");
+			}
+		}
+
+		// Add an argument holding the workflow
+		File tmp = createTempFile(".wf_", ".scufl2", workingDir);
+		try (OutputStream os = new FileOutputStream(tmp)) {
+			os.write(workflow);
+		}
+		pb.command().add(workflowFile.getAbsolutePath());
+
+		// Indicate what working directory to use
+		pb.directory(workingDir);
+		wd = workingDir;
+
+		Map<String, String> env = pb.environment();
+		for (String name : ENVIRONMENT_TO_REMOVE)
+			env.remove(name);
+
+		// Merge any options we have had imposed on us from outside
+		env.putAll(environment);
+
+		// Patch the environment to deal with TAVUTILS-17
+		assert env.get("PATH") != null;
+		env.put("PATH", new File(System.getProperty("java.home"), "bin")
+				+ pathSeparator + env.get("PATH"));
+		// Patch the environment to deal with TAVSERV-189
+		env.put("TAVERNA_APPHOME", workingDir.getCanonicalPath());
+		// Patch the environment to deal with TAVSERV-224
+		env.put("TAVERNA_RUN_ID", token);
+		if (interactionHost != null || local.interactionFeedURL != null
+				|| local.webdavURL != null) {
+			env.put("INTERACTION_HOST", makeInterHost(local.interactionFeedURL));
+			env.put("INTERACTION_PORT", makeInterPort(local.interactionFeedURL));
+			env.put("INTERACTION_FEED", makeInterPath(local.interactionFeedURL));
+			env.put("INTERACTION_WEBDAV",
+					local.webdavURL != null ? local.webdavURL.getPath()
+							: interactionWebdavPath);
+			String pub = makeInterPublish(local.publishURL);
+			if (pub != null && !pub.isEmpty())
+				env.put("INTERACTION_PUBLISH", pub);
+		}
+		return pb;
+	}
+
+	@Nullable
+	private static String makeInterHost(@Nullable URL url) {
+		if (url == null)
+			return interactionHost;
+		return url.getProtocol() + "://" + url.getHost();
+	}
+
+	@Nullable
+	private static String makeInterPort(@Nullable URL url) {
+		if (url == null)
+			return interactionPort;
+		int port = url.getPort();
+		if (port == -1)
+			port = url.getDefaultPort();
+		return Integer.toString(port);
+	}
+
+	@Nullable
+	private static String makeInterPublish(@Nullable URL url)
+			throws IOException {
+		if (url == null)
+			return null;
+		try {
+			URI uri = url.toURI();
+			int port = uri.getPort();
+			if (port == -1)
+				return uri.getScheme() + "://" + uri.getHost();
+			else
+				return uri.getScheme() + "://" + uri.getHost() + ":" + port;
+		} catch (URISyntaxException e) {
+			throw new IOException("problem constructing publication url", e);
+		}
+	}
+
+	@Nullable
+	private static String makeInterPath(@Nullable URL url) {
+		if (url == null)
+			return interactionFeedPath;
+		return url.getPath();
+	}
+
+	/**
+	 * Kills off the subprocess if it exists and is alive.
+	 */
+	@Override
+	public void killWorker() {
+		if (!finished && subprocess != null) {
+			final Holder<Integer> code = new Holder<>();
+			for (TimingOutTask tot : new TimingOutTask[] { new TimingOutTask() {
+				/** Check if the workflow terminated of its own accord */
+				@Override
+				public void doIt() throws IOException {
+					code.value = subprocess.exitValue();
+					accounting.runCeased();
+					buildUR(code.value == 0 ? Completed : Failed, code.value);
+				}
+			}, new TimingOutTask() {
+				/** Tell the workflow to stop */
+				@Override
+				public void doIt() throws IOException {
+					code.value = killNicely();
+					accounting.runCeased();
+					buildUR(code.value == 0 ? Completed : Aborted, code.value);
+				}
+			}, new TimingOutTask() {
+				/** Kill the workflow, kill it with fire */
+				@Override
+				public void doIt() throws IOException {
+					code.value = killHard();
+					accounting.runCeased();
+					buildUR(code.value == 0 ? Completed : Aborted, code.value);
+				}
+			} }) {
+				try {
+					tot.doOrTimeOut(DEATH_TIME);
+				} catch (Exception e) {
+				}
+				if (code.value != null)
+					break;
+			}
+			finished = true;
+			setExitCode(code.value);
+			readyToSendEmail = true;
+		}
+	}
+
+	/**
+	 * Integrated spot to handle writing/logging of the exit code.
+	 * 
+	 * @param code
+	 *            The exit code.
+	 */
+	private void setExitCode(int code) {
+		exitCode = code;
+		if (code > 256 - 8) {
+			out.println("workflow aborted, Raven issue = " + (code - 256));
+		} else if (code > 128) {
+			out.println("workflow aborted, signal=" + (code - 128));
+		} else {
+			out.println("workflow exited, code=" + code);
+		}
+	}
+
+	@Nonnull
+	private JobUsageRecord newUR() throws DatatypeConfigurationException {
+		try {
+			if (wd != null)
+				return new JobUsageRecord(wd.getName());
+		} catch (RuntimeException e) {
+		}
+		return new JobUsageRecord("unknown");
+	}
+
+	/**
+	 * Fills in the accounting information from the exit code and stderr.
+	 * 
+	 * @param exitCode
+	 *            The exit code from the program.
+	 */
+	private void buildUR(@Nonnull Status status, int exitCode) {
+		try {
+			Date now = new Date();
+			long user = -1, sys = -1, real = -1;
+			Matcher m = TimeRE.matcher(stderr.toString());
+			ur = newUR();
+			while (m.find())
+				for (int i = 1; i < 6; i += 2)
+					if (m.group(i + 1).equals("user"))
+						user = parseDuration(m.group(i));
+					else if (m.group(i + 1).equals("sys")
+							|| m.group(i + 1).equals("system"))
+						sys = parseDuration(m.group(i));
+					else if (m.group(i + 1).equals("real")
+							|| m.group(i + 1).equals("elapsed"))
+						real = parseDuration(m.group(i));
+			if (user != -1)
+				ur.addCpuDuration(user).setUsageType("user");
+			if (sys != -1)
+				ur.addCpuDuration(sys).setUsageType("system");
+			ur.addUser(System.getProperty("user.name"), null);
+			ur.addStartAndEnd(start, now);
+			if (real != -1)
+				ur.addWallDuration(real);
+			else
+				ur.addWallDuration(now.getTime() - start.getTime());
+			ur.setStatus(status.toString());
+			ur.addHost(getLocalHost().getHostName());
+			ur.addResource("exitcode", Integer.toString(exitCode));
+			ur.addDisk(sizeOfDirectory(wd)).setStorageUnit("B");
+			if (urreceiver != null)
+				urreceiver.acceptUsageRecord(ur.marshal());
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	private long parseDuration(@Nonnull String durationString) {
+		try {
+			return (long) (parseDouble(durationString) * 1000);
+		} catch (NumberFormatException nfe) {
+			// Not a double; maybe MM:SS.mm or HH:MM:SS.mm
+		}
+		long dur = 0;
+		for (String d : durationString.split(":"))
+			try {
+				dur = 60 * dur + parseLong(d);
+			} catch (NumberFormatException nfe) {
+				// Assume that only one thing is fractional, and that it is last
+				return 60000 * dur + (long) (parseDouble(d) * 1000);
+			}
+		return dur * 1000;
+	}
+
+	private void signal(@Nonnull String signal) throws Exception {
+		int pid = getPID();
+		if (pid > 0
+				&& getRuntime().exec("kill -" + signal + " " + pid).waitFor() == 0)
+			return;
+		throw new Exception("failed to send signal " + signal + " to process "
+				+ pid);
+	}
+
+	@Nullable
+	private Integer killNicely() {
+		try {
+			signal("TERM");
+			return subprocess.waitFor();
+		} catch (Exception e) {
+			return null;
+		}
+	}
+
+	@Nullable
+	private Integer killHard() {
+		try {
+			signal("QUIT");
+			return subprocess.waitFor();
+		} catch (Exception e) {
+			return null;
+		}
+	}
+
+	/**
+	 * Move the worker out of the stopped state and back to operating.
+	 * 
+	 * @throws Exception
+	 *             if it fails.
+	 */
+	@Override
+	public void startWorker() throws Exception {
+		signal("CONT");
+		stopped = false;
+	}
+
+	/**
+	 * Move the worker into the stopped state from the operating state.
+	 * 
+	 * @throws Exception
+	 *             if it fails.
+	 */
+	@Override
+	public void stopWorker() throws Exception {
+		signal("STOP");
+		stopped = true;
+	}
+
+	/**
+	 * @return The status of the workflow run. Note that this can be an
+	 *         expensive operation.
+	 */
+	@Override
+	public RemoteStatus getWorkerStatus() {
+		if (subprocess == null)
+			return Initialized;
+		if (finished)
+			return Finished;
+		try {
+			setExitCode(subprocess.exitValue());
+		} catch (IllegalThreadStateException e) {
+			if (stopped)
+				return Stopped;
+			return Operating;
+		}
+		finished = true;
+		readyToSendEmail = true;
+		accounting.runCeased();
+		buildUR(exitCode.intValue() == 0 ? Completed : Failed, exitCode);
+		return Finished;
+	}
+
+	@Override
+	public String getConfiguration() {
+		return "";
+	}
+
+	@Override
+	public String getName() {
+		return DEFAULT_LISTENER_NAME;
+	}
+
+	@Override
+	public String getProperty(String propName) throws RemoteException {
+		switch (Property.is(propName)) {
+		case STDOUT:
+			return stdout.toString();
+		case STDERR:
+			return stderr.toString();
+		case EXIT_CODE:
+			return (exitCode == null) ? "" : exitCode.toString();
+		case EMAIL:
+			return emailAddress;
+		case READY_TO_NOTIFY:
+			return Boolean.toString(readyToSendEmail);
+		case USAGE:
+			try {
+				JobUsageRecord toReturn;
+				if (subprocess == null) {
+					toReturn = newUR();
+					toReturn.setStatus(Held.toString());
+				} else if (ur == null) {
+					toReturn = newUR();
+					toReturn.setStatus(Started.toString());
+					toReturn.addStartAndEnd(start, new Date());
+					toReturn.addUser(System.getProperty("user.name"), null);
+				} else {
+					toReturn = ur;
+				}
+				/*
+				 * Note that this record is not to be pushed to the server. That
+				 * is done elsewhere (when a proper record is produced)
+				 */
+				return toReturn.marshal();
+			} catch (Exception e) {
+				e.printStackTrace();
+				return "";
+			}
+		default:
+			throw new RemoteException("unknown property");
+		}
+	}
+
+	@Override
+	public String getType() {
+		return DEFAULT_LISTENER_NAME;
+	}
+
+	@Override
+	public String[] listProperties() {
+		return Property.names();
+	}
+
+	@Override
+	public void setProperty(String propName, String value)
+			throws RemoteException {
+		switch (Property.is(propName)) {
+		case EMAIL:
+			emailAddress = value;
+			return;
+		case READY_TO_NOTIFY:
+			readyToSendEmail = parseBoolean(value);
+			return;
+		case STDOUT:
+		case STDERR:
+		case EXIT_CODE:
+		case USAGE:
+			throw new RemoteException("property is read only");
+		default:
+			throw new RemoteException("unknown property");
+		}
+	}
+
+	@Override
+	public RemoteListener getDefaultListener() {
+		return this;
+	}
+
+	@Override
+	public void setURReceiver(@Nonnull UsageRecordReceiver receiver) {
+		urreceiver = receiver;
+	}
+
+	@Override
+	public void deleteLocalResources() throws ImplementationException {
+		try {
+			if (workflowFile != null && workflowFile.getParentFile().exists())
+				forceDelete(workflowFile);
+		} catch (IOException e) {
+			throw new ImplementationException("problem deleting workflow file",
+					e);
+		}
+	}
+}
+
+/**
+ * An engine for asynchronously copying from an {@link InputStream} to a
+ * {@link Writer}.
+ * 
+ * @author Donal Fellows
+ */
+class AsyncCopy extends Thread {
+	@Nonnull
+	private BufferedReader from;
+	@Nonnull
+	private Writer to;
+	@Nullable
+	private Holder<Integer> pidHolder;
+
+	AsyncCopy(@Nonnull InputStream from, @Nonnull Writer to)
+			throws UnsupportedEncodingException {
+		this(from, to, null);
+	}
+
+	AsyncCopy(@Nonnull InputStream from, @Nonnull Writer to,
+			@Nullable Holder<Integer> pid) throws UnsupportedEncodingException {
+		this.from = new BufferedReader(new InputStreamReader(from,
+				SYSTEM_ENCODING));
+		this.to = to;
+		this.pidHolder = pid;
+		setDaemon(true);
+		start();
+	}
+
+	@Override
+	public void run() {
+		try {
+			if (pidHolder != null) {
+				String line = from.readLine();
+				if (line.matches("^pid:\\d+$"))
+					synchronized (pidHolder) {
+						pidHolder.value = parseInt(line.substring(4));
+					}
+				else
+					to.write(line + System.getProperty("line.separator"));
+			}
+			copy(from, to);
+		} catch (IOException e) {
+		}
+	}
+}
+
+/**
+ * A helper for asynchronously writing a password to a subprocess's stdin.
+ * 
+ * @author Donal Fellows
+ */
+class PasswordWriterThread extends Thread {
+	private OutputStream to;
+	private char[] chars;
+
+	PasswordWriterThread(@Nonnull Process to, @Nonnull char[] chars) {
+		this.to = to.getOutputStream();
+		assert chars != null;
+		this.chars = chars;
+		setDaemon(true);
+		start();
+	}
+
+	@Override
+	public void run() {
+		try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(to,
+				SYSTEM_ENCODING))) {
+			pw.println(chars);
+		} catch (UnsupportedEncodingException e) {
+			// Not much we can do here
+			e.printStackTrace();
+		} finally {
+			/*
+			 * We don't trust GC to clear password from memory. We also take
+			 * care not to clear the default password!
+			 */
+			if (chars != KEYSTORE_PASSWORD)
+				Arrays.fill(chars, '\00');
+		}
+	}
+}
+
+enum Property {
+	STDOUT("stdout"), STDERR("stderr"), EXIT_CODE("exitcode"), READY_TO_NOTIFY(
+			"readyToNotify"), EMAIL("notificationAddress"), USAGE("usageRecord");
+
+	private String s;
+
+	private Property(String s) {
+		this.s = s;
+		pmap.put(s, this);
+	}
+
+	@Override
+	public String toString() {
+		return s;
+	}
+
+	public static Property is(@Nonnull String s) {
+		return pmap.get(s);
+	}
+
+	@Nonnull
+	public static String[] names() {
+		return pmap.keySet().toArray(new String[pmap.size()]);
+	}
+}
+
+enum Status {
+	Aborted, Completed, Failed, Held, Queued, Started, Suspended
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/impl/utils/FilenameVerifier.java b/server-worker/src/main/java/org/taverna/server/localworker/impl/utils/FilenameVerifier.java
new file mode 100644
index 0000000..fbc3a72
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/impl/utils/FilenameVerifier.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2010-2011 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.impl.utils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utility class that handles filename validation on different target platforms.
+ * 
+ * @author Donal Fellows.
+ */
+public abstract class FilenameVerifier {
+	private FilenameVerifier(){}
+
+	static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("win");
+
+	@SuppressWarnings("serial")
+	private static final Set<String> ILLEGAL_NAMES = new HashSet<String>(){{
+		add("");
+		add("..");
+		add(".");
+		if (IS_WINDOWS) {
+			add("con");
+			add("prn");
+			add("nul");
+			add("aux");
+			for (int i = 1; i <= 9; i++) {
+				add("com" + i);
+				add("lpt" + i);
+			}
+		}
+	}};
+	@SuppressWarnings("serial")
+	private static final Set<Character> ILLEGAL_CHARS = new HashSet<Character>(){{
+		add('/');
+		for (char i=0 ; i<32 ; i++)
+			add(i);
+		if (IS_WINDOWS) {
+			add('\\');
+			add('>');
+			add('<');
+			add(':');
+			add('"');
+			add('|');
+			add('?');
+			add('*');
+		} else {
+			add(' '); // whitespace; too much trouble from these
+			add('\t');
+			add('\r');
+			add('\n');
+		}
+	}};
+	@SuppressWarnings("serial")
+	private static final Set<String> ILLEGAL_PREFIXES = new HashSet<String>(){{
+		if (IS_WINDOWS) {
+			add("con.");
+			add("prn.");
+			add("nul.");
+			add("aux.");
+			for (int i = 1; i <= 9; i++) {
+				add("com" + i + ".");
+				add("lpt" + i + ".");
+			}
+		}
+	}};
+	@SuppressWarnings("serial")
+	private static final Set<String> ILLEGAL_SUFFIXES = new HashSet<String>(){{
+		if (IS_WINDOWS) {
+			add(" ");
+			add(".");
+		}
+	}};
+
+	/**
+	 * Construct a file handle, applying platform-specific filename validation
+	 * rules in the process.
+	 * 
+	 * @param dir
+	 *            The directory acting as a root, which is assumed to be
+	 *            correctly named. May be <tt>null</tt>.
+	 * @param names
+	 *            The names of filename fragments to apply the checks to. Must
+	 *            have at least one value.
+	 * @return The file handle. Never <tt>null</tt>.
+	 * @throws IOException
+	 *             If validation fails.
+	 */
+	public static File getValidatedFile(File dir, String... names)
+			throws IOException {
+		if (names.length == 0)
+			throw new IOException("empty filename");
+		File f = dir;
+		for (String name : names) {
+			String low = name.toLowerCase();
+			if (ILLEGAL_NAMES.contains(low))
+				throw new IOException("illegal filename");
+			for (char c : ILLEGAL_CHARS)
+				if (low.indexOf(c) >= 0)
+					throw new IOException("illegal filename");
+			for (String s : ILLEGAL_PREFIXES)
+				if (low.startsWith(s))
+					throw new IOException("illegal filename");
+			for (String s : ILLEGAL_SUFFIXES)
+				if (low.endsWith(s))
+					throw new IOException("illegal filename");
+			f = new File(f, name);
+		}
+		assert f != null;
+		return f;
+	}
+
+	/**
+	 * Create a file handle where the underlying file must exist.
+	 * 
+	 * @param dir
+	 *            The directory that will contain the file.
+	 * @param name
+	 *            The name of the file; will be validated.
+	 * @return The handle.
+	 * @throws IOException
+	 *             If validation fails or the file doesn't exist.
+	 */
+	public static File getValidatedExistingFile(File dir, String name)
+			throws IOException {
+		File f = getValidatedFile(dir, name);
+		if (!f.exists())
+			throw new IOException("doesn't exist");
+		return f;
+	}
+
+	/**
+	 * Create a file handle where the underlying file must <i>not</i> exist.
+	 * 
+	 * @param dir
+	 *            The directory that will contain the file.
+	 * @param name
+	 *            The name of the file; will be validated.
+	 * @return The handle. The file will not be created by this method.
+	 * @throws IOException
+	 *             If validation fails or the file does exist.
+	 */
+	public static File getValidatedNewFile(File dir, String name)
+			throws IOException {
+		File f = getValidatedFile(dir, name);
+		if (f.exists())
+			throw new IOException("already exists");
+		return f;
+	}
+}
diff --git a/server-worker/src/main/java/org/taverna/server/localworker/impl/utils/TimingOutTask.java b/server-worker/src/main/java/org/taverna/server/localworker/impl/utils/TimingOutTask.java
new file mode 100644
index 0000000..3dd3ac1
--- /dev/null
+++ b/server-worker/src/main/java/org/taverna/server/localworker/impl/utils/TimingOutTask.java
@@ -0,0 +1,40 @@
+package org.taverna.server.localworker.impl.utils;
+
+import javax.annotation.Nullable;
+
+/**
+ * A class that handles running a task that can take some time.
+ * 
+ * @author Donal Fellows
+ * 
+ */
+public abstract class TimingOutTask extends Thread {
+	public abstract void doIt() throws Exception;
+
+	@Nullable
+	private Exception ioe;
+
+	@Override
+	public final void run() {
+		try {
+			doIt();
+		} catch (Exception ioe) {
+			this.ioe = ioe;
+		}
+	}
+
+	public TimingOutTask() {
+		this.setDaemon(true);
+	}
+
+	public void doOrTimeOut(long timeout) throws Exception {
+		start();
+		try {
+			join(timeout);
+		} catch (InterruptedException e) {
+			interrupt();
+		}
+		if (ioe != null)
+			throw ioe;
+	}
+}
\ No newline at end of file
diff --git a/server-worker/src/main/resources/security.policy b/server-worker/src/main/resources/security.policy
new file mode 100644
index 0000000..5b5c322
--- /dev/null
+++ b/server-worker/src/main/resources/security.policy
@@ -0,0 +1,11 @@
+//keystore "signers.jks";
+//grant signedBy "taverna" {
+//   permission java.util.PropertyPermission "*", "read,write";
+//   permission java.lang.RuntimePermission "shutdownHooks";
+//   permission java.lang.RuntimePermission "exitVM";
+//   permission java.io.FilePermission "<<ALL FILES>>", "read,write,execute,delete";
+//   permission java.net.SocketPermission "localhost:1024-" "accept,connect,listen";
+//};
+grant {
+   permission java.security.AllPermission "*:*";
+};
\ No newline at end of file
diff --git a/server-worker/src/test/java/org/taverna/server/localworker/impl/LocalWorkerTest.java b/server-worker/src/test/java/org/taverna/server/localworker/impl/LocalWorkerTest.java
new file mode 100644
index 0000000..7bcd92e
--- /dev/null
+++ b/server-worker/src/test/java/org/taverna/server/localworker/impl/LocalWorkerTest.java
@@ -0,0 +1,551 @@
+/*
+ * Copyright (C) 2010-2012 The University of Manchester
+ * 
+ * See the file "LICENSE" for license terms.
+ */
+package org.taverna.server.localworker.impl;
+
+import static java.util.UUID.randomUUID;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.taverna.server.localworker.impl.LocalWorker.DO_MKDIR;
+
+import java.io.File;
+import java.rmi.RemoteException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.taverna.server.localworker.api.Worker;
+import org.taverna.server.localworker.api.WorkerFactory;
+import org.taverna.server.localworker.remote.IllegalStateTransitionException;
+import org.taverna.server.localworker.remote.ImplementationException;
+import org.taverna.server.localworker.remote.RemoteDirectory;
+import org.taverna.server.localworker.remote.RemoteInput;
+import org.taverna.server.localworker.remote.RemoteListener;
+import org.taverna.server.localworker.remote.RemoteStatus;
+import org.taverna.server.localworker.server.UsageRecordReceiver;
+
+public class LocalWorkerTest {
+	LocalWorker lw;
+	static List<String> events;
+
+	public static RemoteStatus returnThisStatus = RemoteStatus.Operating;
+
+	static class DummyWorker implements Worker {
+		@Override
+		public RemoteListener getDefaultListener() {
+			return new RemoteListener() {
+				@Override
+				public String getConfiguration() {
+					return "RLCONFIG";
+				}
+
+				@Override
+				public String getName() {
+					return "RLNAME";
+				}
+
+				@Override
+				public String getProperty(String propName) {
+					return "RLPROP[" + propName + "]";
+				}
+
+				@Override
+				public String getType() {
+					return "RLTYPE";
+				}
+
+				@Override
+				public String[] listProperties() {
+					return new String[] { "RLP1", "RLP2" };
+				}
+
+				@Override
+				public void setProperty(String propName, String value) {
+					events.add("setProperty[");
+					events.add(propName);
+					events.add(value);
+					events.add("]");
+				}
+			};
+		}
+
+		@Override
+		public RemoteStatus getWorkerStatus() {
+			events.add("status=" + returnThisStatus);
+			return returnThisStatus;
+		}
+
+		@Override
+		public boolean initWorker(LocalWorker local,
+				String executeWorkflowCommand, byte[] workflow,
+				File workingDir, File inputBaclava,
+				Map<String, File> inputFiles, Map<String, String> inputValues,
+				Map<String, String> delimiters, File outputBaclava, File cmdir,
+				char[] cmpass, boolean doprov, Map<String, String> env,
+				String id, List<String> conf) throws Exception {
+			events.add("init[");
+			events.add(executeWorkflowCommand);
+			events.add(new String(workflow, "UTF-8"));
+			int dirLen = workingDir.getName().length();
+			events.add(Integer.toString(dirLen));
+			events.add(inputBaclava == null ? "<null>" : inputBaclava
+					.toString().substring(dirLen));
+			Map<String, String> in = new TreeMap<>();
+			for (Entry<String, File> name : inputFiles.entrySet())
+				in.put(name.getKey(), name.getValue() == null ? "<null>" : name
+						.getValue().getName());
+			events.add(in.toString());
+			events.add(new TreeMap<>(inputValues).toString());
+			events.add(outputBaclava == null ? "<null>" : outputBaclava
+					.getName());
+			// TODO: check cmdir and cmpass
+			// TODO: check doprov
+			// TODO: log env
+			// TODO: check delimiters
+			events.add("]");
+			return true;
+		}
+
+		@Override
+		public void killWorker() throws Exception {
+			events.add("kill");
+		}
+
+		@Override
+		public void startWorker() throws Exception {
+			events.add("start");
+		}
+
+		@Override
+		public void stopWorker() throws Exception {
+			events.add("stop");
+		}
+
+		@Override
+		public void setURReceiver(UsageRecordReceiver receiver) {
+			// We just ignore this
+		}
+
+		@Override
+		public void deleteLocalResources() throws ImplementationException {
+			// Nothing to do here
+		}
+	}
+
+	WorkerFactory factory = new WorkerFactory() {
+		@Override
+		public Worker makeInstance() throws Exception {
+			return new DummyWorker();
+		}
+	};
+
+	@Before
+	public void setUp() throws Exception {
+		lw = new LocalWorker("XWC", "WF".getBytes("UTF-8"), null, randomUUID(),
+				new HashMap<String, String>(), new ArrayList<String>(), factory);
+		events = new ArrayList<>();
+		returnThisStatus = RemoteStatus.Operating;
+	}
+
+	@After
+	public void tearDown() throws Exception {
+		lw.destroy();
+	}
+
+	private List<String> l(String... strings) {
+		return Arrays.asList(strings);
+	}
+
+	// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
+
+	@Test
+	public void testDestroy1() throws Exception {
+		lw.destroy();
+		assertEquals(l(), events);
+	}
+
+	@Test
+	public void testDestroy2() throws Exception {
+		lw.setStatus(RemoteStatus.Operating);
+		lw.destroy();
+		assertEquals(
+				l("init[", "XWC", "WF", "36", "<null>", "{}", "{}", "<null>",
+						"]", "kill"), events);
+	}
+
+	@Test
+	public void testDestroy3() throws Exception {
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Stopped);
+		lw.destroy();
+		assertEquals(
+				l("init[", "XWC", "WF", "36", "<null>", "{}", "{}", "<null>",
+						"]", "stop", "kill"), events);
+	}
+
+	@Test
+	public void testDestroy4() throws Exception {
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Finished);
+		assertEquals(
+				l("init[", "XWC", "WF", "36", "<null>", "{}", "{}", "<null>",
+						"]", "kill"), events);
+		lw.destroy();
+		assertEquals(
+				l("init[", "XWC", "WF", "36", "<null>", "{}", "{}", "<null>",
+						"]", "kill"), events);
+	}
+
+	@Test
+	public void testAddListener() {
+		Throwable t = null;
+		try {
+			lw.addListener(null);
+		} catch (Throwable caught) {
+			t = caught;
+		}
+		assertNotNull(t);
+		assertSame(ImplementationException.class, t.getClass());
+		assertNotNull(t.getMessage());
+		assertEquals("not implemented", t.getMessage());
+	}
+
+	@Test
+	public void testGetInputBaclavaFile() throws Exception {
+		assertNull(lw.getInputBaclavaFile());
+		lw.setInputBaclavaFile("IBaclava");
+		assertNotNull(lw.getInputBaclavaFile());
+		assertEquals("IBaclava", lw.getInputBaclavaFile());
+		lw.makeInput("FOO").setValue("BAR");
+		assertNull(lw.getInputBaclavaFile());
+	}
+
+	@Test
+	public void testGetInputsWithValue() throws Exception {
+		assertEquals(0, lw.getInputs().size());
+
+		lw.makeInput("FOO").setValue("BAR");
+
+		assertEquals(1, lw.getInputs().size());
+		assertEquals("FOO", lw.getInputs().get(0).getName());
+		assertNull(lw.getInputs().get(0).getFile());
+		assertNotNull(lw.getInputs().get(0).getValue());
+
+		lw.setInputBaclavaFile("BLAH");
+
+		assertEquals(1, lw.getInputs().size());
+		assertNull(lw.getInputs().get(0).getFile());
+		assertNull(lw.getInputs().get(0).getValue());
+	}
+
+	@Test
+	public void testGetInputsWithFile() throws Exception {
+		assertEquals(0, lw.getInputs().size());
+
+		lw.makeInput("BAR").setFile("FOO");
+
+		assertEquals(1, lw.getInputs().size());
+		assertEquals("BAR", lw.getInputs().get(0).getName());
+		assertNotNull(lw.getInputs().get(0).getFile());
+		assertNull(lw.getInputs().get(0).getValue());
+
+		lw.setInputBaclavaFile("BLAH");
+
+		assertEquals(1, lw.getInputs().size());
+		assertNull(lw.getInputs().get(0).getFile());
+		assertNull(lw.getInputs().get(0).getValue());
+	}
+
+	@Test
+	public void testGetListenerTypes() {
+		assertEquals("[]", lw.getListenerTypes().toString());
+	}
+
+	@Test
+	public void testGetListeners() throws Exception {
+		assertEquals(1, lw.getListeners().size());
+		RemoteListener rl = lw.getListeners().get(0);
+		assertEquals("RLNAME", rl.getName());
+		assertEquals("RLCONFIG", rl.getConfiguration());
+		assertEquals("RLTYPE", rl.getType());
+		assertEquals("[RLP1, RLP2]", Arrays.asList(rl.listProperties())
+				.toString());
+		assertEquals("RLPROP[RLP1]", rl.getProperty("RLP1"));
+		assertEquals("RLPROP[RLP2]", rl.getProperty("RLP2"));
+		rl.setProperty("FOOBAR", "BARFOO");
+		assertEquals(l("setProperty[", "FOOBAR", "BARFOO", "]"), events);
+	}
+
+	@Test
+	public void testGetOutputBaclavaFile() throws Exception {
+		assertNull(lw.getOutputBaclavaFile());
+		lw.setOutputBaclavaFile("notnull");
+		assertEquals("notnull", lw.getOutputBaclavaFile());
+		lw.setOutputBaclavaFile(null);
+		assertNull(lw.getOutputBaclavaFile());
+	}
+
+	@Test
+	public void testGetSecurityContext() throws Exception {
+		boolean md = DO_MKDIR;
+		LocalWorker.DO_MKDIR = false; // HACK! Work around Hudson problem...
+		try {
+			assertNotNull(lw.getSecurityContext());
+		} finally {
+			LocalWorker.DO_MKDIR = md;
+		}
+	}
+
+	@Test
+	public void testGetStatusInitial() {
+		assertEquals(RemoteStatus.Initialized, lw.getStatus());
+	}
+
+	@Test
+	public void testGetStatus() throws Exception {
+		assertEquals(RemoteStatus.Initialized, lw.getStatus());
+		returnThisStatus = RemoteStatus.Operating;
+		assertEquals(RemoteStatus.Initialized, lw.getStatus());
+		lw.setStatus(RemoteStatus.Operating);
+		assertEquals(RemoteStatus.Operating, lw.getStatus());
+		assertEquals(RemoteStatus.Operating, lw.getStatus());
+		returnThisStatus = RemoteStatus.Finished;
+		assertEquals(RemoteStatus.Finished, lw.getStatus());
+		returnThisStatus = RemoteStatus.Stopped;
+		assertEquals(RemoteStatus.Finished, lw.getStatus());
+		assertEquals(
+				l("init[", "XWC", "WF", "36", "<null>", "{}", "{}", "<null>",
+						"]", "status=Operating", "status=Operating",
+						"status=Finished"), events);
+	}
+
+	@Test
+	public void testGetWorkingDirectory() throws Exception {
+		RemoteDirectory rd = lw.getWorkingDirectory();
+		assertNotNull(rd);
+		assertNotNull(rd.getContents());
+		assertNull(rd.getContainingDirectory());
+		assertNotNull(rd.getName());
+		assertEquals(-1, rd.getName().indexOf('/'));
+		assertFalse("..".equals(rd.getName()));
+		assertEquals("", rd.getName());
+	}
+
+	@Test
+	public void testValidateFilename() throws Exception {
+		lw.validateFilename("foobar");
+		lw.validateFilename("foo/bar");
+		lw.validateFilename("foo.bar");
+		lw.validateFilename("foo..bar");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testValidateFilenameBad0() throws Exception {
+		lw.validateFilename("./.");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testValidateFilenameBad1() throws Exception {
+		lw.validateFilename("/");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testValidateFilenameBad2() throws Exception {
+		lw.validateFilename("");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testValidateFilenameBad3() throws Exception {
+		lw.validateFilename(null);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testValidateFilenameBad4() throws Exception {
+		lw.validateFilename("..");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testValidateFilenameBad5() throws Exception {
+		lw.validateFilename("foo/../bar");
+	}
+
+	@Test
+	public void testMakeInput() throws Exception {
+		assertEquals(0, lw.getInputs().size());
+
+		RemoteInput ri = lw.makeInput("TEST");
+
+		assertNotNull(ri);
+		assertEquals(1, lw.getInputs().size());
+		assertNotSame(ri, lw.getInputs().get(0)); // different delegates
+		assertEquals("TEST", ri.getName());
+		assertNull(ri.getFile());
+		assertNull(ri.getValue());
+
+		lw.setInputBaclavaFile("bad");
+		ri.setFile("good");
+		assertEquals("good", ri.getFile());
+		assertNull(lw.getInputBaclavaFile());
+		ri.setValue("very good");
+		assertEquals("very good", ri.getValue());
+		assertNull(ri.getFile());
+		assertNull(lw.getInputBaclavaFile());
+
+		lw.makeInput("TEST2");
+		assertEquals(2, lw.getInputs().size());
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testMakeInputFileSanity() throws Exception {
+		lw.makeInput("foo").setFile("/../bar");
+	}
+
+	@Test
+	public void testMakeListener() {
+		Throwable t = null;
+		try {
+			lw.makeListener("?", "?");
+		} catch (Throwable caught) {
+			t = caught;
+		}
+		assertNotNull(t);
+		assertSame(RemoteException.class, t.getClass());
+		assertNotNull(t.getMessage());
+		assertEquals("listener manufacturing unsupported", t.getMessage());
+	}
+
+	@Test
+	public void testSetInputBaclavaFile1() throws Exception {
+		assertNull(lw.getInputBaclavaFile());
+		lw.setInputBaclavaFile("eg");
+		assertEquals("eg", lw.getInputBaclavaFile());
+	}
+
+	@Test
+	public void testSetInputBaclavaFile2() throws Exception {
+		RemoteInput ri = lw.makeInput("foo");
+		ri.setValue("bar");
+		assertEquals("bar", ri.getValue());
+		lw.setInputBaclavaFile("eg");
+		assertNull(ri.getValue());
+	}
+
+	@Test
+	public void testSetOutputBaclavaFile1() throws Exception {
+		assertNull(lw.outputBaclava);
+		lw.setOutputBaclavaFile("foobar");
+		assertEquals("foobar", lw.outputBaclava);
+		assertEquals("foobar", lw.getOutputBaclavaFile());
+		lw.setOutputBaclavaFile("foo/bar");
+		assertEquals("foo/bar", lw.outputBaclava);
+		assertEquals("foo/bar", lw.getOutputBaclavaFile());
+		lw.setOutputBaclavaFile(null);
+		assertNull(lw.outputBaclava);
+		assertNull(lw.getOutputBaclavaFile());
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testSetOutputBaclavaFile2() throws Exception {
+		lw.setOutputBaclavaFile("/foobar");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testSetOutputBaclavaFile3() throws Exception {
+		lw.setOutputBaclavaFile("foo/../bar");
+	}
+
+	@Test
+	public void testSetStatus0() throws Exception {
+		lw.setStatus(RemoteStatus.Initialized);
+		lw.setStatus(RemoteStatus.Initialized);
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Stopped);
+		lw.setStatus(RemoteStatus.Stopped);
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Finished);
+		lw.setStatus(RemoteStatus.Finished);
+		assertEquals(
+				l("init[", "XWC", "WF", "36", "<null>", "{}", "{}", "<null>",
+						"]", "stop", "start", "kill"), events);
+	}
+
+	@Test
+	public void testSetStatus1() throws Exception {
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Stopped);
+		lw.setStatus(RemoteStatus.Finished);
+		assertEquals(
+				l("init[", "XWC", "WF", "36", "<null>", "{}", "{}", "<null>",
+						"]", "stop", "kill"), events);
+	}
+
+	@Test
+	public void testSetStatus2() throws Exception {
+		lw.setStatus(RemoteStatus.Initialized);
+		lw.setStatus(RemoteStatus.Finished);
+		assertEquals(l(), events);
+	}
+
+	@Test(expected = IllegalStateTransitionException.class)
+	public void testSetStatus3() throws Exception {
+		lw.setStatus(RemoteStatus.Initialized);
+		lw.setStatus(RemoteStatus.Finished);
+		lw.setStatus(RemoteStatus.Initialized);
+	}
+
+	@Test(expected = IllegalStateTransitionException.class)
+	public void testSetStatus4() throws Exception {
+		lw.setStatus(RemoteStatus.Initialized);
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Initialized);
+	}
+
+	@Test(expected = IllegalStateTransitionException.class)
+	public void testSetStatus5() throws Exception {
+		lw.setStatus(RemoteStatus.Initialized);
+		lw.setStatus(RemoteStatus.Stopped);
+	}
+
+	@Test(expected = IllegalStateTransitionException.class)
+	public void testSetStatus6() throws Exception {
+		lw.setStatus(RemoteStatus.Finished);
+		lw.setStatus(RemoteStatus.Stopped);
+	}
+
+	@Test(expected = IllegalStateTransitionException.class)
+	public void testSetStatus7() throws Exception {
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Stopped);
+		lw.setStatus(RemoteStatus.Initialized);
+	}
+
+	@Test
+	public void testLifecycle() throws Exception {
+		lw.makeInput("foo").setFile("foofile");
+		lw.makeInput("bar").setValue("barvalue");
+		lw.setOutputBaclavaFile("spong");
+		lw.setOutputBaclavaFile("boo");
+		lw.setStatus(RemoteStatus.Operating);
+		lw.setStatus(RemoteStatus.Finished);
+		// Assumes order of map, so fragile but works...
+		assertEquals(
+				l("init[", "XWC", "WF", "36", "<null>",
+						"{bar=<null>, foo=foofile}",
+						"{bar=barvalue, foo=null}", "boo", "]", "kill"), events);
+	}
+}
diff --git a/src/clients/tcl/taverna2server.tcl b/src/clients/tcl/taverna2server.tcl
new file mode 100644
index 0000000..02e2260
--- /dev/null
+++ b/src/clients/tcl/taverna2server.tcl
@@ -0,0 +1,419 @@
+# Tcl package providing access to Taverna 2 Server release 1.
+# This code also works as an executable script. Use it like this:
+#
+#    tclsh taverna2server.tcl http://host/taverna-server workflow.t2flow \
+#            input1Name input1.file input2Name input2.file ...
+#
+# Dependencies:
+#    Tcl    8.5
+#    TclOO  0.6.2
+#    tdom   0.8.2
+#    base64 2.4.1
+# _or_
+#    Tcl    8.6
+#    tdom   0.8.2
+#
+# Copyright (c) 2010, The University of Manchester
+#
+# $Id$
+#
+
+if {[package vsatisfies [package require Tcl 8.5] 8.6]} {
+    # Skip requiring features that are built in to 8.6
+} else {
+    package require TclOO 0.6.2
+    package require base64 2.4.1
+}
+package require http
+package require tdom 0.8.2
+
+namespace eval ::taverna2server {
+	namespace path ::oo
+    variable LogWADL 0
+    variable SNS "http://ns.taverna.org.uk/2010/xml/server/"
+    variable RNS "http://ns.taverna.org.uk/2010/xml/server/rest/"
+    namespace export service
+
+    class create RestSupportCore {
+        variable base wadls acceptedmimetypestack
+        constructor baseURL {
+            set base $baseURL
+            my LogWADL $baseURL
+        }
+
+        method ExtractError {tok} {
+            return [http::code $tok],[http::data $tok]
+        }
+
+        method OnRedirect {tok location} {
+            upvar 1 url url
+            set url $location
+            set where $location
+            my LogWADL $where
+            if {[string equal -length [string length $base/] $location $base/]} {
+                set where [string range $where [string length $base/] end]
+                return -level 2 [split $where /]
+            }
+            return -level 2 $where
+        }
+
+        method LogWADL url {
+            variable ::taverna2server::LogWADL
+            if {!$LogWADL} {
+                return;# do nothing
+            }
+            set tok [http::geturl $url?_wadl]
+            set w [http::data $tok]
+            http::cleanup $tok
+            if {![info exist wadls($w)]} {
+                set wadls($w) 1
+                puts stderr $w
+            }
+        }
+
+        method PushAcceptedMimeTypes args {
+            lappend acceptedmimetypestack [http::config -accept]
+            http::config -accept [join $args ", "]
+            return
+        }
+        method PopAcceptedMimeTypes {} {
+            set old [lindex $acceptedmimetypestack end]
+            set acceptedmimetypestack [lrange $acceptedmimetypestack 0 end-1]
+            http::config -accept $old
+            return
+        }
+
+        method DoRequest {method url {type ""} {value ""}} {
+            for {set reqs 0} {$reqs < 5} {incr reqs} {
+                if {[info exists tok]} {
+                    http::cleanup $tok
+                }
+                set tok [http::geturl $url -method $method -type $type \
+                        -query $value]
+                if {[http::ncode $tok] > 399} {
+                    set msg [my ExtractError $tok]
+                    http::cleanup $tok
+                    return -code error $msg
+                } elseif {[http::ncode $tok]>299 || [http::ncode $tok]==201} {
+                    set location {}
+                    if {[catch {
+                        set location [dict get [http::meta $tok] Location]
+                    }]} {
+                        http::cleanup $tok
+                        error "missing a location header!"
+                    }
+                    my OnRedirect $tok $location
+                } else {
+                    set s [http::data $tok]
+                    http::cleanup $tok
+                    return $s
+                }
+            }
+            error "too many redirections!"
+        }
+
+        method GET args {
+            return [my DoRequest GET $base/[join $args /]]
+        }
+
+        method POST {args} {
+            set type [lindex $args end-1]
+            set value [lindex $args end]
+            set path [join [lrange $args 0 end-2] /]
+            return [my DoRequest POST $base/$path $type $value]
+        }
+
+        method PUT {args} {
+            set type [lindex $args end-1]
+            set value [lindex $args end]
+            set path [join [lrange $args 0 end-2] /]
+            return [my DoRequest PUT $base/$path $type $value]
+        }
+
+        method DELETE args {
+            return [my DoRequest DELETE $base/[join $args /]]
+        }
+    }
+
+    class create service {
+        superclass RestSupportCore
+        self {
+            variable service
+            method address= serviceURL {
+                set service $serviceURL
+            }
+            method address {{suffix {}}} {
+                if {$suffix ne ""} {
+                    return $service/$suffix
+                } else {
+                    return $service
+                }
+            }
+
+            method SimpleGet path {
+                set tok [http::geturl [my address rest/$path]]
+                set result [http::data $tok]
+                http::cleanup $tok
+                return $result
+            }
+            forward runLimit            my SimpleGet policy/runLimit
+            forward permittedWorkflows  my SimpleGet policy/permittedWorkflows
+            forward permittedListeners  my SimpleGet policy/permittedListeners
+            method runs {} {
+                variable ::taverna2server::RNS
+                dom parse [my SimpleGet runs] doc
+                set result {}
+                foreach run [doc selectNodes -namespaces [list t2sr $RNS] \
+                        "/t2sr:runs/t2sr:run"] {
+                    lappend result [$run @xlink:href]
+                }
+                return $result
+            }
+
+            method withFile {filename as varName do script} {
+                # Verify the sugar
+                if {$as ne "as" || $do ne "do"} {
+                    return -code error "syntax error"
+                }
+                upvar 1 $varName var
+                set var [[self] new -file $filename]
+                catch {
+                    uplevel 1 $script
+                } msg opt
+                $var destroy
+                return -options $opt $msg
+            }
+        }
+        variable SNS RNS created
+        constructor {op arg} {
+            set created 0
+            my eval {namespace upvar ::taverna2server SNS SNS RNS RNS}
+
+            if {$op eq "-file"} {
+                set f [open $arg]
+                set t2flow [read $f]
+                close $f
+            } elseif {$op eq "-id"} {
+                next [[self class] address rest/runs/$arg]
+                return
+            } else {
+                return -code error "unknown operation: must be -file or -id"
+            }
+
+            dom parse $t2flow workflow
+            set contents [[$workflow documentElement] asList]
+            dom createDocumentNS $SNS workflow wrapped
+            [$wrapped documentElement] appendFromList $contents
+
+            set tok [http::geturl [[self class] address rest/runs] \
+                    -type application/xml -query [$wrapped asXML]]
+            if {[http::ncode $tok] > 399} {
+                set msg [my ExtractError $tok]
+                http::cleanup $tok
+                return -code error $msg
+            } elseif {[http::ncode $tok] < 300 && [http::ncode $tok] != 201} {
+                http::cleanup $tok
+                return -code error "unexpected OK"
+            }
+            next [dict get [http::meta $tok] Location]
+            set created 1
+            http::cleanup $tok
+        }
+        destructor {
+            if {$created && [catch {my DELETE} msg]} {
+                puts stderr "WARNING: $msg"
+            }
+        }
+
+        method status {{status ""}} {
+            if {$status eq ""} {
+                return [my GET status]
+            } else {
+                return [my PUT status text/plain $status]
+            }
+        }
+
+        method executeSynchronously {} {
+            if {[my status] eq "Initialized"} {
+                my status Operating
+            }
+            while {[my status] eq "Operating"} {
+                after 1000
+            }
+        }
+
+        method expiry {{expiry ""}} {
+            if {$expiry eq ""} {
+                set t [my GET expiry]
+            } else {
+                set t [my PUT expiry text/plain $expiry]
+            }
+            clock scan $t -format %Y-%m-%dT%H:%M:%S%z
+        }
+
+        method createTime {} {
+            clock scan [my GET createTime] -format %Y-%m-%dT%H:%M:%S%z
+        }
+        method startTime {} {
+            set t [my GET startTime]
+            if {$t eq ""} return
+            clock scan $t -format %Y-%m-%dT%H:%M:%S%z
+        }
+        method finishTime {} {
+            set t [my GET finishTime]
+            if {$t eq ""} return
+            clock scan $t -format %Y-%m-%dT%H:%M:%S%z
+        }
+
+        method property {listener property {value ""}} {
+            if {[llength [info level 0]] == 4} {
+                my GET listeners $listener properties $property
+            } else {
+                my PUT listeners $listener properties $property text/plain $value
+            }
+        }
+
+        method input {port file|value literal} {
+            switch ${file|value} {
+                file - value {
+                    # OK
+                }
+                default {
+                    return -code error "unknown input type"
+                }
+            }
+            dom createDocumentNS $RNS runInput valuedoc
+            set v [$valuedoc createElementNS $RNS ${file|value}]
+            $v appendChild [$valuedoc createTextNode $literal]
+            [$valuedoc documentElement] appendChild $v
+            my PUT input input $port application/xml [$valuedoc asXML]
+            return
+        }
+
+        method inputs file {
+            my PUT input baclava text/plain $file
+            return
+        }
+
+        method outputs file {
+            my PUT output text/plain $file
+        }
+
+        method ls {{base ""}} {
+            my PushAcceptedMimeTypes application/xml
+            set code [catch {
+                my GET wd $base
+            } result opt]
+            my PopAcceptedMimeTypes
+            if {$code} {
+                return -options $opt $result
+            }
+
+            set items {}
+            dom parse $result doc
+            set nsmap [list ts2 $SNS t2sr $RNS]
+            foreach dir [$doc selectNodes -namespaces $nsmap \
+                             "t2sr:directoryContents/t2s:dir"] {
+                lappend items [$dir @name]/
+            }
+            foreach file [$doc selectNodes -namespaces $nsmap \
+                              "t2sr:directoryContents/t2s:file"] {
+                lappend items [$file @name]
+            }
+            return $items
+        }
+
+        method get file {
+            my PushAcceptedMimeTypes application/octet-stream
+            set out [my GET wd $file]
+            my PopAcceptedMimeTypes
+            return $out
+        }
+
+        # Helper for file operations
+        method FileOp {op name {content ""}} {
+            dom createDocumentNS $RNS $op doc
+            set element [$doc documentElement]
+            $element setAttributeNS "" xmlns:t2sr $RNS
+            $element setAttributeNS $RNS t2sr:name $name
+            if {[llength [info level 0]] == 5} {
+                if {[info tclversion] eq "8.5"} {
+                    $element appendChild [$doc createTextNode \
+                            [base64::encode $content]]
+                } else {
+                    $element appendChild [$doc createTextNode \
+                            [binary encode base64 $content]]
+                }
+            }
+            return [$doc asXML]
+        }
+
+        method put {fileName contents} {
+            set path [file split $fileName]
+            #### <t2sr:upload t2sr:name="..."> base64data </t2sr:upload>
+            set message [my FileOp upload [lindex $path end] $contents]
+            return [join [lrange [my POST wd {*}[lrange $path 0 end-1] \
+                    application/xml $message] 1 end] "/"]
+        }
+
+        method mkdir {dirName} {
+            set path [file split $dirName]
+            #### <t2sr:mkdir t2sr:name="..."/>
+            set message [my FileOp mkdir [lindex $path end]]
+            return [join [lrange [my POST wd {*}[lrange $path 0 end-1] \
+                    application/xml $message] 1 end] "/"]
+        }
+    }
+}
+
+package provide taverna2server 1.0
+
+# Demonstration code
+if {[info script] ne $::argv0} {
+    return
+}
+
+namespace eval sample-code {
+    proc ReadBinaryFile filename {
+        set f [open $filename]
+        fconfigure $f -translation binary
+        set data [read $f]
+        close $f
+        return $data
+    }
+    proc WriteBinaryFile {filename data} {
+        set f [open $filename w]
+        fconfigure $f -translation binary
+        puts -nonewline $f $data
+        close $f
+    }
+
+    namespace import taverna2server::service
+    service address= [lindex $argv 0]
+    service withFile [lindex $argv 1] as run do {
+        $run mkdir in
+        foreach {name filename} [lrange $argv 2 end] {
+            # Upload the file to a synthetic name
+            $run input $name file [$run put in/f[incr i] \
+                    [ReadBinaryFile $filename]]
+        }
+        $run executeSynchronously
+        puts STDOUT:\t[$run property io stdout]
+        puts STDERR:\t[$run property io stderr]
+        puts EXIT:\t[$run property io exitcode]
+        foreach filename [$run ls out] {
+            puts FILE:\t$filename
+            # Ignore subdirectories
+            if {[string match */ $filename]} continue
+            # Download the file
+            WriteBinaryfile [file tail $filename] [$run get out/$filename]
+        }
+    }
+}
+
+return
+
+# Local Variables:
+# mode: tcl
+# indent-tabs-mode: nil
+# End:
diff --git a/src/main/signing/signing.jks b/src/main/signing/signing.jks
new file mode 100644
index 0000000..34ce02d
--- /dev/null
+++ b/src/main/signing/signing.jks
Binary files differ
diff --git a/tomcatcontext.xsd b/tomcatcontext.xsd
new file mode 100644
index 0000000..3fa73c4
--- /dev/null
+++ b/tomcatcontext.xsd
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xs:schema targetNamespace="urn:tomcat6" xmlns="urn:tomcat6"
+	xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="unqualified"
+	attributeFormDefault="unqualified">
+	<xs:element name="Context">
+		<xs:complexType>
+			<xs:choice minOccurs="0" maxOccurs="unbounded">
+				<xs:element ref="Valve" />
+				<xs:element ref="Parameter" />
+				<xs:element ref="Environment" />
+				<xs:element ref="Resource" />
+			</xs:choice>
+			<xs:attribute name="path" type="xs:string" use="required" />
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="Parameter">
+		<xs:complexType>
+			<xs:sequence />
+			<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
+			<xs:attribute name="value" type="xs:string" use="required" />
+			<xs:attribute name="description" type="xs:string" use="optional" />
+			<xs:attribute name="override" type="xs:boolean" default="false" />
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="Environment">
+		<xs:complexType>
+			<xs:sequence />
+			<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
+			<xs:attribute name="value" type="xs:string" use="required" />
+			<xs:attribute name="type" type="xs:NMTOKEN" use="required" />
+			<xs:attribute name="description" type="xs:string" use="optional" />
+			<xs:attribute name="override" type="xs:boolean" default="false" />
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="Resource">
+		<xs:complexType>
+			<xs:sequence />
+			<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
+			<xs:attribute name="type" type="xs:NMTOKEN" use="required" />
+			<xs:attribute name="description" type="xs:string" use="optional" />
+			<xs:attribute name="override" type="xs:boolean" default="false" />
+			<xs:attribute name="auth">
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="Application" />
+						<xs:enumeration value="Container" />
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:attribute name="scope" default="Shareable">
+				<xs:simpleType>
+					<xs:restriction base="xs:NMTOKEN">
+						<xs:enumeration value="Unshareable" />
+						<xs:enumeration value="Shareable" />
+					</xs:restriction>
+				</xs:simpleType>
+			</xs:attribute>
+			<xs:anyAttribute namespace="##any" processContents="lax" />
+		</xs:complexType>
+	</xs:element>
+	<xs:element name="Valve">
+		<xs:complexType>
+			<xs:sequence />
+			<xs:attribute name="className" type="xs:NMTOKEN" use="required" />
+			<xs:anyAttribute namespace="##any" processContents="lax" />
+		</xs:complexType>
+	</xs:element>
+</xs:schema>
\ No newline at end of file
diff --git a/usage.docx b/usage.docx
new file mode 100644
index 0000000..065813b
--- /dev/null
+++ b/usage.docx
Binary files differ
diff --git a/usage.pdf b/usage.pdf
new file mode 100644
index 0000000..4ad64de
--- /dev/null
+++ b/usage.pdf
Binary files differ